JavaScript深入之new的模擬實現

冴羽發表於2017-05-04

JavaScript深入系列第十二篇,通過new的模擬實現,帶大家揭開使用new獲得建構函式例項的真相

new

一句話介紹 new:

new 運算子建立一個使用者定義的物件型別的例項或具有建構函式的內建物件型別之一

也許有點難懂,我們在模擬 new 之前,先看看 new 實現了哪些功能。

舉個例子:

// Otaku 御宅族,簡稱宅
function Otaku (name, age) {
    this.name = name;
    this.age = age;

    this.habit = 'Games';
}

// 因為缺乏鍛鍊的緣故,身體強度讓人擔憂
Otaku.prototype.strength = 60;

Otaku.prototype.sayYourName = function () {
    console.log('I am ' + this.name);
}

var person = new Otaku('Kevin', '18');

console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // 60

person.sayYourName(); // I am Kevin複製程式碼

從這個例子中,我們可以看到,例項 person 可以:

  1. 訪問到 Otaku 建構函式裡的屬性
  2. 訪問到 Otaku.prototype 中的屬性

接下來,我們可以嘗試著模擬一下了。

因為 new 是關鍵字,所以無法像 bind 函式一樣直接覆蓋,所以我們寫一個函式,命名為 objectFactory,來模擬 new 的效果。用的時候是這樣的:

function Otaku () {
    ……
}

// 使用 new
var person = new Otaku(……);
// 使用 objectFactory
var person = objectFactory(Otaku, ……)複製程式碼

初步實現

分析:

因為 new 的結果是一個新物件,所以在模擬實現的時候,我們也要建立一個新物件,假設這個物件叫 obj,因為 obj 會具有 Otaku 建構函式裡的屬性,想想經典繼承的例子,我們可以使用 Otaku.apply(obj, arguments)來給 obj 新增新的屬性。

在 JavaScript 深入系列第一篇中,我們便講了原型與原型鏈,我們知道例項的 __proto__ 屬性會指向建構函式的 prototype,也正是因為建立起這樣的關係,例項可以訪問原型上的屬性。

現在,我們可以嘗試著寫第一版了:

// 第一版程式碼
function objectFactory() {

    var obj = new Object(),

    Constructor = [].shift.call(arguments);

    obj.__proto__ = Constructor.prototype;

    Constructor.apply(obj, arguments);

    return obj;

};複製程式碼

在這一版中,我們:

  1. 用new Object() 的方式新建了一個物件 obj
  2. 取出第一個引數,就是我們要傳入的建構函式。此外因為 shift 會修改原陣列,所以 arguments 會被去除第一個引數
  3. 將 obj 的原型指向建構函式,這樣 obj 就可以訪問到建構函式原型中的屬性
  4. 使用 apply,改變建構函式 this 的指向到新建的物件,這樣 obj 就可以訪問到建構函式中的屬性
  5. 返回 obj

更多關於:

原型與原型鏈,可以看《JavaScript深入之從原型到原型鏈》

apply,可以看《JavaScript深入之call和apply的模擬實現》

經典繼承,可以看《JavaScript深入之繼承》

複製以下的程式碼,到瀏覽器中,我們可以做一下測試:

function Otaku (name, age) {
    this.name = name;
    this.age = age;

    this.habit = 'Games';
}

Otaku.prototype.strength = 60;

Otaku.prototype.sayYourName = function () {
    console.log('I am ' + this.name);
}

function objectFactory() {
    var obj = new Object(),
    Constructor = [].shift.call(arguments);
    obj.__proto__ = Constructor.prototype;
    Constructor.apply(obj, arguments);
    return obj;
};

var person = objectFactory(Otaku, 'Kevin', '18')

console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // 60

person.sayYourName(); // I am Kevin複製程式碼

[]~( ̄▽ ̄)~**

返回值效果實現

接下來我們再來看一種情況,假如建構函式有返回值,舉個例子:

function Otaku (name, age) {
    this.strength = 60;
    this.age = age;

    return {
        name: name,
        habit: 'Games'
    }
}

var person = new Otaku('Kevin', '18');

console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // undefined
console.log(person.age) // undefined複製程式碼

在這個例子中,建構函式返回了一個物件,在例項 person 中只能訪問返回的物件中的屬性。

而且還要注意一點,在這裡我們是返回了一個物件,假如我們只是返回一個基本型別的值呢?

再舉個例子:

function Otaku (name, age) {
    this.strength = 60;
    this.age = age;

    return 'handsome boy';
}

var person = new Otaku('Kevin', '18');

console.log(person.name) // undefined
console.log(person.habit) // undefined
console.log(person.strength) // 60
console.log(person.age) // 18複製程式碼

結果完全顛倒過來,這次儘管有返回值,但是相當於沒有返回值進行處理。

所以我們還需要判斷返回的值是不是一個物件,如果是一個物件,我們就返回這個物件,如果沒有,我們該返回什麼就返回什麼。

再來看第二版的程式碼,也是最後一版的程式碼:

// 第二版的程式碼
function objectFactory() {

    var obj = new Object(),

    Constructor = [].shift.call(arguments);

    obj.__proto__ = Constructor.prototype;

    var ret = Constructor.apply(obj, arguments);

    return typeof ret === 'object' ? ret : obj;

};複製程式碼

下一篇文章

JavaScript深入之類陣列物件與arguments

相關連結

《JavaScript深入之從原型到原型鏈》

《JavaScript深入之call和apply的模擬實現》

《JavaScript深入之繼承》

深入系列

JavaScript深入系列目錄地址:github.com/mqyqingfeng…

JavaScript深入系列預計寫十五篇左右,旨在幫大家捋順JavaScript底層知識,重點講解如原型、作用域、執行上下文、變數物件、this、閉包、按值傳遞、call、apply、bind、new、繼承等難點概念。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎star,對作者也是一種鼓勵。

相關文章