new 操做符到底作了什麼?

原文: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是來作繼承的而不是所謂的建立對象!!


那繼承獲得的實例對象有什麼特色呢?

  1. 訪問構造函數裏面的屬性
  2. 訪問原型鏈上的屬性

下面是一段經典的繼承,經過這段代碼來熱熱身,好戲立刻開始:

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()
複製代碼
  • 如今咱們來解決第一個問題:咱們能夠經過什麼方式實現訪問到構造函數裏面的屬性呢?答案是callapply
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()
複製代碼

執行代碼,發現只有namegender這兩個字段如期輸出,agenation爲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操做符到底作了什麼?

  1. 獲取實參中的第一個參數(構造函數),就是調用New函數傳進來的第一個參數,暫時記爲Constructor
  2. 使用Constructor的原型鏈結合Object.create建立一個對象,此時新對象的原型鏈爲Constructor函數的原型對象;(結合咱們上面討論的,要訪問原型鏈上面的屬性和方法,要使用實例對象的__proto__屬性)
  3. 改變Constructor函數的this指向,指向新建立的實例對象,而後call方法再調用Constructor函數,爲新對象賦予屬性和方法;(結合咱們上面討論的,要訪問構造函數的屬性和方法,要使用call或apply)
  4. 返回新建立的對象,爲Constructor函數的一個實例對象。

如今我,咱們來回答文章開始時提出的問題,new是用來建立對象的嗎?

如今咱們能夠勇敢的回答,new是用來作繼承的,而建立對象的實際上是Object.create(null)。 在new操做符的做用下,咱們使用新建立的對象去繼承了他的構造函數上的屬性和方法、以及他的原型鏈上的屬性和方法!


寫在最後:

補充一點關於原型鏈的知識:

  1. JavaScript中的函數也是對象,並且對象除了使用字面量定之外,都須要經過函數來建立對象;
  2. prototype屬性能夠給函數和對象添加可共享(繼承)的方法、屬性,而__proto__是查找某函數或對象的原型鏈方式;
  3. prototype和__proto__都指向原型對象;
  4. 任意一個函數(包括構造函數)都有一個prototype屬性,指向該函數的原型對象;
  5. 任意一個實例化的對象,都有一個__proto__屬性,指向該實例化對象的構造函數的原型對象。

補充一下關於Object.create()的知識:

  1. Object.create(null)能夠建立一個沒有原型鏈、真正意義上的空對象,該對象不擁有js原生對象(Object)的任何特性和功能。 就如:即便經過人爲賦值的方式(newObj.__proto__ = constructor.prototype)給這個對象賦予了原型鏈, 也不能實現原型鏈逐層查找屬性的功能,由於這個對象看起來彷佛即便有了"__proto__"屬性,可是它始終沒有直接或間接繼承自Object.prototype, 也就不可能擁有js原生對象(Object)的特性或功能了;
  2. 該API配合Object.defineProperty能夠建立javascript極其靈活的自定義對象;
  3. 該API是實現繼承的一種方式;
  4. ...

原文:blog.xieyangogo.cn/2019/04/12/…

相關文章
相關標籤/搜索