摘要:原文相信很多才接觸前端的小伙伴甚至工作幾年的前端小伙伴對這個操作符的了解還停留在一知半解的地步,比較模糊。
原文:http://blog.xieyangogo.cn/201...
相信很多才接觸前端的小伙伴甚至工作幾年的前端小伙伴對new這個操作符的了解還停留在一知半解的地步,比較模糊。就比如前不久接觸到一個入職兩年的前端小伙伴,他告訴我new是用來創建對象的,無可厚非,可能很多人都會這么答!
那這么答到底是錯很是對呢?
下面我們全面來討論一下這個問題:
我們要拿到一個對象,有很多方式,其中最常見的一種便是對象字面量:
var obj = {}
但是從語法上來看,這就是一個賦值語句,是把對面字面量賦值給了obj這個變量(這樣說或許不是很準確,其實這里是得到了一個對象的實例!?。?p>很多時候,我們說要創建一個對象,很多小伙伴雙手一摸鍵盤,啪啪幾下就敲出了這句代碼。上面說了,這句話其實只是得到了一個對象的實例,那這句代碼到底還能不能和創建對象畫上等號呢?
我們繼續往下看。
要拿到一個對象的實例,還有一種和對象字面量等價的做法就是構造函數:
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 }
再執行代碼,發現又出問題了?。。?/p>
又出問題了!為什么……?
...
剛才不是總結了返回基本類型時構造函數不受影響嗎,而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()
這樣創建的對象就擁有了它初始的原型鏈了,這個原型鏈是我們傳進來的構造函數賦予它的。
也就是說,我們在創建新對象的時候,就為它指定了原型鏈了——新創建的對象繼承自傳進來的構造函數!
看到這里,小伙伴們長長舒了一口氣,有本事你再給我安排一個坑出來!既然都看到這里了,大家要相信我們離最終的曙光已經不遠了!
我想說的是,坑是沒有了,但是為了程序員吹毛求疵的精神!哦不對,是精益求精的精神,我們還有必要啰嗦一點點?。?/p>
想必細心的小伙伴已經注意到了,為什么最后一步中的以下代碼:
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操作符到底做了什么?
獲取實參中的第一個參數(構造函數),就是調用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是實現繼承的一種方式;
...
原文:http://blog.xieyangogo.cn/201...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/103467.html
摘要:今年徹底火了一把,其中最牛逼之處就是操作符了,以前只知道怎么用,這幾天看了看源碼,大致的弄清楚了操作符的工作過程,今天分享給大家。如果有什么不對地方,請大家多多指教。今天我們已為例,看代碼一個很簡單的小例子,用過濾操作符找出大于等于的數字。 ????RxJava今年徹底火了一把,其中最牛逼之處就是操作符了,以前只知道怎么用,這幾天看了看源碼,大致的弄清楚了操作符的工作過程,今天分享給大...
摘要:正常情況,的返回值就是一個對象,其實也就是對象。好了,上面算是基本說清楚了使用語法定義類繼承類,到底發生了什么,如果錯誤,還請指正,謝謝 自從有了webpack之后,我們這些jscoder似乎得到了前所未有的解放,箭頭函數,對象解構,let,const關鍵字,以及class、extends等等關鍵字使用得不亦樂乎,反正,webpack會幫我們把這些es6代碼轉換成瀏覽器能夠識別的es5...
摘要:當構造函數沒有顯式地返回一個值的時候,對其執行操作之后,會返回這個構造函數實例化之后的對象。 JavaScript里實例化一個對象的時候,我們常用的方法就是使用new操作符。 var Foo = function(x, y) { this.x = x this.y = y } var foo = new Foo(1, 2) // Foo?{x: 1, y: 2} 那么...
閱讀 3351·2021-10-13 09:40
閱讀 2586·2021-10-08 10:17
閱讀 3989·2021-09-28 09:45
閱讀 922·2021-09-28 09:35
閱讀 1805·2019-08-30 10:51
閱讀 2898·2019-08-26 12:11
閱讀 1645·2019-08-26 10:41
閱讀 3091·2019-08-23 17:10