原型
每個函數(構造函數)都有一個 prototype 屬性,指向該函數(構造函數)的原型對象。實例沒有 prototype 屬性,但是有 __proto__
屬性。函數同時有 prototype 和 __proto__
屬性。
function Person(name) {
this.name = name;
}
let person = new Person('xiaoming');
person.__proto__ === Person.prototype; // true
// 因爲函數也是對象,所以也有 __proto__ 屬性,指向 Function.prototype
Person.__proto__ === Function.prototype //true
由字面量創建的普通對象是Object的實例,
由 function 關鍵字聲明的函數是Function的實例
let obj = {};
obj.__proto__ === Object.prototype; // true
function fn() {};
fn.__proto__ === Function.prototype; // true
__proto__
屬性雖然在ECMAScript 6語言規範中標準化,但是不推薦被使用,現在更推薦使用Object.getPrototypeOf
,Object.getPrototypeOf(obj)
也可以獲取到obj對象的原型。
Object.getPrototypeOf(person) === person.__proto__; // true
原型鏈
當訪問一個對象的屬性時,先在對象的本身找,找不到就去對象的原型上找,如果還是找不到,就去對象的原型(原型也是對象,也有它自己的原型)的原型上找,如此繼續,直到找到爲止,或者查找到最頂層的原型對象中也沒有找到,就結束查找,返回 undefined。
這條由對象及其原型組成的鏈就叫做原型鏈。
原型鏈存在的意義就是繼承:訪問對象屬性時,在對象本身找不到,就在原型鏈上一層一層找。說白了就是一個對象可以訪問其他對象的屬性。
繼承存在的意義就是屬性共享:好處有二:一是代碼重用,字面意思;二是可擴展,不同對象可能繼承相同的屬性,也可以定義只屬於自己的屬性
原型鏈的頂端
Object 類型
Object.prototype.__proto__
是原型鏈的頂端了,指向 null。
let obj = {};
obj.__proto__ === Object.prototype; //true
Object.prototype.__proto__ === null; //true
Function 類型
對象都是被構造函數創建的,函數對象的構造函數就是 Function ,注意這裏 F 是大寫。
函數的最大的構造函數爲 Function,其也有 prototype 屬性,由於 Function.prototype
爲一個對象,所以 Function.prototype.__proto__
指向 Object.prototype
。
Array.__proto__ === Function.prototype; //true
Object.__proto__ === Function.prototype; // true
同時,由於 Function 也是一個對象,所以其也有 __proto__
屬性,規定其屬性指向 Function.prototype
。
function fn() {};
fn.__proto__ === Function.prototype;
Function.prototype.__proto__ === Object.prototype; // true
Function.__proto__ === Function.prototype; // true
instanceOf 的原理
作用:判斷實例對象與構造函數之間是否爲繼承關係
原理:判斷實例對象的 __proto__
屬性和構造函數的 prototype 屬性,是否爲同一個引用。
注意點:如果存在多層繼承關係,instanceof 會一直沿着原型鏈往上找。
function Person () {};
let person = new Person();
person instanceof Person; // true
person instanceof Object; // true
person.__proto__ === Person.prototype; // true
Person.prototype.__proto__ === Object.prototype; // true
由上面可以看出,如果用 instanceof 是無法準確的判斷出實例對象是否直接繼承自該構造函數。此時需要使用 constructor。
person.__proto__.constructor === Person; // true
person.__proto__.constructor === Object; // false
小結:
Object.prototype.__proto__ === null
;Array.__proto__ === Function.prototype
;Object.__proto__ === Function.prototype
;Function.prototype.__proto__ === Object.prototype
;Function.__proto__ === Function.prototype
;- instanceof 不能準確的判斷實例是否直接繼承於構造函數
繼承
1.原型鏈法
基本原理是:將父類的實例賦值給子類的原型。
Son.prototype = new Father()
// 父類
function Staff() {
this.company = 'tianchuang';
this.list = [];
}
// 父類的原型
Staff.prototype.getComName = function() {
return this.company;
};
// 子類
function Coder(name, skill) {
this.name = name;
this.skill = skill;
}
// 繼承 Staff
Coder.prototype = new Staff();
// 因爲子類原型的指向已經變了,所以需要把子類原型的contructor指向子類本身
Coder.prototype.constructor = Coder;
// 給子類原型添加屬性
Coder.prototype.getInfo = function() {
return {
name: this.name,
skill: this.skill
};
};
let coder = new Coder('小明', 'javascript');
coder.getInfo(); // {name: '小明', skill: 'javascript'}
coder.getComName(); // 'tianchuang'
這種繼承方式的缺點:
子類的實例可以訪問父類的私有屬性,子類的實例還可以更改該屬性,這樣不安全。
let coder1 = new Coder('zhangsan', 'python');
let coder2 = new Coder('liutian', 'java');
coder1.list; // []
coder1.list.push(1); //[1]
coder2.list // [1]
2. 借用構造函數
原理:在子類構造函數中,使用call來將子類的this綁定到父類中去
// 父類
function Staff() {
this.company = 'tianchuang';
this.list = [];
}
// 父類的原型
Staff.prototype.getComName = function() {
return this.company;
};
// 子類
function Coder(name, skill) {
Staff.call(this);
this.name = name;
this.skill = skill;
}
let coder = new Coder('xiaoming', 'java');
let coder2 = new Coder('zhaosan', 'c');
coder.getComName(); // Uncaught TypeError: coder.getComName is not a function
coder.list; //[]
coder.list.push(1); //[1]
coder2.list; //[]
優點:
借用構造函數法可以解決原型中引用類型值被修改的問題;
缺點:
只能繼承父對象的實例屬性和方法,不能繼承父對象原型屬性和方法
組合繼承
將原型繼承和借用構造函數兩種方式組合起來
// 父類
function Staff() {
this.company = 'tianchuang';
this.list = [];
}
// 父類的原型
Staff.prototype.getComName = function() {
return this.company;
};
// 子類
function Coder(name, skill) {
Staff.call(this); // 第一次調用
this.name = name;
this.skill = skill;
}
Coder.prototype = new Staff();// 第二次調用
Coder.prototype.constructor = Coder;
let coder = new Coder('xiaoming', 'java');
let coder2 = new Coder('zhaosan', 'c');
coder.getComName();
coder.list; //[]
coder.list.push(1); //[1]
coder2.list; //[]
優點:
可以保證每個函數有自己的屬性,可以解決原型中引用類型值被修改的問題;
子類的實例可以繼承父類原型上面的屬性和方法
缺點:
在實例化子類的過程中,父類構造函數調用了兩次
寄生組合式繼承(推薦)
所謂寄生繼承:通過 Object.create() 將子類的原型繼承到父類的原型上。
Coder.prototype = Object.create(Staff.prototype);
完整實例:
// 父類
function Staff() {
this.company = 'tianchuang';
this.list = [];
}
// 父類的原型
Staff.prototype.getComName = function() {
return this.company;
};
// 子類
function Coder(name, skill) {
Staff.call(this);
this.name = name;
this.skill = skill;
}
Coder.prototype = Object.create(Staff.prototype);
Coder.prototype.constructor = Coder;
let coder = new Coder('xiaoming', 'java');
let coder2 = new Coder('zhaosan', 'c');
coder.getComName(); // 'tianchuang'
coder.list; //[]
coder.list.push(1); //[1]
coder2.list; //[]
《JavaScript 高級程序設計》中對寄生組合式繼承的誇讚是:
這種方式的高效率體現在紙雕用了一次父類構造函數,並且因此避免了在父類的prototype 上面創建不必要的、多餘的屬性。
同時,原型鏈還能保持不變,可以正常使用 instanceof 和 isPrototypeOf 。
開發人員普遍認爲寄生組合式繼承是引用類型最理想的繼承範式。
class extend 繼承
ES6 中有了類的概念,可以通過 class
聲明一個類,通過 extends
關鍵字來實現繼承關係。
class 與 ES5 構造函數的主要區別:
- class 只能通過 new 來調用,而構造函數則可以直接調用;
- class 內部所有定義的方法,都是不可枚舉的(non-enumerable)
class Parent {
constructor(name) {
this.name = name;
}
static say() {
return 'hello';
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 調用父類的 constructor(name)
this.age = age;
}
}
var child1 = new Child('kevin', '18');
console.log(child1);
值得注意的是:
- super 關鍵字表示父類的構造函數,相當於 ES5 的
Parent.call(this)
。
子類必須在 constructor 方法中調用 super 方法,否則新建實例時會報錯。這是因爲子類沒有自己的 this 對象,而是繼承父類的 this 對象,然後對其進行加工,如果不調用 super 方法,子類就得不到 this 對象。
正是因爲這個原因,在子類的構造函數中,只有調用 super 之後,纔可以使用 this 關鍵字,否則會報錯。
- 子類的
__proto__
屬性指向父類
因此,子類可以繼承父類的靜態方法。
Child.__proto__ === Parent; // true
Child.say(); // hello
- 子類的原型的
__proto__
,總是指向父類的 prototype 屬性
Child.prototype.__proto__ === Parent.prototype; // true
我們會發現,相比寄生組合式繼承,ES6 的 class 多了一個 Object.setPrototypeOf(Child, Parent) 的步驟,即 Child.__proto__ = Parent
。
Babel 編譯 class extends
ES6 代碼
class Parent {
constructor(name) {
this.name = name;
}
static say() {
return 'hello';
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 調用父類的 constructor(name)
this.age = age;
}
}
var child1 = new Child('kevin', '18');
Babel 編譯爲:
'use strict';
function _possibleConstructorReturn(self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
}
function _inherits(subClass, superClass) {
// extend 的繼承目標必須是函數或者是 null
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
// 類似於 ES5 的寄生組合式繼承,使用 Object.create 方法,設置子類 prototype 屬性的 __proto__ 屬性指向父類的 prototype 屬性,同時將 子類原型的構造器指向子類本身
subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } });
// 設置子類的 __proto__ 屬性指向父類
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Parent = function Parent(name) {
_classCallCheck(this, Parent);
this.name = name;
};
var Child = function(_Parent) {
_inherits(Child, _Parent);
function Child(name, age) {
_classCallCheck(this, Child);
// 調用父類的 constructor(name),相當於 Parent.call(this, name)
var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name));
_this.age = age;
return _this;
}
return Child;
}(Parent);
var child1 = new Child('kevin', '18');
以上代碼,關鍵點在於 _inherits
函數和 _possibleConstructorReturn
函數。
-
_inherits
方法使用Object.create
方法實現了子類原型繼承於父類原型,這就是寄生方式的繼承,建立了Child和Parent的原型鏈關係。 -
_possibleConstructorReturn
方法使用Parent.call(this)
將子類構造函數(Child)的this綁定到父類構造函數(Parent)中去,通過這樣就將父類的屬性添加到子類的實例對象上去了。
Object.create(superObj) 、Object.setPrototypeOf(subObj, superObject)
Object.create(superObj)
Object.create()
用於從原型對象(superObj)上生成新的實例對象(subObj),其實質是 subObj.__proto__ = superObj
,則 subObj 也能夠讀取到 superObj 的屬性。返回值是一個新對象。
let superObj = {name: 'super'};
let subObj = Object.create(superObj);
console.log(subObj.name); // super
console.log(subObj.__proto__ === superObj); // true
Object.create()
其實可以接受兩個參數,第二個參數表示要添加到新創建對象的屬性。注意這裏是給新創建的對象即返回值添加屬性,而不是在新創建對象的原型對象上添加。
const o = Object.create({}, {
p: {
value: 42,
enumerable: false,
// 該屬性不可寫
writable: false,
configurable: true
}
});
o.p = 24;
console.log(o.p); // 42
Object.create
函數的實現原理:
Object.create = function(obj, props) {
function F() {};
F.prototype = obj;
let newObj = new F();
Object.assign(newObj, props);
return newObj;
};
Object.setPrototypeOf(subObj, superObject)
Object.setPrototypeOf
跟 Object.create
的作用是一樣的,只不過 Object.setPrototypeOf
是給現有的對象設置原型,返回一個新對象,接受兩個參數。
let subObj = {age: 19};
let superObj = {name: 'tian'};
Object.setPrototypeOf(subObj, superObj);
console.log(subObj.name); // tian
console.log(subObj.__proto__ === superObj); // true
Object.getPrototypeOf(obj)
Object.getPrototypeOf
方法返回一個對象的原型。這是獲取原型對象的標準方法。
let obj = {};
Object.getPrototypeOf(obj); // Object.prototype
function fn() {};
Object.getPrototypeOf(fn); // Function.prototype