如何寫一個作用域安全的建構函式

汪氵淼發表於2018-11-30

基礎部分

建構函式本質上就是一個使用new操作符呼叫的函式,使用new呼叫時,建構函式內用到的this物件會指向新建立的物件例項:

function Girlfriend(name, age, height) {
  this.name = name;
  this.age = age;
  this.height = height;
}

// 使用new操作符來分配這些屬性
var girlfriend = new Girlfriend("Ying", 23, 170);

平時寫變數時,如果因為失誤忘記使用new操作符時,會造成一個糟糕的影響————因為this物件是在執行時繫結的,直接呼叫建構函式,this會對映到全域性物件window上,導致錯誤物件屬性的增加,增添不必要的變數到全域性物件中:

var girlfriend = new Girlfriend("Ying", 23, 170);
console.log(window.name); // "Ying"
console.log(window.age); // 23
console.log(window.height); // 170

特別的,當你自己建構函式內的某些變數名與window變數名重名時(像一些常用變數名name、length等),對這些屬性的偶然覆蓋就很可能導致其他地方出錯,並且這個bug還相當難找!

在這種情況下構造一個作用域安全的建構函式就顯得很有必要:

function Girlfriend(name, age, height) {
  // 首先確認this物件是正確型別的例項,
  // 如果不是就建立新的例項並返回
  if (this instanceof Girlfriend) { // 新增一個檢查
    console.log(`created`);
    this.name = name;
    this.age = age;
    this.height = height;
  } else {
    console.log(`new`);
    return new Girfriend(name, age, height);
  }
}

var girlfriend1 = Girlfriend("Ying", 23, 170); // "new" "created"
console.log(window.name); // ""
console.log(girfriend1.name); // "Ying"

var girlfriend2 = new Girlfriend("Lin", 22, 165); // "created"
console.log(girfriend1.name); // "Lin"

girlfriend1背後建構函式先new了一個例項並返回例項(列印“new”),再對例項進行賦值(列印“created”)。
girlfriend2自己就先new了一個例項,直接對該例項進行賦值(只列印“created”)。

這樣在任何情況下就都可以返回一個安全作用域的例項了。

進階部分

使用上面新增一個檢查的方法可以建立一個作用域安全的建構函式,但如果有的函式竊取該函式的繼承且沒有使用原型鏈,那這個繼承將被破壞不生效:

function Bmi(sex, weight=1, height=1) { // ES6開始支援的預設值
  if (this instanceof Bmi) {
    this.sex = sex;
    this.weight = weight;
    this.height = height;
    this.getBmi = function() {
      return this.weight / (this.height ** 2);
    };
  } else {
    return new Bmi(sex);
  }
}

function People(height, weight) {
  Bmi.call(this, `male`);
  this.height = height;
  this.weight = weight;
}

var guy = new People(1.75, 68); // 單位是m和kg
console.log(guy.sex) // undefined

Bmi建構函式作用域是安全的,但People並不是。新建立一個People例項後,這個例項準備通過Bmi.call()來繼承Bmi的sex屬性,但由於Bmi的作用域是安全的,this物件並非是Bmi的例項,所以Bmi會先自己建立一個新的Bmi物件,不會把新的Bmi物件的值傳遞到People中去。

這樣People中的this物件並沒有得到增長,同時Bmi.call()返回的值也沒有用到,所以People例項中就不會有sex、weight、height屬性和getBmi()函式。

解決辦法: 建構函式結合使用原型鏈或寄生組合:

function Bmi(sex, weight=1, height=1) {
  if (this instanceof Bmi) {
    this.sex = sex;
    this.weight = weight;
    this.height = height;
    this.getBmi = function() {
      return this.weight / (this.height ** 2);
    };
  } else {
    return new Bmi(sex);
  }
}

function People(height, weight) {
  Bmi.call(this, `male`);
  this.height = height;
  this.weight = weight;
}


People.prototype = new Bmi(); // 重點

var guy = new People(1.75, 68);
console.log(guy.sex) // "male"

這樣寫的話,一個People的例項同時也是一個Bmi的例項,所以Bmi.call()才會去執行,為People例項新增上屬性和函式。

總結

當多個人一同構建一個專案時,作用域構安全函式就非常必要,對全域性物件意外的更改可能就會導致一些常常難以追蹤的錯誤,這和平常設定空變數和空函式一樣避免因為其他人可能發生的錯誤而阻塞程式執行。

相關文章