原文:http://blog.xieyangogo.cn/201...
相信很多才接觸前端的小夥伴甚至工作幾年的前端小夥伴對new這個操作符的瞭解還停留在一知半解的地步,比較模糊。
就比如前不久接觸到一個入職兩年的前端小夥伴,他告訴我new是用來創建對象的,無可厚非,可能很多人都會這麼答!
那這麼答到底是錯很是對呢?
下面我們全面來討論一下這個問題:
我們要拿到一個對象,有很多方式,其中最常見的一種便是對象字面量:
var obj = {}
但是從語法上來看,這就是一個賦值語句,是把對面字面量賦值給了obj這個變量(這樣說或許不是很準確,其實這裏是得到了一個對象的實例!!)
很多時候,我們說要創建一個對象,很多小夥伴雙手一摸鍵盤,啪啪幾下就敲出了這句代碼。
上面說了,這句話其實只是得到了一個對象的實例,那這句代碼到底還能不能和創建對象畫上等號呢?我們繼續往下看。
要拿到一個對象的實例,還有一種和對象字面量等價的做法就是構造函數:
var obj = new Object()
這句代碼一敲出來,相信小夥伴們對剛纔我說的obj
只是一個實例對象沒有異議了吧!那很多小夥伴又會問了:這不就是new
了一個新對象出來嘛!
沒錯,這確實是new了一個新對象出來,因爲javascript之中,萬物解釋對象,obj是一個對象,而且是通過new運算符得到的,所以說很多小夥伴就肯定的說:new就是用來創建對象的!
這就不難解釋很多人把創建對象和實例化對象混爲一談!!
我們在換個思路看看:既然js一切皆爲對象,那爲什麼還需要創建對象呢?本身就是對象,我們何來創建一說?那我們可不可以把這是一種繼承
呢?
說了這麼多,相信不少夥伴已經看暈了,但是我們的目的就是一個:理清new是來做繼承的而不是所謂的創建對象!!
那繼承得到的實例對象有什麼特點呢?
- 訪問構造函數裏面的屬性
- 訪問原型鏈上的屬性
下面是一段經典的繼承,通過這段代碼來熱熱身,好戲馬上開始:
function Person(name, age) {
this.name = name
this.age = age
this.gender = '男'
}
Person.prototype.nation = '漢'
Person.prototype.say = function() {
console.log(`My name is ${this.age}`)
}
var person = new Person('小明', 25)
console.log(person.name)
console.log(person.age)
console.log(person.gender)
console.log(person.nation)
person.say()
- 現在我們來解決第一個問題:我們可以通過什麼方式實現訪問到構造函數裏面的屬性呢?答案是
call
或apply
function Parent() {
this.name = ['A', 'B']
}
function Child() {
Parent.call(this)
}
var child = new Child()
console.log(child.name) // ['A', 'B']
child.name.push('C')
console.log(child.name) // ['A', 'B', 'C']
- 第一個問題解決了,那我們又來解決第二個:那又怎麼訪問原型鏈上的屬性呢?答案是
__proto__
現在我們把上面那段熱身代碼稍加改造,不使用new來創建實例:
function Person(name, age) {
this.name = name
this.age = age
this.gender = '男'
}
Person.prototype.nation = '漢'
Person.prototype.say = function() {
console.log(`My name is ${this.age}`)
}
// var person = new Person('小明', 25)
var person = New(Person, '小明', 25)
console.log(person.name)
console.log(person.age)
console.log(person.gender)
console.log(person.nation)
person.say()
function New() {
var obj = {}
Constructor = [].shift.call(arguments) // 獲取arguments第一個參數:構造函數
// 注意:此時的arguments參數在shift()方法的截取後只剩下兩個元素
obj.__proto__ = Constructor.prototype // 把構造函數的原型賦值給obj對象
Constructor.apply(obj, arguments) // 改變夠着函數指針,指向obj,這是剛纔上面說到的訪問構造函數裏面的屬性和方法的方式
return obj
}
以上代碼中的New函數,就是new操作符的實現
主要步驟:
- 創建一個空對象
- 獲取arguments第一個參數
- 將構造函數的原型鏈賦給obj
- 使用apply改變構造函數this指向,指向obj對象,其後,obj就可以訪問到構造函數中的屬性以及原型上的屬性和方法了
- 返回obj對象
可能很多小夥伴看到這裏覺得new不就是做了這些事情嗎,然而~~
然而我們卻忽略了一點,js裏面的函數是有返回值的,即使構造函數也不例外。
如果我們在構造函數裏面返回一個對象或一個基本值,上面的New函數會怎樣?
我們再來看一段代碼:
function Person(name, age) {
this.name = name
this.age = age
this.gender = '男'
return {
name: name,
gender: '男'
}
}
Person.prototype.nation = '漢'
Person.prototype.say = function() {
console.log(`My name is ${this.age}`)
}
var person = new Person('小明', 25)
console.log(person.name)
console.log(person.age)
console.log(person.gender)
console.log(person.nation)
person.say()
執行代碼,發現只有name
和gender
這兩個字段如期輸出,age
、nation
爲undefined,say()
報錯。
改一下代碼構造函數的代碼:
function Person(name, age) {
this.name = name
this.age = age
this.gender = '男'
// return {
// name: name,
// gender: '男'
// }
return 1
}
// ...
執行一下代碼,發現所有字段終於如期輸出。
這裏做個小結:
- 當構造函數返回引用類型時,構造裏面的屬性不能使用,只能使用返回的對象;
- 當構造函數返回基本類型時,和沒有返回值的情況相同,構造函數不受影響。
那我們現在來考慮下New函數要怎麼改才能實現上面總結的兩點功能呢?繼續往下看:
function Person(name, age) {
// ...
}
function New() {
var obj = {}
Constructor = [].shift.call(arguments)
obj.__proto__ = Constructor.prototype
// Constructor.apply(obj, arguments)
var result = Constructor.apply(obj, arguments)
// return obj
return typeof result === 'object' ? result : obj
}
var person = New(Person, '小明', 25)
console.log(person.name)
// ...
執行此代碼,發現已經實現了上面總結的兩點。
解決方案:使用變量接收構造函數的返回值,然後在New函數裏面判斷一下返回值類型,根據不同類型返回不同的值。
看到這裏。又有小夥伴說,這下new已經完全實現了吧?!!答案肯定是否定的,下面我們繼續看一段代碼:
function Person(name, age) {
this.name = name
this.age = age
this.gender = '男'
// 返回引用類型
// return {
// name: name,
// gender: '男'
// }
// 返回基本類型
// return 1
// 例外
return null
}
再執行代碼,發現又出問題了!!!
那爲什麼會出現這個問題呢?
剛纔不是總結了返回基本類型時構造函數不受影響嗎,而null就是基本類型啊?
此時心裏一萬頭草泥馬在奔騰啊有木有!!!
解惑:null是基本類型沒錯,但是使用操作符typeof後我們不難發現:
typeof null === 'object' // true
特例:typeof null
返回爲'object'
,因爲特殊值null
被認爲是一個空的對象引用
。
明白了這一點,那問題就好解決了:
function Person(name, age) {
// ...
}
function New() {
var obj = {}
Constructor = [].shift.call(arguments)
obj.__proto__ = Constructor.prototype
// Constructor.apply(obj, arguments)
var result = Constructor.apply(obj, arguments)
// return obj
// return typeof result === 'object' ? result : obj
return typeof result === 'object' ? result || obj : obj
}
var person = New(Person, '小明', 25)
console.log(person.name)
// ...
解決方案:判斷一下構造函數返回值result,如果result是一個引用(引用類型和null),就返回result,但如果此時result爲false(null),就使用操作符||
之後的obj
好了,到現在應該又有小夥伴發問了,這下New函數是徹徹底底實現了吧!!!
答案是,離完成不遠了!!
別急,在功能上,New函數基本完成了,但是在代碼嚴謹度上,我們還需要做一點工作,繼續往下看:
這裏,我們在文章開篇做的鋪墊要派上用場了:
var obj = {}
實際上等價於
var obj = new Object()
前面說了,以上兩段代碼其實只是獲取了object對象的一個實例。再者,我們本來就是要實現new,但是我們在實現new的過程中卻使用了new
!
這個問題把我們引入到了到底是先有雞還是先有蛋的問題上!
這裏,我們就要考慮到ECMAScript底層的API了————Object.create(null)
這句代碼的意思纔是真真切切地創建
了一個對象!!
function Person(name, age) {
// ...
}
function New() {
// var obj = {}
// var obj = new Object()
var obj = Object.create(null)
Constructor = [].shift.call(arguments)
obj.__proto__ = Constructor.prototype
// Constructor.apply(obj, arguments)
var result = Constructor.apply(obj, arguments)
// return obj
// return typeof result === 'object' ? result : obj
return typeof result === 'object' ? result || obj : obj
}
var person = New(Person, '小明', 25)
console.log(person.name)
console.log(person.age)
console.log(person.gender)
// 這樣改了之後,以下兩句先註釋掉,原因後面再討論
// console.log(person.nation)
// person.say()
好了好了,小夥伴常常舒了一口氣,這樣總算完成了!!
但是,這樣寫,新的問題又來了。
小夥伴:啥?還有完沒完?
function Person(name, age) {
this.name = name
this.age = age
this.gender = '男'
}
Person.prototype.nation = '漢'
Person.prototype.say = function() {
console.log(`My name is ${this.age}`)
}
function New() {
// var obj = {}
// var obj = new Object()
var obj = Object.create(null)
Constructor = [].shift.call(arguments)
obj.__proto__ = Constructor.prototype
// Constructor.apply(obj, arguments)
var result = Constructor.apply(obj, arguments)
// return obj
// return typeof result === 'object' ? result : obj
return typeof result === 'object' ? result || obj : obj
}
var person = New(Person, '小明', 25)
console.log(person.name)
console.log(person.age)
console.log(person.gender)
// 這裏解開剛纔的註釋
console.log(person.nation)
person.say()
別急,我們執行一下修改後的代碼,發現原型鏈上的屬性nation
和方法say()
報錯,這又是爲什麼呢?
從上圖我們可以清除地看到,Object.create(null)
創建的對象是沒有原型鏈的,而後兩個對象則是擁有__proto__
屬性,擁有原型鏈,這也證明了後兩個對象是通過繼承得來的。
那既然通過Object.create(null)
創建的對象沒有原型鏈(原型鏈斷了),那我們在創建對象的時候把原型鏈加上不就行了,那怎麼加呢?
function Person(name, age) {
this.name = name
this.age = age
this.gender = '男'
}
Person.prototype.nation = '漢'
Person.prototype.say = function() {
console.log(`My name is ${this.age}`)
}
function New() {
Constructor = [].shift.call(arguments)
// var obj = {}
// var obj = new Object()
// var obj = Object.create(null)
var obj = Object.create(Constructor.prototype)
// obj.__proto__ = Constructor.prototype
// Constructor.apply(obj, arguments)
var result = Constructor.apply(obj, arguments)
// return obj
// return typeof result === 'object' ? result : obj
return typeof result === 'object' ? result || obj : obj
}
var person = New(Person, '小明', 25)
console.log(person.name)
console.log(person.age)
console.log(person.gender)
console.log(person.nation)
person.say()
這樣創建的對象就擁有了它初始的原型鏈了,這個原型鏈是我們傳進來的構造函數賦予它的。
也就是說,我們在創建新對象的時候,就爲它指定了原型鏈了,新創建的對象繼承自傳進來的構造函數!
現在,我們來梳理下最終的New函數做了什麼事,也就是本文討論的結果————new操作符到底做了什麼?
- 獲取實參中的第一個參數(構造函數),就是調用New函數傳進來的第一個參數,暫時記爲
Constructor
; - 使用
Constructor
的原型鏈結合Object.create
來創建
一個對象,此時新對象的原型鏈爲Constructor
函數的原型對象;(結合我們上面討論的,要訪問原型鏈上面的屬性和方法,要使用實例對象的__proto__屬性) - 改變
Constructor
函數的this指向,指向新創建的實例對象,然後call
方法再調用Constructor
函數,爲新對象賦予屬性和方法;(結合我們上面討論的,要訪問構造函數的屬性和方法,要使用call或apply) - 返回新創建的對象,爲
Constructor
函數的一個實例對象。
現在我,我們來回答文章開始時提出的問題,new是用來創建對象的嗎?
現在我們可以勇敢的回答,new是用來做繼承的,而創建對象的其實是Object.create(null)。
在new操作符的作用下,我們使用新創建的對象去繼承了他的構造函數上的屬性和方法、以及他的原型鏈上的屬性和方法!
寫在最後:
補充一點關於原型鏈的知識:
- JavaScript中的函數也是對象,而且對象除了使用字面量定義外,都需要通過函數來創建對象
- prototype屬性可以給函數和對象添加可共享(繼承)的方法、屬性,而__proto__是查找某函數或對象的原型鏈方式
- prototype和__proto__都指向原型對象
- 任意一個函數(包括構造函數)都有一個prototype屬性,指向該函數的原型對象
- 任意一個實例化的對象,都有一個__proto__屬性,指向構造函數的原型對象。