傳統的 javascript 中只有對象,沒有類的概念。它是基於原型的面嚮對象語言。 原型對象特點就是將自身的屬性共享給新對象。這樣的寫法相對於其它傳統面向 對象語言來講,很有一種獨樹一幟的感腳!非常容易讓人困惑!
首先大家要明白,爲什麼要出現 class ?
首先回顧我們的 JavaScript 編程思想的發展史。 從 JS 誕生之時,剛開始做的就是面向過程的編程,把一個問題給解釋清楚了, 幾行 js 就可以搞定。隨着 js 的發展以及瀏覽器對於 js 執行速度越來越高。我們 對於 js 實現的功能越來越多,伴隨代碼量也會越來越多,我們仍然使用面向過 程式的編程方案,就會有問題。 我們製作打怪獸遊戲:
攻擊 function attack()
逃跑 function escape()
加血 function resume()
會關注這三個方法的具體實現,並且反覆調用這些方法完成遊戲。這是面向過程 的思路。 當我們增加打怪獸的使用者時,單純這幾個方法不足以完成多人遊戲。 我們需要更高級的思想面向對象,儘管 javascript 不具備面向對象的特徵(繼承, 封裝,多態)。但是我們可以採用類似的這種思想的方式去改寫這樣的需求。
function BraveMan() {
}
BraveMan.prototype.attack = function () {}
Braveman.prototype.escape = function () {}
BraveMan.prototype.resume = function() {}
基於我們原來過程的基礎之上,我們封裝這樣的構造函數,用於產生可以多次執 行這樣過程的對象。這樣的話我們的打怪獸,不單純如何打怪獸(面向過程), 而是變成了 誰能打怪獸(類似面向對象的思想),這裏的勇者就是我們想要的 對象,可以多次實例化。這也是這種思想給我們帶來的好處,模塊化,可擴展等 好處。
在我們的日常 codeing 中,很多大的項目當中都需要使用這種類似面向對象的思 想去進行編程,在 Javascript 中不存在面向對象,我們採用的類似面向對象的過 程叫,基於原型編程,下面是工作中的存在的代碼(音樂播放器中的兩個模塊):
// ControlIndex 模塊
function ControlIndex(len) {
// this 指向 Control 對象
// Control 上掛載兩個屬性一個爲 index 初始值爲 0
// len 爲當前數據長度
this.index = 0;
this.len = len;
}
// 將方法掛在對象得原型上
ControlIndex.prototype = {
prev: function () {
return this.getIndex(-1);
},
next: function () {
return this.getIndex(1);
},
getIndex: function (val) {
var index = this.index;
var len = this.len;
var curIndex = (index + val + len) % len;
this.index = curIndex;
return curIndex;
}
}
// AudioManager 模塊
function AudioManager(){
// 創建一個音頻 audio 對象
this.audio = new Audio();
// 默認音樂狀態暫停
this.status = 'pause';
}
AudioManager.prototype = {
play:function(){
this.audio.play();
this.status = 'play';
},
pause:function(){
this.audio.pause();
this.status = 'pause';
},
setAudioSource:function(src){
this.audio.src = src;
// 重新加載音頻元素
this.audio.load();
}
}
在這裏使用的基於原型編程的一個例子,將一個項目中的不同模塊分解,每個模 塊使用這種方式進行編程。複用性更好,同時分工明確,不單單是 A 方法做完 B 方法做,而是統一的交給管理對象去執行這些方法。
對於 Javascript 的函數來說,我們的函數是由很多不完善的地方,首先我們通過 function 聲明的函數,可以聲明,普通方法,構造函數,單純是這兩個函數我們 是沒有犯法區分的,在之前我們默認採用大頭峯式寫法,表明構造函數,但是必 須每個遵守纔可以,而且容易出問題,當把構造函數當成普通函數來執行,會產 生全局變量,還可能會報錯。
基於上面的種種現象: 我們需要類似的面向對象的思想進行編程。 我們的原始的 function 聲明的構造函數,約束性,安全性不夠,不足以支撐這 種思想。
所以在新的語法規範當中 ECMAScript6 中引入了 class,基於原有 function 的方式 的語法糖,讓我們使用起來,更方便,更安全,目的性更強。
而在 ES6 中引入了 Class(類)這個概念,通過 class 關鍵字可以定義類。該關鍵 字的出現使得其在對象寫法上更加清晰,更像是一種面向對象的語言。ES6 的寫 法就會是這個樣子:
class Person{//定義了一個名字爲 Person 的類
constructor(name,age){//constructor 是一個構造方法,用來接收參數
this.name = name;//this 代表的是實例對象
this.age=age;
}
say(){//這是一個類的方法,注意千萬不要加上 function
return "我的名字叫" + this.name+"今年"+this.age+"歲了";
}
}
var obj=new Person("yinchengnuo",18);
console.log(obj.say());//我的名字叫 yinchengnuo今年 18 歲了
但是要注意的是:
1.在類中聲明方法的時候,千萬不要給該方法加上 function 關鍵字
2.方法之間不要用逗號分隔,否則會報錯
通過以下代碼可以看出類實質上就是一個函數。類自身指向的就是構造函數。所 以可以認爲 ES6 中的類其實就是構造函數的另外一種寫法!
console.log(typeof Person);//function
console.log(Person===Person.prototype.constructor);//true
以下代碼說明構造函數的 prototype 屬性,在 ES6 的類中依然存在着。
console.log(Person.prototype);//輸出的結果是一個對象
實際上類的所有方法都定義在類的 prototype 屬性上。 一起來證明一下:
Person.prototype.say=function(){//定義與類中相同名字的方法。成功實現了覆蓋!
return "我是來證明的,你叫" + this.name+"今年"+this.age+"歲了";
}
var obj=new Person("yinchengnuo",23);
console.log(obj.say());//我是來證明的,你叫 yinchengnuo今年 23 歲了
當然也可以通過 prototype 屬性對類添加方法。如下:
Person.prototype.laodeng=function(){
return "我是通過 prototype 新增加的方法,名字叫 yyy";
}
var obj=new Person("ycn",18);
console.log(obj.laodeng());//我是通過 prototype 新增加的方法,名字叫yyy
還可以通過 Object.assign 方法來爲對象動態增加方法:
Object.assign(Person.prototype,{
getName:function(){
return this.name;
},
getAge:function(){
return this.age;
}
})
var obj=new Person("ycn",1);
console.log(obj.getName());//ycn
console.log(obj.getAge());//1
constructor 方法是類的構造函數的默認方法,通過 new 命令生成對象實例時, 自動調用該方法。
class Box{
constructor(){
console.log("啦啦啦,今天天氣好晴朗");//當實例化對象時該行代碼會執行。
}
}
var obj=new Box();
constructor 方法如果沒有顯式定義,會隱式生成一個 constructor 方法。所以即 使你沒有添加構造函數,構造函數也是存在的。constructor 方法默認返回實例對 象 this,但是也可以指定 constructor 方法返回一個全新的對象,讓返回的實例對 象不是該類的實例。
class Desk{
constructor(){
this.xixi="666";
}
}
class Box{
constructor(){
return new Desk();// 這裏沒有用 this,直接返回一個全新的對象
}
}
var obj=new Box();
console.log(obj.xixi);//666
constructor 中定義的屬性可以稱爲實例屬性(即定義在 this 對象上),constructor 外聲明的屬性都是定義在原型上的,可以稱爲原型屬性(即定義在 class 上)。 hasOwnProperty()函數用於判斷屬性是否是實例屬性。其結果是一個布爾值, true 說明是實例屬性,false 說明不是實例屬性。in 操作符會在通過對象能夠訪 問給定屬性時返回 true,無論該屬性存在於實例中還是原型中。
class Box{
constructor(num1,num2){
this.num1 = num1;
this.num2=num2;
}
sum(){
return num1+num2;
}
}
var box=new Box(12,88);
console.log(box.hasOwnProperty("num1"));//true
console.log(box.hasOwnProperty("num2"));//true
console.log(box.hasOwnProperty("sum"));//false
console.log("num1" in box);//true
console.log("num2" in box);//true
console.log("sum" in box);//true
console.log("say" in box);//false
類的所有實例共享一個原型對象,它們的原型都是 Person.prototype,所以 proto 屬性是相等的。
class Box{
constructor(num1,num2){
this.num1 = num1;
this.num2=num2;
}
sum(){
return num1+num2;
}
}
//box1 與 box2 都是 Box 的實例。它們的__proto__都指向 Box 的 prototype
var box1=new Box(12,88);
var box2=new Box(40,60);
console.log(box1.__proto__===box2.__proto__);//true
由此,也可以通過 proto 來爲類增加方法。使用實例的 proto 屬性改寫原型,會 改變 Class 的原始定義,影響到所有實例,所以不推薦使用!
class Box{
constructor(num1,num2){
this.num1 = num1;
this.num2=num2;
}
sum(){
return num1+num2;
}
}
var box1=new Box(12,88);
var box2=new Box(40,60);
box1.__proto__.sub=function(){
return this.num2-this.num1;
}
console.log(box1.sub());//76
console.log(box2.sub());//20
class 不存在變量提升,所以需要先定義再使用。因爲 ES6 不會把類的聲明提升 到代碼頭部,但是 ES5 就不一樣,ES5 存在變量提升,可以先使用,然後再定義。
//ES5 可以先使用再定義,存在變量提升
new A();
function A(){
}
//ES6 不能先使用再定義,不存在變量提升 會報錯
new B();//B is not defined
class B{
}
這是我們對 ES6 中 class(類)的概念的瞭解,既然提出了類,這個類又是怎麼實現的呢?
在這首先要了解一下類的繼承:有三種屬性,公有屬性,私有屬性, 靜態屬性(Es7)/靜 態類(Es6)
繼承公有屬性:
function Parent(){
this.name = 'parent';
}
new Parent();//this 指向當前實例
Parent() //this 指向 window
function Child(){
this.age = 9;
Parent.call(this);//相當於 this.name = 'parent' //繼承父類屬性
}
繼承父類屬性:
function Parent(){
this.name = 'parent';
}
Parent.prototype.eat = function(){
console.log('eat')
}
function Child(){
this.age = 9;
Parent.call(this);//相當於 this.name = 'parent' //繼承父類屬性
}
Child.prototype.smoking = function(){
console.log('smoking')
}
Child.prototype = Parent.prototype;//這個不叫繼承
//因爲這樣如果改變 Child.prototype 加屬性,Parent.prototype 的實例也會有這個屬性,,
此時這兩者屬於兄弟關係
Child.prototype._proto_ = Parent.prototype // 方法一
//object.create
Child.prototype = object.create(Parent.prototype); // 常用,方法二
function create(parentPrototype,props){
function Fn(){}
Fn.prototype = parentPrototype;
let fn = new Fn();
for(let key in props){
Object.defineProperty(fn,key,{
...props[key],
enumerable:true
});
}
return fn();
}
Child.prototype = create(Parent.prototype,{
constructor: {
value:Child
}
})
繼承公有屬性和私有屬性:
之前的繼承都是原型鏈的繼承,聖盃模式 在 Class 中 的繼承,有什麼不一樣了呢, 在 ES5 中真正的繼承應該是什麼樣呢:
function Animal (weight, name) {
this.name = name
this.age = 0
this.weight = 10
}
Animal.prototype.eat = function () {
console.log('animal eat')
}
Animal.prototype.drink = function () {
console.log('a d')
}
function Person(name, weight) {
Animal.call(this, weight, name)
}
Person.prototype = Object.create(Animal.prototype)
Person.prototype.eat = function () {
console.log('Person eat')
}
var p = new Person('dxm', 70)
這裏有兩點,要注意的地方,首先是
1 父類構造函數 爲什麼要 call
2 原型需要重寫。 在 class 中繼承就變的簡單了許多,同比上面的例子通過 class 來實現
class Animal {
constructor(name) {
this.name = name
}
eat() {
console.log('a e')
}
drink() {
console.log('a d')
}
}
class Person extends Animal {
constructor(name) {
super(name)
}
eat() {
console.log('p e')
}
}
在這裏實現之後,多了兩個大家不認識的次, extends super extends 後面跟的就是我們要繼承的內容
super 有些注意點 子類也叫派生類,必須在 constructor 中調用 super 函數,要不無法使用 this, 準確的說子 類是沒有 this 的 就算是繼承之後什麼也不寫,默認的也會填上去
class Person extends Animal {
constructor(...arg) {
super(...arg)
}
}
在調用 super 之前不可以使用 this 回報錯 super 可以作爲對象使用,指向的是 父類的原型 super 調用父類方法時,會綁定子類的 this。
類只能 new:
class Parent{
//私有屬性
constructor(){
this.name = 'parent',
this.age = '40'
}
//公有屬性,原型上的方法
eat(){
console.log('eat')
}
//靜態方法/屬性 es6/es7
//屬於類上的方法 Child.a()
static b(){
return 2
}
}
new Parent();
class Child extends Parent{ //繼承父親的私有和公有
//私有屬性
constructor(){
super() // 相當於 Parent.call(this)
this.name = 'child'
}
//公有屬性,原型上的方法
smoking(){
console.log('smoking')
}
//靜態方法/屬性 es6/es7
//屬於類上的方法 Child.a()
static a(){
return 1
}
}
let child = new Child();
console.log(child.name,child.age,child.eat(),child.smoking,Child.b())
//類可以繼承公有,私有和靜態
//父類的構造函數中返回類一個引用類型,會把這個引用類型作爲子類的 this
我們首先寫一個創建類的函數:
//檢測實例是不是 new 出來的
function _classCallCheck(instance,constructor){
if(!(instance instanceof constructor)){
throw new Error('Class constructor Child cannot be invoked without new')
}
}
//constructor 構造函數
//prprotoPropertys 構造函數原型
//staticPropertys 靜態方法的描述
function definePropertys(target,arr){
for(let i=0;i<arr.length;i++){
Object.defineProperty(target,arr[i].key,{
...arr[i],
configurable : true,
enumerable : true,
writable:true
})
}
}
function _createClass(constructor,protoPropertys,staticPropertys){
if(protoPropertys.length > 0){
definePropertys(constructor.prototype,protoPropertys)
}
if(staticPropertys.length > 0){
definePropertys(constructor,staticPropertys)
}
}
let Parent = function(){
//寫邏輯
function P(){
_classCallCheck(this,P)
this.name = 'parent';
//return {}
}
_createClass(P,//屬性描述器
[
{
key: 'eat',
value: function () {
console.log('喫')
}
}
],
[
{
key:'b',
value:function () {
return 2;
}
}
]
)
return P;
}()
let p = new Parent();
console.log(p.eat())
這樣我們的類就創建完成了