關於模擬 new操作符的實現

LiberoWang就是我發表於2020-03-12

很簡單的一個分享~


首先看了一下new操作符的使用:

  function Person(name, age) {
    this.name = name;
    this.age = age;
  }
  
  Person.prototype.job = 'engineer';
  Person.prototype.sayAge = function() {
    return this.age;
  }
  
  const person1 = new Person('wang', 24);
  console.log(typeof person1); // "object"
  console.log(person1.name); // wang
  console.log(person1.sayAge()); // 24
  console.log(person1.job); // engineer
複製程式碼

關於模擬 new操作符的實現

Person是一個建構函式,例項person1能訪問到建構函式的屬性,以及Person的原型物件Person.prototype的屬性和方法。

其實我做的更像是已知new的過程,來求證的。new呼叫函式的過程在 ES5官方文件函式定義 一節中做了定義:

  1. 建立一個ECMAScript的原生物件obj.
  2. 給obj設定原生物件的內部屬性;(和原型不同,內部屬性表示為[[PropertyName]],兩個方括號包裹屬性名,並且屬性名大寫,比如[[Prototype]][[Constructor]])
  3. 設定obj的[[Class]]Object.
  4. 設定obj的[[Extensible]]為true.([[Extensible]]決定是否可以向物件新增屬性)
  5. proto的值設定為Fprototype屬性值.
  6. 如果proto是物件型別,則obj的內部屬性[[Prototype]]的值為proto;(進行原型鏈關聯,實現繼承的關鍵)
  7. 如果proto不是物件型別,則設定obj的內部屬性[[Prototype]]的值為內建建構函式Object[[prototype]]的值.(函式的prototype屬性可以被改寫,如果改成非物件型別,obj的[[prototype]]就指向Object的原型物件)
  8. 呼叫函式F,將其返回值賦給result;其中,F執行時的實參傳遞給[[Construct]](即F的本身)的引數,F內部this指向obj.
  9. 如果resultObject型別,返回result.
  10. 如果F返回的不是物件型別(第9步不成立),則返回建立的物件obj.

很多地方都更簡潔的歸納為4個步驟:

  1. 建立一個新的空物件{}.
  2. 將建構函式的作用域賦值給這個新的對.(將建構函式的_prototype_賦值給物件的_proto_屬性)
  3. 執行建構函式的程式碼.
  4. 返回物件.

開始我的簡單的測試

  function Person(name, age) {
    this.name = name;
    this.age = age;
  }
  
  Person.prototype.job = 'engineer';
  Person.prototype.sayAge = function() {
    return this.age;
  }
  
  function newOperator(fn) {
    const obj = {};
    // 或者 const obj = new Object();
    obj._proto_ = fn.prototype;
    fn.apply(obj, arguments);
    return obj;
  }
  
  const person1 = newOperator(Person, 'wang', 24);
  console.log(person1.name);
  console.log(person1.age);
複製程式碼

列印結果:

關於模擬 new操作符的實現

查查查原因:發現arguments導致的。

arguments是一個類陣列,sliceshift結合Function.call可以處理。

重新改一下newOperator函式

  // example 1
  function newOperator(fn, ...res) {
    const obj = new Object();
    obj._proto_ = fn.prototype;
    fn.apply(obj, res);
    return obj;
  }
  
  // example 2
  function newOperator(fn) {
    const obj = new Object();
    obj._proto_ = fn.prototype;
    fn.apply(obj, Array.slice.call(arguments, 1));
    return obj;
  }
  
  // example 3
  function newOperator() {
    const obj = new Object();
    const Construct = [].shift.call(arguments)
    Construct.apply(obj, arguments);
    return obj;
  }
  
  // example 4
  function newOperator(fn) {
    newOperator.target = fn; // ES6 new.target 是指向建構函式
    const obj = Object.create(fn.prototype);
    fn.apply(obj, [].slice.call(arguments, 1));
    return obj;
  }
複製程式碼

修正後的列印結果 -- 正確了:

關於模擬 new操作符的實現

如果我們在建構函式有處理返回值的情況:

  function Person(name, age) {
    this.name = name;
    this.age = age;
    return { name: 'yun' }
  }
  // newOperator 函式直接返回新的物件
  const person1 = newOperator(Person, 'wang', 24);
  console.log(person1.name); // wang
複製程式碼

上面所說的newOperator是最簡單的情況,沒有考慮到之前說的建構函式顯示返回的情況,所以在稍微考慮一下返回結果的情況:

如果函式沒有返回物件型別Object(包含Functoin, Array, Date, RegExg, Error),那麼new表示式中的函式呼叫會自動返回這個新的物件。

  function newOperator() {
    const obj = new Object();
    const Construct = [].shift.call(arguments)
    const result = Construct.apply(obj, arguments);
    const isObject = typeof result === 'object' && result !== null;
    const isFunction = typeof result === 'function';
    if (isObject || isFunction) {
      return result;
    }
    return obj;
  }
複製程式碼

簡單測試一下:

  function Person(name, age) {
    this.name = name;
    this.age = age;
    return { name: 'yun' }
  }
  // newOperator 有判斷建構函式的返回值的情況
  const person1 = newOperator(Person, 'wang', 24);
  console.log(person1.name); // yun
複製程式碼

一點點說明

關於Object型別的檢測,可以用typeOf,也可以用instanceof

  function newOperator() {
    const obj = new Object();
    const Construct = [].shift.call(arguments)
    const result = Construct.apply(obj, arguments);
    return (result instanceof Object) ? result : obj;
  }
複製程式碼

但是instanceof的工作原理是:在表示式x instanceof Foo 中,如果 Foo 的原型(即 Foo.prototype)出現在x的原型鏈中,則返回 true,不然,返回false

例項建立之後重寫建構函式原型,例項指向的原型已經不是建構函式的新的原型了。

  const Foo = function() {};
  const o = new Foo();
  o instanceof Foo; // true

  // 重寫 Foo 原型
  Foo.prototype = {};
  o instanceof Foo; // false
複製程式碼

關於建立的新的物件:

  //方式多種
  const obj = {};
  const obj = new Object();
  const obj = Object.create();
複製程式碼

但是我還沒有理解他們是有什麼區別的嗎,在最後建立出來例項當中的引用。

參考資料:

JS中new呼叫函式原理

深入之new的模擬實現

相關文章