理解Javascript的原型和原型鏈
前言
總括: 結(jié)合實(shí)例闡述了原型和原型鏈的概念并總結(jié)了幾種創(chuàng)建對(duì)象的方法,擴(kuò)展原型鏈的方法。
正文
原型
Javascript中有一句話,叫一切皆是對(duì)象,當(dāng)然這句話也不嚴(yán)謹(jǐn),比如null和undefined就不是對(duì)象,除了這倆完全可以說(shuō)Javascript一切皆是對(duì)象。而Javascript對(duì)象都有一個(gè)叫做原型的公共屬性,屬性名是_proto_。這個(gè)原型屬性是對(duì)另一個(gè)對(duì)象的引用,通過(guò)這個(gè)原型屬性我們就可以訪問(wèn)另一個(gè)對(duì)象所有的屬性和方法。比如:
- let numArray = [1, 2, -8, 3, -4, 7];
Array對(duì)象就有一個(gè)原型屬性指向Array.prototype,變量numArray繼承了Array.prototype對(duì)象所有的屬性和方法。
這就是為什么可以直接調(diào)用像sort()這種方法:
- console.log(numArray.sort()); // -> [-4, -8, 1, 2, 3, 7]
也就是說(shuō):
- numArray.__proto__ === Array.prototype // true
對(duì)于其他對(duì)象(函數(shù))也是一樣(比如Date(),Function(), String(),Number()等);
當(dāng)一個(gè)構(gòu)造函數(shù)被創(chuàng)建后,實(shí)例對(duì)象會(huì)繼承構(gòu)造函數(shù)的原型屬性,這是構(gòu)造函數(shù)的一個(gè)非常重要的特性。在Javascript中使用new關(guān)鍵字來(lái)對(duì)構(gòu)造函數(shù)進(jìn)行實(shí)例化。看下面的例子:
- const Car = function(color, model, dateManufactured) {
- this.color = color;
- this.model = model;
- this.dateManufactured = dateManufactured;
- };
- Car.prototype.getColor = function() {
- return this.color;
- };
- Car.prototype.getModel = function() {
- return this.model;
- };
- Car.prototype.carDate = function() {
- return `This ${this.model} was manufactured in the year ${this.dateManufactured}`
- }
- let firstCar = new Car('red', 'Ferrari', '1985');
- console.log(firstCar);
- console.log(firstCar.carDate());
上面的例子中,方法getColor,carDate,getModel都是對(duì)象(函數(shù))Car的方法,而Car的實(shí)例對(duì)象firstCar可以繼承Car原型上的一切方法和屬性。
結(jié)論:每一個(gè)實(shí)例對(duì)象都有一個(gè)私有屬性_proto_,指向它的構(gòu)造函數(shù)的原型對(duì)象(prototype)。
原型鏈
在Javascript中如果訪問(wèn)一個(gè)對(duì)象本身不存在的屬性或是方法,就首先在它的原型對(duì)象上去尋找,如果原型對(duì)象上也不存在,就繼續(xù)在原型對(duì)象的原型對(duì)象上去尋找,直到找到為止。那么原型對(duì)象有盡頭么?所有對(duì)象的原型盡頭是Object.prototype,那么Object.prototype這個(gè)對(duì)象的_proto_指向啥呢?答案是null。我們?nèi)粘i_(kāi)發(fā)中用到的絕大多數(shù)對(duì)象的_proto_基本不會(huì)直接指向Object.prototype,基本都是指向另一個(gè)對(duì)象。比如所有的函數(shù)的_proto_都會(huì)指向Function.prototype,所有數(shù)組的_proto_都會(huì)指向Array.prototype。
- let protoRabbit = {
- color: 'grey',
- speak(line) {
- console.log(`The ${this.type} rabbit says ${line}`);
- }
- };
- let killerRabbit = Object.create(protoRabbit);
- killerRabbit.type = "assassin";
- killerRabbit.speak("SKREEEE!");
上面代碼中變量protoRabbit設(shè)置為所有兔子對(duì)象的公有屬性對(duì)象集,killerRabbit這只兔子通過(guò)Object.create方法繼承了protoRabbit的所有屬性和方法,然后給killerRabbit賦值了一個(gè)type屬性,再看下面的代碼:
- let mainObject = {
- bar: 2
- };
- // create an object linked to `anotherObject`
- let myObject = Object.create( mainObject );
- for (let k in myObject) {
- console.log("found: " + k);
- }
- // found: bar
- ("bar" in myObject);
如上變量myObject本身并沒(méi)有bar屬性,但這里會(huì)循著原型鏈一層一層往上找,直到找到或者原型鏈結(jié)束為止。如果到原型鏈盡頭還是沒(méi)找到該屬性,那么訪問(wèn)該屬性的時(shí)候就會(huì)返回undefined了。
使用for...in關(guān)鍵字對(duì)對(duì)象進(jìn)行迭代的過(guò)程,和上面訪問(wèn)某個(gè)屬性循著原型鏈查找類似,會(huì)去遍歷所有原型鏈上的屬性(不論屬性是否可枚舉)。
- let protoRabbit = {
- color: 'grey',
- speak(line) {
- console.log(`The ${this.type} rabbit says ${line}`);
- }
- };
- let killerRabbit = Object.create(protoRabbit);
- killerRabbit.type = "assassin";
- killerRabbit.speak("SKREEEE!");
上面的代碼中訪問(wèn)speak的效率很高,但如果我們想創(chuàng)建很多個(gè)Rabbit對(duì)象,就必須要重復(fù)寫很多代碼。而這正是原型和構(gòu)造函數(shù)的真正用武之地。
- let protoRabbit = function(color, word, type) {
- this.color = color;
- this.word = word;
- this.type = type;
- };
- protoRabbit.prototype.getColor = function() {
- return this.color;
- }
- protoRabbit.prototype.speak = function() {
- console.log(`The ${this.type} rabbit says ${this.word}`);
- }
- let killerRabbit = new protoRabbit('grey', 'SKREEEEE!', 'assassin');
- killerRabbit.speak();
如上代碼,使用構(gòu)造函數(shù)的方式就可以節(jié)省很多的代碼。
結(jié)論:每一個(gè)實(shí)例對(duì)象都有一個(gè)私有屬性_proto_,指向它的構(gòu)造函數(shù)的原型對(duì)象(prototype)。原型對(duì)象也有自己的_proto_,層層向上直到一個(gè)對(duì)象的原型對(duì)象為null。這一層層原型就是原型鏈。
附贈(zèng)一張?jiān)玩湹膱D:
創(chuàng)建對(duì)象的四種方法
- 字面量對(duì)象
這是比較常用的一種方式:
- let obj = {};
- 構(gòu)造函數(shù)創(chuàng)建
構(gòu)造函數(shù)創(chuàng)建的方式更多用來(lái)在Javascript中實(shí)現(xiàn)繼承,多態(tài),封裝等特性。
- function Animal(name) {
- this.name = name;
- }
- let cat = new Animal('Tom');
- class創(chuàng)建
class關(guān)鍵字是ES6新引入的一個(gè)特性,它其實(shí)是基于原型和原型鏈實(shí)現(xiàn)的一個(gè)語(yǔ)法糖。
- class Animal {
- constructor(name) {
- this.name = name;
- }
- }
- let cat = new Animal('Tom');
擴(kuò)展原型鏈的四種方法
- 構(gòu)造函數(shù)創(chuàng)建
上面例子有用到使用構(gòu)造函數(shù)創(chuàng)建對(duì)象的例子,我們?cè)賮?lái)看一個(gè)實(shí)際的例子:
- function Animal(name) {
- this.name = name;
- }
- Animal.prototype = {
- run() {
- console.log('跑步');
- }
- }
- let cat = new Animal('Tom');
- cat.__proto__ === Animal.prototype; // true
- Animal.prototype.__proto__ === Object.prototype; // true
優(yōu)點(diǎn):支持目前以及所有可想象到的瀏覽器(IE5.5都可以使用). 這種方法非???,非常符合標(biāo)準(zhǔn),并且充分利用JIST優(yōu)化。
缺點(diǎn):為使用此方法,這個(gè)問(wèn)題中的函數(shù)必須要被初始化。另外構(gòu)造函數(shù)的初始化,可能會(huì)給生成對(duì)象帶來(lái)并不想要的方法和屬性。
- Object.create
ECMAScript 5 中引入了一個(gè)新方法: Object.create()??梢哉{(diào)用這個(gè)方法來(lái)創(chuàng)建一個(gè)新對(duì)象。新對(duì)象的原型就是調(diào)用 create 方法時(shí)傳入的第一個(gè)參數(shù):
- var a = {a: 1};
- // a ---> Object.prototype ---> null
- var b = Object.create(a);
- b.__proto__ === a; // true
優(yōu)點(diǎn): 支持當(dāng)前所有非微軟版本或者 IE9 以上版本的瀏覽器。允許一次性地直接設(shè)置 __proto__ 屬性,以便瀏覽器能更好地優(yōu)化對(duì)象。同時(shí)允許通過(guò) Object.create(null) 來(lái)創(chuàng)建一個(gè)沒(méi)有原型的對(duì)象。
缺點(diǎn):不支持 IE8 以下的版本;這個(gè)慢對(duì)象初始化在使用第二個(gè)參數(shù)的時(shí)候有可能成為一個(gè)性能黑洞,因?yàn)槊總€(gè)對(duì)象的描述符屬性都有自己的描述對(duì)象。當(dāng)以對(duì)象的格式處理成百上千的對(duì)象描述的時(shí)候,可能會(huì)造成嚴(yán)重的性能問(wèn)題。
- Object.setPrototypeOf
語(yǔ)法:
- Object.setPrototypeOf(obj, prototype)
參數(shù):
參數(shù)名 | 含義 |
---|---|
obj | 要設(shè)置其原型的對(duì)象。 |
prototype | 該對(duì)象的新原型(一個(gè)對(duì)象 或 null ). |
- var a = { n: 1 };
- var b = { m : 2 };
- Object.setPrototypeOf(a, b);
- a.__proto__ === b; // true
優(yōu)點(diǎn):支持所有現(xiàn)代瀏覽器和微軟IE9+瀏覽器。允許動(dòng)態(tài)操作對(duì)象的原型,甚至能強(qiáng)制給通過(guò) Object.create(null) 創(chuàng)建出來(lái)的沒(méi)有原型的對(duì)象添加一個(gè)原型。
缺點(diǎn):這個(gè)方式表現(xiàn)并不好,應(yīng)該被棄用;動(dòng)態(tài)設(shè)置原型會(huì)干擾瀏覽器對(duì)原型的優(yōu)化;不支持 IE8 及以下的瀏覽器版本。
- _proto_
- var a = { n: 1 };
- var b = { m : 2 };
- a.__proto__ = b;
- a.__proto__ === b; // true
使用_proto_也可以動(dòng)態(tài)設(shè)置對(duì)象的原型。
優(yōu)點(diǎn):支持所有現(xiàn)代非微軟版本以及 IE11 以上版本的瀏覽器。將 __proto__ 設(shè)置為非對(duì)象的值會(huì)靜默失敗,并不會(huì)拋出錯(cuò)誤。
缺點(diǎn):應(yīng)該完全將其拋棄因?yàn)檫@個(gè)行為完全不具備性能可言;干擾瀏覽器對(duì)原型的優(yōu)化;不支持 IE10 及以下的瀏覽器版本。