前端面試必備 | 5000字長文解釋千萬不能錯過的原型操做方法及其模擬實現(原型篇:下)

這篇文章主要講解原型的查找、變動、判斷和刪除,附帶着對原型的做用方式作一下回顧。前端

instanceof

instanceof 運算符用於檢測構造函數的 prototype 屬性是否出如今某個實例對象的原型鏈上。面試

即經過下面的操做來判斷:瀏覽器

object.__proto__ === Constructor.prototype ?

object.__proto__.__proto__ === Constructor.prototype ?

object.__proto__.__proto__....__proto__ === Constructor.prototype
複製代碼

當左邊的值是 null 時,會中止查找,返回 false閉包

實際是檢測 Constructor.prototype 是否存在於參數 object 的原型鏈上。app

用法:ide

object instanceof Constructor
複製代碼

看看下面的例子:函數

// 定義構造函數
function C(){} 
function D(){} 

var o = new C();

o instanceof C; // true,由於 Object.getPrototypeOf(o) === C.prototype

o instanceof D; // false,由於 D.prototype 不在 o 的原型鏈上

o instanceof Object; // true,由於 Object.prototype.isPrototypeOf(o) 返回 true
C.prototype instanceof Object // true,同上

C.prototype = {};
var o2 = new C();

o2 instanceof C; // true

o instanceof C; // false,C.prototype 指向了一個空對象,這個空對象不在 o 的原型鏈上.

D.prototype = new C(); // 繼承
var o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 由於 C.prototype 如今在 o3 的原型鏈上
複製代碼

須要注意的是 Constructor.prototype 可能會因爲人爲的改動,致使在改動以前實例化的對象在改動以後的判斷返回 falseC.prototype = {}; 直接更改了構造函數的原型對象的指向,因此後面再次執行 o instanceof C; 會返回 false佈局

再看看下面一組例子,演示 String Date 對象都屬於 Object 類型。測試

var simpleStr = "This is a simple string"; 
var myString  = new String();
var newStr    = new String("String created with constructor");
var myDate    = new Date();
var myObj     = {};
var myNonObj  = Object.create(null);

simpleStr instanceof String; // 返回 false, 檢查原型鏈會找到 undefined
myString  instanceof String; // 返回 true
newStr    instanceof String; // 返回 true
myString  instanceof Object; // 返回 true

myObj instanceof Object;    // 返回 true, 儘管原型沒有定義
({})  instanceof Object;    // 返回 true, 同上
myNonObj instanceof Object; // 返回 false, 一種建立非 Object 實例的對象的方法

myString instanceof Date; //返回 false

myDate instanceof Date;     // 返回 true
myDate instanceof Object;   // 返回 true
myDate instanceof String;   // 返回 false
複製代碼

instanceof 模擬實現ui

function simulateInstanceOf(left, right) {
  if (right === null || right === undefined) {
    throw new TypeError(`Right-hand side of ' instanceof ' is not an object`)
  }
  const rightPrototype = right.prototype
  left = Object.getPrototypeOf(left)

  while (left !== null) {
    if (left === rightPrototype) return true
    left = Object.getPrototypeOf(left)
  }

  return false
}
複製代碼

Symbol.hasInstance

Symbol.hasInstance 用於判斷某對象是否爲某構造器的實例。所以你能夠用它自定義 instanceof 操做符在某個類上的行爲。

class MyArray {  
  static [Symbol.hasInstance](instance) {
    // instance 是左邊的參數
    return Array.isArray(instance);
  }
}
console.log([] instanceof MyArray); // true
複製代碼

Object.prototype.isPrototypeOf()

prototypeObj.isPrototypeOf(object)

isPrototypeOf() 方法用於測試一個對象是否存在於另外一個對象的原型鏈上。

function Foo() {}
function Bar() {}
function Baz() {}

Bar.prototype = Object.create(Foo.prototype);
Baz.prototype = Object.create(Bar.prototype);

var baz = new Baz();

console.log(Baz.prototype.isPrototypeOf(baz)); // true
console.log(Bar.prototype.isPrototypeOf(baz)); // true
console.log(Foo.prototype.isPrototypeOf(baz)); // true
console.log(Object.prototype.isPrototypeOf(baz)); // true
複製代碼

Object.getPrototypeOf

Object.getPrototypeOf(object)

Object.getPrototypeOf() 方法返回指定對象的原型(內部 [[Prototype]] 屬性的值)。若是沒有繼承屬性,則返回 null

var proto = {};
var obj = Object.create(proto);
Object.getPrototypeOf(obj) === proto; // true

var reg = /a/;
Object.getPrototypeOf(reg) === RegExp.prototype; // true
複製代碼

注意:Object.getPrototypeOf(Object) 不是 Object.prototype

ObjectFunction 都屬於函數對象,因此它們都是 Function 構造函數的實例,也就是說,會有下面的結果,具體緣由請看個人上一篇文章

Object instanceof Function
// true
複製代碼

Object.getPrototypeOf( Object ) 是把 Object 這一構造函數看做對象,返回的固然是函數對象的原型,也就是 Function.prototype

正確的方法是,Object.prototype 是構造出來的對象的原型。

var obj = new Object();
Object.prototype === Object.getPrototypeOf( obj );              // true
Object.prototype === Object.getPrototypeOf( {} );               // true
複製代碼

在 ES5 中,若是參數不是一個對象類型,將拋出一個 TypeError 異常。在 ES6 中,參數會被強制轉換爲一個 Object(使用包裝對象來獲取原型)。

Object.getPrototypeOf('foo');
// TypeError: "foo" is not an object (ES5)
Object.getPrototypeOf('foo');
// String.prototype (ES6)
複製代碼

該方法的模擬實現:

Object.getPrototypeOf = function(obj) {
  if (obj === null || obj === undefined) {
    throw new Error('Cannot convert undefined or null to object')
  }
  if (typeof obj === 'boolean' || typeof obj === 'number' || typeof obj === 'string') return Object(obj).__proto__
  return obj.__proto__
}
複製代碼

Object.setPrototypeOf

Object.setPrototypeOf(obj, prototype)

Object.setPrototypeOf() 方法設置一個指定的對象的原型 ( 即, 內部 [[Prototype]] 屬性)到另外一個對象或 null

若是 prototype 參數不是一個對象或者 null (例如,數字,字符串,boolean,或者 undefined),則會報錯。該方法將 obj[[Prototype]] 修改成新的值。

對於 Object.prototype.__proto__ ,它被認爲是修改對象原型更合適的方法。

該方法的模擬實現:

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj; 
}
複製代碼

Object.create

Object.create(proto[, propertiesObject])

propertiesObject 對應 Object.defineProperties() 的第二個參數,表示給新建立的對象的屬性設置描述符。

若是 propertiesObject 參數是 null 或非原始包裝對象,則拋出一個 TypeError 異常。

Object.create() 方法建立一個新對象,使用現有的對象來提供新建立的對象的 __proto__

看下面的例子:

const person = {
  isHuman: false,
  printIntroduction: function () {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  }
};

const me = Object.create(person);

me.name = "Matthew"; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // inherited properties can be overwritten

me.printIntroduction();
// expected output: "My name is Matthew. Am I human? true"
複製代碼

上面的操做和咱們實例化一個新對象很相似。

下面咱們使用 Object.create() 實現繼承,Object.create() 用來構建原型鏈,使用構造函數給實例附加本身的屬性:

// Shape - 父類(superclass)
function Shape() {
  this.x = 0;
  this.y = 0;
}

// 父類添加原型方法
Shape.prototype.move = function(x, y) {
  this.x += x;
  this.y += y;
  console.info('Shape moved.');
};

// Rectangle - 子類(subclass)
function Rectangle() {
  // 讓子類的實例也擁有父類的構造函數中的附加的屬性
  Shape.call(this); // call super constructor.
}

// 子類繼承父類
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

var rect = new Rectangle();

console.log('Is rect an instance of Rectangle?', rect instanceof Rectangle); // true
console.log('Is rect an instance of Shape?', rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'
複製代碼

關於 Object.createpropertyObject 參數

若是不指定對應的屬性描述符,則默認都是 false。描述符有如下幾個:

  1. enumerable 可枚舉,默認 false
  2. configurable 可刪除,默認 false
  3. writable 可賦值,默認 false
  4. value 屬性的值

看下面的例子:

var 0;
o = Object.create(Object.prototype, {
  name: {
    value: 'lxfriday', // 其餘屬性描述符都是 false
  },
  age: {
    value: 100,
    enumerable: true, // 除了可枚舉,其餘描述符都是 false
  }
})
複製代碼

從上面的結果能夠看出,描述符默認都是 false,不可枚舉的屬性也沒法經過 ES6 的對象擴展進行淺複製。

Object.create 的模擬實現:

Object.create = function(proto, propertiesObject) {
  const res = {}
  // proto 只能爲 null 或者 type 爲 object 的數據類型
  if (!(proto === null || typeof proto === 'object')) {
    throw new TypeError('Object prototype may only be an Object or null')
  }
  Object.setPrototypeOf(res, proto)

  if (propertiesObject === null) {
    throw new TypeError('Cannot convert undefined or null to object')
  }
  if (propertiesObject) {
    Object.defineProperties(res, propertiesObject)
  }

  return res
}

複製代碼

Object.assign

Object.assign(target, ...sources)

方法用於將全部可枚舉屬性的值從一個或多個源對象複製到目標對象。它將返回目標對象。它屬於淺拷貝,只會複製引用。

若是目標對象中的屬性具備相同的鍵,則屬性將被源對象中的屬性覆蓋。後面的源對象的屬性將相似地覆蓋前面的源對象的屬性。

Object.assign 方法只會拷貝源對象自身的而且可枚舉的屬性到目標對象。該方法使用源對象的 [[Get]] 和目標對象的 [[Set]],因此它會調用相關 gettersetter。若是合併源包含 getter,這可能使其不適合將新屬性合併到原型中。

String 類型和 Symbol 類型的屬性都會被拷貝。

當拷貝的中途出錯時,已經拷貝的值沒法 rollback,也就是說可能存在只拷貝部分值的狀況。

Object.assign 不會在那些 source 對象值爲 nullundefined 的時候拋出錯誤。

const o1 = { a: 1, b: 1, c: 1 };
const o2 = { b: 2, c: 2 };
const o3 = { c: 3 };

const obj = Object.assign({}, o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
複製代碼

拷貝 symbol 類型的屬性

const o1 = { a: 1 };
const o2 = { [Symbol('foo')]: 2 };

const obj = Object.assign({}, o1, o2);
console.log(obj); // { a : 1, [Symbol("foo")]: 2 }
Object.getOwnPropertySymbols(obj); // [Symbol(foo)]
複製代碼

繼承屬性和不可枚舉屬性是不能拷貝的

const obj = Object.create({foo: 1}, { // foo 是個繼承屬性。
  bar: {
    value: 2  // bar 是個不可枚舉屬性。
  },
  baz: {
    value: 3,
    enumerable: true  // baz 是個自身可枚舉屬性。
  }
});

const copy = Object.assign({}, obj);
console.log(copy); // { baz: 3 }
複製代碼

原始類型會被包裝爲對象

const v1 = "abc";
const v2 = true;
const v3 = 10;
const v4 = Symbol("foo")

const obj = Object.assign({}, v1, null, v2, undefined, v3, v4); 
// 原始類型會被包裝,null 和 undefined 會被忽略。
// 注意,只有字符串的包裝對象纔可能有自身可枚舉屬性。
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
複製代碼

異常會打斷後續拷貝任務

const target = Object.defineProperty({}, "foo", {
  value: 1,
  writable: false
}); // target 的 foo 屬性是個只讀屬性。

Object.assign(target, {bar: 2}, {foo2: 3, foo: 3, foo3: 3}, {baz: 4});
// TypeError: "foo" is read-only
// 注意這個異常是在拷貝第二個源對象的第二個屬性時發生的。

console.log(target.bar);  // 2,說明第一個源對象拷貝成功了。
console.log(target.foo2); // 3,說明第二個源對象的第一個屬性也拷貝成功了。
console.log(target.foo);  // 1,只讀屬性不能被覆蓋,因此第二個源對象的第二個屬性拷貝失敗了。
console.log(target.foo3); // undefined,異常以後 assign 方法就退出了,第三個屬性是不會被拷貝到的。
console.log(target.baz);  // undefined,第三個源對象更是不會被拷貝到的。
複製代碼

拷貝訪問器

訪問器是一個函數, Object.assign 拷貝的時候會直接調用 getter 函數。

const obj = {
  foo: 1,
  get bar() {
    return 2;
  }
};

let copy = Object.assign({}, obj); 
console.log(copy); // { foo: 1, bar: 2 } copy.bar的值來自obj.bar的getter函數的返回值

// 下面這個函數會拷貝全部自有屬性的屬性描述符
function completeAssign(target, ...sources) {
  sources.forEach(source => {
    let descriptors = Object.keys(source).reduce((descriptors, key) => {
      descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
      return descriptors;
    }, {});

    // Object.assign 默認也會拷貝可枚舉的Symbols
    Object.getOwnPropertySymbols(source).forEach(sym => {
      let descriptor = Object.getOwnPropertyDescriptor(source, sym);
      if (descriptor.enumerable) {
        descriptors[sym] = descriptor;
      }
    });
    Object.defineProperties(target, descriptors);
  });
  return target;
}

copy = completeAssign({}, obj);
console.log(copy);
// { foo:1, get bar() { return 2 } }
複製代碼

Object.assign 的模擬實現:

function assign(target, sources) {
  if (target === null || target === undefined) {
    throw new TypeError('Cannot convert undefined or null to object')
  }

  const targetType = typeof target
  const to = targetType === 'object' ? target : Object(target)

  for (let i = 1; i < arguments.length; i++) {
    const source = arguments[i]
    const sourceType = typeof source
    if (sourceType === 'object' || sourceType === 'string') {
      for (const key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
          to[key] = source[key]
        }
      }
    }
  }
  return to
}

Object.defineProperty(Object, 'assign', {
  value: assign,
  writable: true,
  configurable: true,
  enumerable: false,
})
複製代碼

new Constructor()

new constructor[([arguments])]

咱們使用 new 能夠創造一個指向構造函數原型的對象,而且讓該對象擁有構造函數中指定的屬性。

new 操做符的行爲有如下三點須要特別注意,當代碼 new Foo(...) 執行時,會發生如下事情:

  1. 一個繼承自 Foo.prototype 的新對象被建立;
  2. 使用指定的參數調用構造函數 Foo,並將 this 綁定到新建立的對象。new Foo 等同於 new Foo(),也就是沒有指定參數列表,Foo 不帶任何參數調用的狀況。
  3. 由構造函數返回的對象就是 new 表達式的結果。若是構造函數沒有顯式返回一個對象,則使用步驟1建立的對象。(通常狀況下,構造函數不返回值,可是用戶能夠選擇主動返回對象,來覆蓋正常的對象建立步驟)

上面的第三步,返回 null 時,雖然 typeofobject,可是仍然會返回步驟一中建立的對象。

new 的模擬實現:

function monitorNew(constructor, args) {
  // 提取構造函數和參數,arguments 被處理以後不包含構造函數
  const Constructor = Array.prototype.shift.call(arguments)
  // 建立新對象,並把新對象的原型指向 Constructor.prototype 
  const target = Object.create(Constructor.prototype)
  // 把新對象做爲上下文,執行 Constructor
  const ret = Constructor.apply(target, arguments)
  // 構造函數返回 null,則返回建立的新對象
  if (ret === null) return target
  // 若是是對象則返回指定的對象,不然返回建立的對象
  return typeof ret === 'object' ? ret : target
}
複製代碼

參考

最後

往期精彩:

關注公衆號能夠看更多哦。

感謝閱讀,歡迎關注個人公衆號 雲影 sky,帶你解讀前端技術,掌握最本質的技能。關注公衆號能夠拉你進討論羣,有任何問題都會回覆。

公衆號
相關文章
相關標籤/搜索