原文 developer.mozilla.org/en-US/docs/…
JS並不提供類的實現(class關鍵字是在ES2015引入的但僅是語法糖,JS本身仍是基於原型的)。
當談到繼承,JS僅有一種結構:物件。每一個物件(object)都有一個指向其他物件的私有屬性,這個屬性叫做這個物件的原型(prototype)。
幾乎所有的物件都是Object的例項,也可以說所有物件都繼承自Object,它是原型鏈的頂端。Object.protorype指向null,null是沒有prototype的。
當我們去訪問某一個物件的屬性時,JS會從此物件沿著原型鏈往上找,直到找不到為止。
Prototype:
----------以下重點---------最難理解的部分---------
obj.[[Prototype]]是ES標準的記法,obj.__proto__是瀏覽器廠商實際使用的記法,Object.getPrototypeOf(obj)/Object.setPrototypeOf(child, parent) 是ES6提供的訪問器,這三種寫法本質上是同一種東西。
someFunction.prototype跟上面的不是一種東西,它是一種抽象的泛指,表示所有用someFunction當作構造器創造出來的例項的__proto__。他倆的關係類似於class和instance的關係。
如果你仔細觀察的話,會發現.prototype總是跟在function後面,.__proto__總是跟在創造出的物件後:
function doSomething(){}
doSomething.prototype.foo = "bar";
console.log( doSomething.prototype );複製程式碼
結果:
{
foo: "bar",
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}複製程式碼
當建立例項後
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value"; // add a property onto the object
console.log( doSomeInstancing );複製程式碼
結果
{
prop: "some value",
__proto__: {
foo: "bar",
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
}複製程式碼
你可以看到,如果你找doSomeInstancing.__proto__,他指向的是doSomething.prototype。所以當你定義一個方法或類的時候,加在prototype上的屬性會變成以後這個類的例項(或後代)的原型鏈__proto__上的屬性。
例項化或者繼承在JS中是一樣的存在,都會是創造一個新的物件然後讓他加入原型鏈。
當你嘗試去在某物件中找屬性,JS就會從這個物件開始遍歷原型鏈直到最頂端,所以說繼承的越多(原型鏈越長)效能越差。
當寫任何一個東西的時候,JS會自動幫你新增原型鏈,這就是為什麼說幾乎所有的物件都是Object的例項。除非你用var freshObj = Object.create(null);創造出的就是沒有繼承Object的
var o = {a: 1};
// The newly created object o has Object.prototype as its [[Prototype]]
// o has no own property named 'hasOwnProperty'
// hasOwnProperty is an own property of Object.prototype.
// So o inherits hasOwnProperty from Object.prototype
// Object.prototype has null as its prototype.
// o ---> Object.prototype ---> null
// 再次提醒:上面體現的是o.__proto__ === Object.prototype, 而不是o.prototype,這玩意是undefined
var b = ['yo', 'whadup', '?'];
// Arrays inherit from Array.prototype
// (which has methods indexOf, forEach, etc.)
// The prototype chain looks like:
// b ---> Array.prototype ---> Object.prototype ---> null
// 同理你在用Array的例項,上面語法其實是new Array('yo', 'whadup', '?')
function f() {
return 2;
}
// Functions inherit from Function.prototype
// (which has methods call, bind, etc.)
// f ---> Function.prototype ---> Object.prototype ---> null
// 這裡在定義一個類或方法,所以上面是f.prototype === Function.prototype複製程式碼
需要用hasOwnProperty()檢查某屬性是否在這個物件本身定義的。
擴充套件原型鏈的4種方法:
1. new
function foo(){}
foo.prototype = {
foo_prop: "foo val"
};
function bar(){}
var proto = new foo;
proto.bar_prop = "bar val";
bar.prototype = proto;
var inst = new bar;
console.log(inst.foo_prop);
console.log(inst.bar_prop);複製程式碼
優:瀏覽器支援最廣(除了ie5.5之前的),非常快,非常標準。
缺:new的函式必須是初始化過的。因為初始化時,他的構造器可能會儲存每個物件必須生成的獨特的資訊,然而這個資訊只會生成一次,所以會導致潛在的問題。
2. Object.create()
版本1:
function foo(){}
foo.prototype = {
foo_prop: "foo val"
};
function bar(){}
var proto = Object.create(
foo.prototype
);
proto.bar_prop = "bar val";
bar.prototype = proto;
var inst = new bar;
console.log(inst.foo_prop);
console.log(inst.bar_prop);複製程式碼
版本2:使用create的第二個引數設定屬性
function foo(){}
foo.prototype = {
foo_prop: "foo val"
};
function bar(){}
var proto = Object.create(
foo.prototype,
{
bar_prop: {
value: "bar val"
}
}
);
bar.prototype = proto;
var inst = new bar;
console.log(inst.foo_prop);
console.log(inst.bar_prop)複製程式碼
優:大部分瀏覽器支援(除ie9之前)。允許直接設定__proto__,因為是一次性設定的所以瀏覽器能做相應的優化。能創造不繼承Object的物件。
缺:如果新增了第二個引數的話,物件初始化過程效能會變很差。因為每個物件描述器(object-descriptor)屬性有它自己的描述器(descriptor)物件。
3. Object.setPrototypeOf()
版本1:第一個引數就是child
function foo(){}
foo.prototype = {
foo_prop: "foo val"
};
function bar(){}
var proto = {
bar_prop: "bar val"
};
Object.setPrototypeOf(
proto, foo.prototype
);
bar.prototype = proto;
var inst = new bar;
console.log(inst.foo_prop);
console.log(inst.bar_prop);複製程式碼
版本2:返回值是child
function foo(){}
foo.prototype = {
foo_prop: "foo val"
};
function bar(){}
var proto;
proto=Object.setPrototypeOf(
{ bar_prop: "bar val" },
foo.prototype
);
bar.prototype = proto;
var inst = new bar;
console.log(inst.foo_prop);
console.log(inst.bar_prop)複製程式碼
優:大部分瀏覽器支援(除ie9之前)。能動態地操作物件原型,甚至強加prototype給無原型的物件(Object.create(null)創造的)。
缺:效能很差。瀏覽器大多會做對原型的優化且嘗試去猜某個方法在記憶體的位置在你呼叫某例項之前,而這種動態新增原型的方法基本上毀了他們的優化甚至強迫某些瀏覽器重編譯。
4. .__proto__
function foo(){}
foo.prototype = {
foo_prop: "foo val"
};
function bar(){}
var proto = {
bar_prop: "bar val",
__proto__: foo.prototype
};
bar.prototype = proto;
var inst = new bar;
console.log(inst.foo_prop);
console.log(inst.bar_prop);複製程式碼
版本2:從頭到腳直接用proto
var inst = {
__proto__: {
bar_prop: "bar val",
__proto__: {
foo_prop: "foo val",
__proto__: Object.prototype
}
}
};
console.log(inst.foo_prop);
console.log(inst.bar_prop)複製程式碼
優:大部分瀏覽器支援(除ie11之前)。把__proto__設定成非物件的值會隱式失敗,不會拋異常。
缺:已被標準棄用(儘管還有瀏覽器支援),效能很差。跟上面一樣,瀏覽器大多會做對原型的優化且嘗試去猜某個方法在記憶體的位置在你呼叫某例項之前,而這種動態新增原型的方法基本上毀了他們的優化甚至強迫某些瀏覽器重編譯。
看完MDN後的幾點疑惑及思考
1. 為什麼在文件上面提到不要func.prototype = {a: 'a'}這麼寫,因為會破壞原型鏈,而下面的好多例子又直接那麼寫呢?
// add properties in f function's prototype
f.prototype.b = 3;
f.prototype.c = 4;
// do not set the prototype f.prototype = {b:3,c:4}; this will break the prototype chain複製程式碼
因為如果你有很多層的繼承的話,顯而易見,你直接讓他指向一個其他物件,那麼前面的鏈就斷了。可是例子裡面都只有一層繼承,他們上面都是Object.prototype,所以無所謂了
2. 為什麼var b = {}; 或 b = []列印出來的b.prototype === undefined?
已經在前面講過了,物件(例項)要用__proto__
3. 為啥說b = []的繼承鏈上一層是Array.prototype呢, 從哪看出來的?
列印一下就知道了:
4. class(用class關鍵字宣告的)怎麼繼承純obj?
const Animal = {
speak() {
console.log(this.name + ' makes a noise.');
}
};
class Dog {
constructor(name) {
this.name = name;
}
}
// If you do not do this you will get a TypeError when you invoke speak
Object.setPrototypeOf(Dog.prototype, Animal);
let d = new Dog('Mitzie');
d.speak(); // Mitzie makes a noise.複製程式碼