本篇介紹 new 操作符的背后原理以及 JS 如何依賴原型形成原型鏈阶祭,完成繼承。
new 操作符的本質(zhì)
new 操作符置于構(gòu)造函數(shù)前面直秆,來創(chuàng)建一個基于該構(gòu)造函數(shù)的實例濒募。其仍屬于一種模擬 Java 類行為的寫法,但它的本質(zhì)是基于原型鏈的繼承圾结。
JS 是基于原型的語言瑰剃,并不具備“類”的概念,ES6 中的 class 屬于一種語法糖筝野,能夠讓開發(fā)者更好理解晌姚。
這里的構(gòu)造函數(shù)粤剧,既可以是 JS 已經(jīng)內(nèi)置的函數(shù)(String, Boolean, Object等),也可以是我們自己定義的普通函數(shù)挥唠。我們知道抵恋,JS 自身提供了一些內(nèi)置的構(gòu)造函數(shù),可以用其創(chuàng)建各類數(shù)據(jù)類型的實例:
// 每一種數(shù)據(jù)類型都有對應(yīng)的內(nèi)置構(gòu)造函數(shù)
// 注意:ES6 新增的 Symbol 類型不支持 new 新建實例
const str = new String('i am a string');
const num = new Number(123);
我們在實際開發(fā)中猛遍,常使用字面量形式來定義這些數(shù)據(jù)類型馋记,兩者的本質(zhì)是類似的(但推薦使用后者):
const str = 'i am a string';
const num = 123;
對于自定義的普通函數(shù),仍然可以通過 new 操作符創(chuàng)建其實例:
function Person(name) {
this.name = name;
this.sayName = function () {
console.log(this.name)
};
}
const personA = new Person('Jack');
personA.sayName(); // 'Jack'
如同內(nèi)置函數(shù)的寫法懊烤,當(dāng)一個普通函數(shù)作為構(gòu)造函數(shù)時梯醒,其首字母需要大寫,這只是一種寫法上的約定腌紧,就算你使用小寫茸习,也沒錯,但不推薦這么做壁肋。
如上所述号胚,new 操作符的本質(zhì),仍屬于基于原型的繼承行為浸遗。新建的實例擁有其構(gòu)造函數(shù)原型上的所有屬性和方法猫胁。下面我們具體分析 new 操作符背后發(fā)生了什么,方便更好理解其本質(zhì)跛锌。
new 操作符背后發(fā)生了什么弃秆?
我們提到,new 操作符是在背后默默地為我們完成了一些操作髓帽,才能實現(xiàn)實例完整繼承構(gòu)造函數(shù)的效果菠赚。new 的背后其實是以下的四步操作:
- 創(chuàng)建一個空的 JavaScript 對象:{}
- 鏈接該對象和構(gòu)造函數(shù),也就是設(shè)置其原型
- 將步驟 1 的對象作為this的上下文
- 如果該構(gòu)造函數(shù)沒有返回對象郑藏,則返回 this
詳細(xì)來看衡查,第1步很好理解,我們來看第2步是如何將空對象鏈接到該構(gòu)造函數(shù)的必盖?
其實際的操作仍是基于原型:將空對象的 proto 屬性指向構(gòu)造函數(shù)的 prototype 屬性拌牲,{}.__proto__ === Constructor.prototype
我們可以通過前面的例子進(jìn)行測試:
personA.__proto__ === Person.prototype // true
我們暫且不糾結(jié) proto 和 prototype 這兩個屬性,留待后面細(xì)解筑悴,你可以將它理解為兩個插口们拙,兩個沒有關(guān)系的對象,因為它們相愛走到了一起阁吝。
完成連接后砚婆,這個空對象已經(jīng)具備了構(gòu)造函數(shù)的全部屬性和方法。
接下來要做的是,將該對象作為 this 的上下文装盯,這樣我們就可以通過 this 來訪問該對象的所有屬性和方法坷虑。
最后一步,如果構(gòu)造函數(shù)明確返回了一個對象埂奈,則我們的實例目前能訪問到的屬性和方法來自于該對象迄损。
function Person(name) {
this.name = name;
this.sayName = function () {
console.log(this.name)
};
// 返回一個對象
return {
name: 'Rose'
}
}
const personA = new Person('Jack');
personA.name; // 'Rose'
如果沒有返回任何值,則會返回 this.
若是返回一個原始類型的值账磺,實例會忽視它芹敌,仍然拿到this.
function Person(name) {
this.name = name;
this.sayName = function () {
console.log(this.name)
};
return 'my name is Bob';
}
const personA = new Person('Jack');
console.log(personA)
現(xiàn)在我們對于 new 的背后發(fā)生了什么,已經(jīng)很清楚垮抗,就是新建一個對象氏捞,將該對象通過原型與構(gòu)造函數(shù)相連,擁有構(gòu)造函數(shù)返回(this 或者 顯示返回的對象)的全部方法和屬性冒版。
構(gòu)造函數(shù)與普通函數(shù)的區(qū)別是:
- 前者首字母大寫液茎,但不是必須
- 普通函數(shù)前面加上
new
,就是構(gòu)造函數(shù)辞嗡,會返回一個創(chuàng)建的對象捆等,去掉new
,就是普通函數(shù)续室,會得到其 return 的值栋烤。
我們也許會對上面第二步的操作感到疑惑,__proto__
和prototype
的區(qū)別和聯(lián)系是什么挺狰?原型鏈又是怎么實現(xiàn)的班缎?
原型、原型鏈及繼承
首先她渴,繼承很好理解,許多語言都有這個功能蔑祟,其基本的目的是趁耗,完成功能的復(fù)用。一般來講疆虚,繼承指的是面向?qū)ο蟮睦^承苛败,在 Java 中,通過類實現(xiàn)繼承径簿,但在 JS 中罢屈,是沒有類這個概念的,它擁有一套獨立而強大的繼承機制:基于原型鏈的繼承篇亭,原型鏈又是基于原型這個特性實現(xiàn)的缠捌。
proto、prototype 和 constructor
我們先來理清這三個概念译蒂。
-
__proto__
:每一個對象都擁有一個隱式的屬性__proto__
曼月,指向其構(gòu)造函數(shù)的原型對象 -
prototype
:只有函數(shù)才會擁有的屬性谊却,指向函數(shù)的原型對象 -
constructor
: 每一個原型對象都擁有這個屬性,指向該對象的構(gòu)造函數(shù)哑芹。
首先明確以下事實:
- JS 中的所有對象一定都有一個原型炎辨,并且繼承了來自原型的所有屬性和方法,而對象找到這個原型的路徑就是
obj.__proto__
聪姿。 - 不是所有的對象都會有
prototype
屬性碴萧,只有函數(shù)才有:{x: 1}.prototype
的值就為 undefined.
有點繞,請仔細(xì)看看這張經(jīng)典的圖:
我們跟著這張圖和上面三句話的指引末购,來看看下面的簡單例子:
function Person(name) {
this.name = name;
}
// sayName方法屬于 Person 這個構(gòu)造函數(shù)的原型對象
Person.prototype.sayName = function () {
return `Hello, I am ${this.name}`;
}
const p1 = new Person('Alice')
console.log(p1.__proto__ === Person.prototype) // true
console.log(Person.prototype.constructor === Person) // true
console.log(p1.name) // Alice
console.log(p1.sayName()) // Hello, I am Alice
從這個簡單例子中破喻,我們可以看到,p1既擁有了 Person 的屬性招盲,也擁有了 Person 原型對象的方法低缩。這樣,三者就完成了一次繼承曹货,而這個方式咆繁,就是通過原型鏈實現(xiàn)。
這條鏈從下游到上游依次是:p1 → Person → Person.prototype.(實際上顶籽,這個鏈條上游更長玩般,Person.prototype仍然擁有自己的原型,一直到 Object.prototype)
所以礼饱,我們的 new 操作符仍然是一種繼承行為坏为,但其仍屬于打造原型鏈的過程。
在這條鏈上面镊绪,上游的方法和屬性被下游的實例所共有匀伏,同時,下游的對象可以自由定制自己的屬性和方法蝴韭,當(dāng)上下游擁有同名的屬性和方法時够颠,就會出現(xiàn)“屬性遮蔽”的情況:
function Person(name) {
this.name = name;
this.sayName = function () {
return 'Hahaha, I am Bob.';
}
}
// sayName方法屬于 Person 這個構(gòu)造函數(shù)的原型對象
Person.prototype.sayName = function () {
return `Hello, I am ${this.name}`;
}
const p1 = new Person('Alice')
console.log(p1.sayName()) // "Hahaha, I am Bob."
那么,為什么會出現(xiàn)“屬性遮蔽”的行為榄鉴,這涉及到原型鏈的工作方式履磨。
我們提到,可以把原型鏈比作一個上下游的關(guān)系庆尘,這個上游可達(dá)對象的基本構(gòu)造函數(shù) Object 的原型對象:Object.prototype
剃诅,下游可以以多種方式進(jìn)行拓展,new 操作符正是其中一種驶忌。
當(dāng)我們訪問一個下游節(jié)點的屬性時矛辕,首先會優(yōu)先從當(dāng)前節(jié)點開始查詢,在上面的例子中,p1 本身沒有一個 sayName 方法如筛,所以堡牡,它會沿著原型鏈,找到它的構(gòu)造函數(shù) Person杨刨。
Person 內(nèi)部定義了 sayName 方法晤柄,所有就返回了。如果這里也沒有找到妖胀,就會繼續(xù)向上查找芥颈,找到其原型對象,也就是 Person.prototype赚抡,仍然未找到爬坑,繼續(xù)向上查找,一直到最后的 Object.prototype.這個對象是 null涂臣,所以到此為止盾计。
也就是說,Object.prototype
是對象原型鏈的最上游赁遗,發(fā)源地署辉,下游的實例從這里繼承了 Object 的所有實例和方法,例如 toSting
岩四、hasOwnProperty
哭尝,感興趣的同學(xué)可以在控制臺打印看看。
我們可以看到剖煌,正是通過 __proto__
以及 prototype
這兩個屬性通力合作材鹦,JS 才能實現(xiàn)繼承,打造原型鏈耕姊。
instanceof 操作符的工作機制
看看 MDN 上對于 instanceof
的定義:
The instanceof operator tests whether the prototype property of a constructor appears anywhere in the prototype chain of an object.
instanceof
操作符檢測構(gòu)造函數(shù)的 prototype 屬性出否出現(xiàn)在一個對象原型鏈的任何位置桶唐。
換句話說:檢測一個對象的原型是否出現(xiàn)在另一個對象的原型鏈上游。按前面的例子進(jìn)行舉例:
console.log(p1 instanceof Person) // true
console.log(p1 instanceof Object) // true
console.log(Person instanceof Object) // true
那么茉兰,可以思考莽红,instanceof
是如何工作的呢?
沿著左邊對象的原型鏈向上查詢邦邦,一直到最頂部,能找到右邊對象醉蚁,返回 true燃辖,反之返回 false。
也就是判斷 left.__proto__ === right.prototype
网棍,如果 false
黔龟,沿著原型鏈,繼續(xù)判斷:
left.__proto__.__proto__ === right
,一直到 Object.prototype.
動手實現(xiàn)一個 new 操作符
我們先回顧 new
操作符背后做的工作:
- 創(chuàng)建一個空的 JavaScript 對象:{}
- 鏈接該對象和構(gòu)造函數(shù)氏身,也就是設(shè)置其原型
- 將步驟 1 的對象作為this的上下文
- 如果該構(gòu)造函數(shù)沒有返回對象巍棱,則返回 this
明確了它背后發(fā)生的事情,現(xiàn)在我們動手親自實現(xiàn)一個 new
:
function anotherNew(constructor) {
// 判斷傳入的值是否為構(gòu)造函數(shù)
if (typeof constructor !== 'function') {
return `${constructor} is not a constructor`;
}
let obj = {}; // 1.新建一個空對象
obj.__proto__ = constructor.prototype;
this = obj
}
參考文章
1蛋欣、https://github.com/creeperyang/blog/issues/9
2航徙、https://juejin.im/post/584e1ac50ce463005c618ca2
3、https://juejin.im/post/5c7b963ae51d453eb173896e
4陷虎、https://juejin.im/post/58f94c9bb123db411953691b
5到踏、https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain