夜深了......
1. 前言#
在探究 JavaScript 中的構造函數之前,我們需要搞清楚什麼是構造函數,正如其名字一樣 “構造” ,它的作用就是構造,接下來我們來看看它是在構造什麼。
在繼續往下說之前,我們需要明確所謂的構造函數其實就是一個函數,但是它具有自己的特徵和用法,但是我們會發現如何區分和使用構造函數,那就不得不離開 JavaScript 中的new
關鍵字了。
2.new 關鍵字#
在說明 JavaScript 中的構造函數時,我們必然不可能繞過它的兩個特點:
- 函數體內部使用了 this 關鍵字,代表所要生成的對象實例。
- 生成對象的時候,必須使用 new 關鍵字。
1.new 命令的基礎用法#
以下就是一個最基礎的使用new
命令來實例化一個構造函數
let MyNew = function (){
this.say_hello = function (){
console.log('hello world')
}
}
let mynew = new MyNew
上述的操作就是通過 new 命令,讓構造函數MyNew
生成一個實例對象,並將結果交給mynew
,mynew
也就得到了構造函數中的屬性。
2. 傳參構造函數#
在實際中我們不可能通過一個構造函數來構造出一大堆相同對象,所以我們可以在new
一個構造函數時傳入參數。
let MyNew_1 = function (name){
this.name = name
}
let mynew_1 = new MyNew_1('張三')
console.log(mynew_1.name) // '張三'
上述的操作就是通過new
關鍵字在實例化一個對象的時候傳入對應的參數。
3.new 關鍵字的規範#
因為在使用 new 關鍵字對構造函數進行實例化的時候,在最後它會自動的執行,所以在使用 new 關鍵字的可以發現。
let mynew_2 = new MyNew_1 // 不推薦
let mynew_3 = new MyNew_1() // 推薦
上述的兩種方式雖然都可以,但是更推薦使用第二種,相較於第一種來說,更容易被識別。
4. 注意#
在使用構造函數的時候,如果我們在使用構造函數時忘記了使用new
關鍵字,那麼它將會被當成一個函數來執行。
let MyNew_1 = function (name){
this.name = name
}
let test = MyNew_1()
console.log(test.name) // undefined
上述之所以會輸出undefined
,使用為MyNew_1
在被當成函數後,它的作用域就是全局的,而內部的this
就指向的是window
而window
並沒有聲明這個變量,所以最後輸出了undefined
。
那麼我們應該怎麼避免這樣的事情發生呢?
1. 使用嚴格模式
除了正常運行模式,ECMAscript5 添加了第二種運行模式:“嚴格模式”(strict mode)。顧名思義,這種模式是的Javascript
在更嚴格的條件下運行。
function Fubar(foo, bar){
'use strict';
this._foo = foo;
this._bar = bar;
}
Fubar()
使用嚴格模式後,如果你將構造函數當成函數使用,那麼將會直接報錯。結果如下:
之所以會報錯,主要歸功於在嚴格模式中,函數內部的 this 不能指向全局對象,默認等於undefined
,導致不加new
調用會報錯(JavaScript 不允許對undefined
添加屬性)。
2. 使用 instanceof 關鍵字
我們可以通過instanceof
關鍵字來判斷。
function Fubar_1(foo, bar) {
if (!(this instanceof Fubar_1)) {
return new Fubar(foo, bar);
}
this._foo = foo;
this._bar = bar;
}
console.log(Fubar_1(1, 2)._foo)// 1
console.log((new Fubar_1(1, 2))._foo) // 1
如上述代碼,我們只需要判斷this
是否為Fubar_1
的實例對象就可以知道該構造函數是否使用了new
關鍵字,如果沒有,則幫你new
,並將實例化的對象返回。
3.new 關鍵字的原理#
在我們使用new
關鍵字的時候,實際上它執行了下面的幾步:
- 創建一個空對象,作為將要返回的對象實例。
- 將這個空對象的原型,指向構造函數的
prototype
屬性。 - 將這個空對象賦值給函數內部的
this
關鍵字。 - 開始執行構造函數內部的代碼。
轉化為代碼就是如下的操作:
function _new(/* 構造函數 */ constructor, /* 構造函數參數 */ params) {
// 將 arguments 對象轉為數組
var args = [].slice.call(arguments);
// 取出構造函數
var constructor = args.shift();
// 創建一個空對象,繼承構造函數的 prototype 屬性
var context = Object.create(constructor.prototype);
// 執行構造函數
var result = constructor.apply(context, args);
// 如果返回結果是對象,就直接返回,否則返回 context 對象
return (typeof result === 'object' && result != null) ? result : context;
}
// 實例
var actor = _new(Person, '張三', 28);
後面繼續了解下🧐🧐🧐
4. 構造函數中的返回值#
當構造函數中有return
語句的時候,我們只需要記住一點如果返回的是一個對象,則最終返回的就是這個對象,如果不是一個對象,則會返回this
。
let MyNew_2 = function (){
this.count = 0
return this.count
}
(new MyNew_2()) === 0 // false
let MyNew_3 = function (){
this.count = 0
return {count:this.count}
}
(new MyNew_3()).count === 0 // true
5.new.target#
這是 ECMASript 6 引入一個 新的屬性它主要有兩個作用:
-
當我們使用
new
操作符調用構造函數時,new.target
屬性的值為構造函數,否則為undefined
function Person (fullName) { if (typeof new.target !== 'undefined') { this.fullName = fullName; } else { throw new Error('須通過 new 關鍵字來調用 Person。'); } } let student = new Person('aa'); let student_1 = Person('aa'); // 報錯
-
可以檢查
new.target
是否被某個特定構造函數所有調用。function Person (fullName, age) { if (typeof new.target === Person) { this.fullName = fullName; this.age = age; } else { throw new Error('必須通過 new 關鍵字來調用 Person。'); } } function Dog (fullName, age) { Person.call(this, fullName, age) } let dog = new Dog('HeHe', 3) console.log(dog)
在 es5 中如果我們也要確定一個構造函數是否使用,那麼我們將第一時間想到instanceof
。
如下:
function Person (name) {
if(this instanceof Person){
this.name = name
} else {
throw new Error('未通過new調用')
}
}
var person2 = Person('Hello') //拋出異常
通過上述的操作,我們就可以判斷構造函數是否使用new
關鍵字。但是它不夠好,比如下面這個例子:
function Person (name) {
if(this instanceof Person){
this.name = name
} else {
throw new Error('未通過new調用')
}
}
var person1 = new Person('Hello')
var person2 = Person.call(person1,'Hello') //正常執行
上述代碼中使用call
方法將 this 設為person1
的實例,對於函數本身,無法通過區分Person.call
還是new
關鍵字得到Person
的實例。但是通過new.target
就可以實現😍😍😍
6. 參考鏈接#
- 王昱潇 --- 判斷函數被調用的方法
- 網道 --- 實例對象與 new 命令
- 萌新小吉 --- 嚴格模式詳解