在 JavaScript 中,new 運算符創建一個用戶定義的對象類型的實例或具有構造函數的內置對象的實例。創建一個對象很簡單,為什么我們還要多此一舉使用 new 運算符呢?它到底有什么樣的魔力?
認識 new 運算符
通過下面的例子理解 new 運算符:
function Person (name) {
this.name = name
}
Person.prototype.getName = function () {
console.log(this.name)
}
var joe = new Person('joe')
joe.sayHello = function () {
console.log('Hello!')
}
joe.getName() // joe
joe.sayHello() // Hello!
Person.sayHello() // Uncaught TypeError: Person.sayHello is not a function
Person 是一個普通的函數,當它與 new 運算符一起使用時,Person 就是一個構造函數。通過 new Person('joe') 得到的新對象 joe 繼承了 Person 的屬性,同時,this 也指向 joe 實例。為 joe 添加的屬性 sayHello 不會影響 Person,即 joe 是區別與 Person 的一個新對象。
因此,通過 new 創建的實例對象和構造函數之間建立了一條原型鏈,並通過原型鏈賦予實例對象繼承屬性的能力。
new 的原理和實現
通過上面的分析,new 運算符內部做了如下四個操作:
- 創建一個空的簡單 JavaScript 對象(即{});
- 鏈接新對象(即設置該新對象的構造函數)到函數對象;
- 將新創建的對象作為 this 的上下文;
- 如果該函數沒有返回對象,返回新創建的對象。
new 的實現如下:
function newOperator (ctor, ...args) {
var obj = {};
obj.__proto__ = ctor.prototype
var res = ctor.apply(obj, args)
return res || obj;
}
優化一下代碼:
function newOperator (ctor, ...args) {
var o = Object.create(ctor.prototype) // 合並第一和第二步:創建一個空的簡單 JavaScript 對象(即{}),鏈接新對象(即設置該新對象的構造函數)到函數對象
return fn.apply(o, args) || o
}
使用 newOperator 函數測試上面 Person 的例子:
function Person(name) {
this.name = name
}
Person.prototype.getName = function () {
console.log(this.name)
}
var joe = newOperator(Person, 'joe')
joe.sayHello = function () {
console.log('Hello!')
}
joe.getName() // joe
joe.sayHello() // Hello!
Person.sayHello() // Uncaught TypeError: Person.sayHello is not a function
結果是一致的。
更好的檢查方式是:
function Person(name) {
this.name = name
}
console.log(new Person('joe')) // @1
console.log(newOperator(Person, 'joe')) // @2
@1 和 @2 在控制台的顯示信息是一模一樣的。
判斷是否使用 new 關鍵字
在 JavaScript 中,一個實例對象的創建必須使用 new 關鍵字。但是限於 JavaScript 的語法特征,實際上構造函數同樣可以像普通函數那樣直接執行。那么構造函數內部如何判斷是否使用了 new 關鍵字?
使用 instanceof 檢測
通過理解 new 操作符的原理,可知,在執行 new 操作時,構造函數的 prototype 賦值給了實例的 proto 屬性。在 JavaScript 中 instanceof 可以用來檢測對象的原型鏈,如:a instanceof A 用來檢測 a 是否是 A 的實例(即 a 的原型鏈中存在原型對象 A)。
function Person () {
if (this instanceof Person) {
console.log('new 調用')
} else {
console.log('普通函數調用')
}
}
const foo = new Person() // new 調用
const bar = Person() // 普通函數調用
使用 new.target 屬性
在 ES6 中引入了 new.target 屬性,new.target 屬性允許你檢測函數或構造方法是否是通過 new 運算符被調用的。在通過 new 運算符被初始化的函數或構造方法中,new.target 返回一個指向構造方法或函數的引用。在普通的函數調用中,new.target 的值是 undefined。
function Person () {
console.log(new.target)
}
const foo = new Person() // ƒ Person () { console.log(new.target) }
const bar = Person() // undefined