JS中的new運算符,從一個自定義對象類型或者包含constructor構建函數的內建對象類型中實例化一個對象。JS中已經“萬物皆對象”。為什么還要存在實例化的操作呢?
首先,先看一段代碼:
function Animal(name) {
this.name = name
}
Animal.color = 'black'
Animal.say = function () {
console.log('it is an ' + this.name)
}
Animal.prototype.say = function () {
console.log('i am a ' + this.name)
}
var cat = new Animal('cat')
console.log(
cat.name, // cat
cat.height // undefined
)
cat.say() // i am a cat
console.log(
Animal.name, //Animal
Animal.color //black
);
Animal.say(); //it is an Animal
- 首先定義了一個函數對象Animal。作為函數對象,Animal擁有原型對象prototype,prototype中擁有一個constructor函數,指向Animal函數自身。
- 給Animal函數對象添加了一個color屬性,賦值為black
- 給Animal函數對象添加了一個say方法,該方法,讀取當前調用的this對象中的name屬性,打印字符串。
- 給Ainimal的原型對象添加一個say的方法,該方法,讀取當前調用的this對象中的name屬性,打印字符串。
- 通過new,從Animal函數對象中創建一個實例,將其賦值為變量cat
- 打印新建cat對象中的兩個屬性,name和height,分別打印為
cat
和undefined
- 調用cat對象的say方法,將cat對象作為this傳遞到Animal.prototype.say函數中,并執行打印輸出
- 打印Animal函數對象中的兩個屬性,name和color,分別打印為
Animal
和black
。其中的name屬性是從Function.prototype上繼承而來,color是Animal函數對象的自有屬性。- 調用Animal函數對象的say方法,將Animal函數對象作為this傳遞到Animal.say函數中,并執行打印輸出
關于Animal,以及Animal的say,name屬性的調用,都可以從函數對象的原型鏈的繼承上得到解釋。它的原型鏈是
Animal->Function.prototype->Object.prototype->null
我們重點關注下
var cat = new Animal('cat')
當JS中使用new操作符 添加到一個函數對象的前面并執行調用的時候。函數對象起到了一個自定義對象的constructor,也既構建函數的作用。
JS的new本身是一個“語法糖”,當JS解釋器碰到new的時候,它會按照下面的偽代碼執行:
// var cat = new Animal('cat')
var cat = (function () {
let obj = {}
obj.__proto__ = Animal.prototype
let result = Animal.call(obj, 'cat')
return (typeof result == 'object') ? result : obj
})()
將其轉成規則,則new所起到的作用流程如下:
- 首先憑空創建一個空對象obj
- 把 obj 的proto 指向構造函數 Animal 的原型對象 prototype,此時便建立了 obj 對象的原型鏈:obj->Animal.prototype->Object.prototype->null
- 在 obj 對象的執行環境調用 Animal 函數并傳遞參數 “ cat ” 。 相當于 var result = obj.Animal("cat")。這句話,將this指向新創建的obj對象。并執行構建函數Animal。當這句執行完之后,obj 便產生了屬性 name 并賦值為 "cat"。關于 call 的用法請參考:深入理解 call、apply 和 bind
- 考察第 3 步的返回值,如果無返回值 或者 返回一個非對象值,則將 obj 作為新對象返回;否則會將 result 作為新對象返回。
此時cat的原型鏈是
cat -> Animal.prototype -> Object.prototype -> null
為什么要使用new來創建對象呢?
new的出現,讓JS擁有了對象的繼承能力,從例子中看到,通過new,成功在cat和Animal之間建立了繼承的關系。cat可以調用Animal的原型對象上的方法。
通過 new 創建的 對象 和 構造函數 之間建立了一條原型鏈,原型鏈的建立,讓原本孤立的對象有了依賴關系和繼承能力,讓JavaScript 對象能以更合適的方式來映射真實世界里的對象,這是面向對象的本質。
測試下
function Foo(){
getName = function(){
console.log(1)
}
return this;
}
Foo.getName = function(){
console.log(2)
}
Foo.prototype.getName = function(){
console.log(3)
}
var getName = function(){
console.log(4)
}
function getName(){
console.log(5)
}
// ouput:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
- Foo.getName(): 調用Foo函數對象的getName方法,此時打印 2
- getName(); 調用全局的getName方法。JS代碼執行分為兩個階段,具體參考執行上下文的文章,首先在代碼為執行前,getName指的是打印 5 的函數聲明,但在執行到此時的時候,上面 getName 全局變量的執行,將getName的賦值指向了 打印4 的函數。因此此時打印 4
- Foo().getName(); 將Foo函數執行后返回的對象作為this,并調用該this中包含的getName方法。首先,Foo()的調用是在全局作用域,因此return this 等價于 return window。Foo() == window。此時Foo().getName()變成了this.getName()。但此時并不等價于第二條,因為在Foo()執行的過程中,在Foo函數內部,對全局變量getName進行了重新賦值,此時全局函數getName打印輸出 1
- getName(); 此時的結果和上一條打印結果輸出相同,打印輸出 1
- new Foo.getName(); 此時出現了new操作符,它將后面的函數Foo.getName作為了構建函數,創建了一個新的實例,在創建的過程中,會執行Foo.getName函數,因此此時輸出 2
- new Foo().getName(); 此時出現了new操作符,根據就近原則,等價于(new Foo()).getName()。即先創建了Foo()對象的一個實例 obj = new Foo()。此時return的this等同于新建實例對象obj。接下來的調用變成了obj.getName,也既Foo.prototype.getName。打印輸出 3
- new new Foo().getName(); 首先出現了兩次new操作符,而每次new操作符,都需要跟隨一個構建函數的調用。在表達式中一共有兩次調用(函數的調用通過
()
實現)。因此按照就近原則,表達式等價于 new (new Foo()).getName()。new Foo(),根據new的執行原則,返回新建實例obj。此時等價于new obj.getName()。和第6條一樣。因此此時,打印輸出 3
最后打印順序為 2,4,1,1,2,3,3
參考鏈接:
https://zhuanlan.zhihu.com/p/23987456
https://stackoverflow.com/questions/1646698/what-is-the-new-keyword-in-javascript
https://www.cnblogs.com/onepixel/p/5043523.html
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new