原文:blog.xieyangogo.cn/2019/04/12/…javascript
相信不少才接觸前端的小夥伴甚至工做幾年的前端小夥伴對new這個操做符的瞭解還停留在只知其一;不知其二的地步,比較模糊。前端
就好比前不久接觸到一個入職兩年的前端小夥伴,他告訴我new是用來建立對象的,無可厚非,可能不少人都會這麼答!java
那這麼答究竟是錯非常對呢?程序員
下面咱們全面來討論一下這個問題:app
咱們要拿到一個對象,有不少方式,其中最多見的一種即是對象字面量:函數
var obj = {}
複製代碼
可是從語法上來看,這就是一個賦值語句,是把對面字面量賦值給了
obj
這個變量(這樣說或許不是很準確,其實這裏是獲得了一個對象的實例!!)ui不少時候,咱們說要建立一個對象,不少小夥伴雙手一摸鍵盤,啪啪幾下就敲出了這句代碼。this
上面說了,這句話其實只是獲得了一個對象的實例,那這句代碼到底還能不能和建立對象畫上等號呢?spa
咱們繼續往下看。prototype
要拿到一個對象的實例,還有一種和對象字面量等價的作法就是構造函數:
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操做符的實現
主要步驟:
1. 建立一個空對象
2. 獲取arguments第一個參數
3. 將構造函數的原型鏈賦給obj
4. 使用apply改變構造函數this指向,指向obj對象,其後,obj就能夠訪問到構造函數中的屬性以及原型上的屬性和方法了
5. 返回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
}
// ...
複製代碼
執行一下代碼,發現全部字段終於如期輸出。
這裏作個小結:
1. 當構造函數返回引用類型時,構造裏面的屬性不能使用,只能使用返回的對象;
2. 當構造函數返回基本類型時,和沒有返回值的狀況相同,構造函數不受影響。
那咱們如今來考慮下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()
複製代碼
這樣建立的對象就擁有了它初始的原型鏈了,這個原型鏈是咱們傳進來的構造函數賦予它的。
也就是說,咱們在建立新對象的時候,就爲它指定了原型鏈了——新建立的對象繼承自傳進來的構造函數!
看到這裏,小夥伴們長長舒了一口氣,有本事你再給我安排一個坑出來!
![]()
既然都看到這裏了,你們要相信咱們離最終的曙光已經不遠了!
![]()
我想說的是,坑是沒有了,可是爲了程序員吹毛求疵的精神!哦不對,是精益求精的精神,咱們還有必要囉嗦一點點!!
想必細心的小夥伴已經注意到了,爲何最後一步中的如下代碼:
Constructor = [].shift.call(arguments)
var obj = Object.create(Constructor.prototype)
複製代碼
不能使用如下代碼來代替?
var obj = Object.create(null)
Constructor = [].shift.call(arguments)
obj.__proto__ = Constructor.prototype
複製代碼
換個方式說,這兩段代碼大體的意思基本相同:都是將構造器的原型賦予新建立的對象。
可是爲什麼第二段代碼要報錯(訪問不到原型鏈上的屬性)呢?
這個問題很吃基本功,認真去研究研究js的底層APIObject.create
以及原型鏈等知識,就會明白其中的道理。小夥伴能夠拉到文章末尾,我把重點都記錄下來了,以供你們參考。
如今,咱們來梳理下最終的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__屬性,指向該實例化對象的構造函數的原型對象。
補充一下關於
Object.create()
的知識:
- Object.create(null)能夠建立一個沒有原型鏈、真正意義上的空對象,該對象不擁有js原生對象(Object)的任何特性和功能。 就如:即便經過人爲賦值的方式(
newObj.__proto__ = constructor.prototype
)給這個對象賦予了原型鏈, 也不能實現原型鏈逐層查找屬性的功能,由於這個對象看起來彷佛即便有了"__proto__"
屬性,可是它始終沒有直接或間接繼承自Object.prototype, 也就不可能擁有js原生對象(Object)的特性或功能了;- 該API配合Object.defineProperty能夠建立javascript極其靈活的自定義對象;
- 該API是實現繼承的一種方式;
- ...