JS基礎(chǔ)回歸01:new操作符敢课,原型和原型鏈

本篇介紹 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 的背后其實是以下的四步操作:

  1. 創(chuàng)建一個空的 JavaScript 對象:{}
  2. 鏈接該對象和構(gòu)造函數(shù),也就是設(shè)置其原型
  3. 將步驟 1 的對象作為this的上下文
  4. 如果該構(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ū)別是:

  1. 前者首字母大寫液茎,但不是必須
  2. 普通函數(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ù)哑芹。

首先明確以下事實:

  1. JS 中的所有對象一定都有一個原型炎辨,并且繼承了來自原型的所有屬性和方法,而對象找到這個原型的路徑就是 obj.__proto__聪姿。
  2. 不是所有的對象都會有 prototype 屬性碴萧,只有函數(shù)才有:{x: 1}.prototype 的值就為 undefined.

有點繞,請仔細(xì)看看這張經(jīng)典的圖:

image

我們跟著這張圖和上面三句話的指引末购,來看看下面的簡單例子:

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 操作符背后做的工作:

  1. 創(chuàng)建一個空的 JavaScript 對象:{}
  2. 鏈接該對象和構(gòu)造函數(shù)氏身,也就是設(shè)置其原型
  3. 將步驟 1 的對象作為this的上下文
  4. 如果該構(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市尚猿,隨后出現(xiàn)的幾起案子窝稿,更是在濱河造成了極大的恐慌,老刑警劉巖凿掂,帶你破解...
    沈念sama閱讀 216,744評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伴榔,死亡現(xiàn)場離奇詭異,居然都是意外死亡庄萎,警方通過查閱死者的電腦和手機踪少,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來惨恭,“玉大人秉馏,你說我怎么就攤上這事⊥严郏” “怎么了萝究?”我有些...
    開封第一講書人閱讀 163,105評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長锉罐。 經(jīng)常有香客問我帆竹,道長,這世上最難降的妖魔是什么脓规? 我笑而不...
    開封第一講書人閱讀 58,242評論 1 292
  • 正文 為了忘掉前任栽连,我火速辦了婚禮,結(jié)果婚禮上侨舆,老公的妹妹穿的比我還像新娘秒紧。我一直安慰自己,他們只是感情好挨下,可當(dāng)我...
    茶點故事閱讀 67,269評論 6 389
  • 文/花漫 我一把揭開白布熔恢。 她就那樣靜靜地躺著,像睡著了一般臭笆。 火紅的嫁衣襯著肌膚如雪叙淌。 梳的紋絲不亂的頭發(fā)上秤掌,一...
    開封第一講書人閱讀 51,215評論 1 299
  • 那天,我揣著相機與錄音鹰霍,去河邊找鬼闻鉴。 笑死,一個胖子當(dāng)著我的面吹牛茂洒,可吹牛的內(nèi)容都是我干的孟岛。 我是一名探鬼主播,決...
    沈念sama閱讀 40,096評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼获黔,長吁一口氣:“原來是場噩夢啊……” “哼蚀苛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起玷氏,我...
    開封第一講書人閱讀 38,939評論 0 274
  • 序言:老撾萬榮一對情侶失蹤堵未,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后盏触,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體渗蟹,經(jīng)...
    沈念sama閱讀 45,354評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,573評論 2 333
  • 正文 我和宋清朗相戀三年赞辩,在試婚紗的時候發(fā)現(xiàn)自己被綠了雌芽。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,745評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡辨嗽,死狀恐怖世落,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情糟需,我是刑警寧澤屉佳,帶...
    沈念sama閱讀 35,448評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站洲押,受9級特大地震影響武花,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜杈帐,卻給世界環(huán)境...
    茶點故事閱讀 41,048評論 3 327
  • 文/蒙蒙 一体箕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧挑童,春花似錦累铅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至大年,卻和暖如春换薄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背翔试。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評論 1 269
  • 我被黑心中介騙來泰國打工轻要, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人垦缅。 一個月前我還...
    沈念sama閱讀 47,776評論 2 369
  • 正文 我出身青樓冲泥,卻偏偏與公主長得像,于是被迫代替她去往敵國和親壁涎。 傳聞我的和親對象是個殘疾皇子凡恍,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,652評論 2 354

推薦閱讀更多精彩內(nèi)容