不能錯過的原型操作方法和實現(原型篇:下)


前端面試必備 | 不能錯過的原型操作方法和實現(原型篇:下)

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

instanceof

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

即通過下面的操作來判斷:

<code>object.__proto__ === Constructor.prototype ?

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

object.__proto__.__proto__....__proto__ === Constructor.prototype
/<code>

當左邊的值是 null 時,會停止查找,返回 false。

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

用法:

<code>object instanceof Constructor
/<code>

看看下面的例子:

<code>// 定義構造函數
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 的原型鏈上
/<code>

需要注意的是 Constructor.prototype 可能會由於人為的改動,導致在改動之前實例化的對象在改動之後的判斷返回 false。 C.prototype = {}; 直接更改了構造函數的原型對象的指向,所以後面再次執行 o instanceof C; 會返回 false。

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

<code>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
/<code>

instanceof 模擬實現

<code>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
}
/<code>

Symbol.hasInstance

Symbol.hasInstance 用於判斷某對象是否為某構造器的實例。因此你可以用它自定義 instanceof 操作符在某個類上的行為。

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

Object.prototype.isPrototypeOf()

prototypeObj.isPrototypeOf(object)

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

<code>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
/<code>

Object.getPrototypeOf

Object.getPrototypeOf(object)

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

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

var reg = /a/;
Object.getPrototypeOf(reg) === RegExp.prototype; // true
/<code>

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

前端面試必備 | 不能錯過的原型操作方法和實現(原型篇:下)

Object 和 Function 都屬於函數對象,所以它們都是 Function 構造函數的實例,也就是說,會有下面的結果,具體原因請看我的上一篇文章

<code>Object instanceof Function
// true
/<code>

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

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

<code>var obj = new Object();
Object.prototype === Object.getPrototypeOf( obj ); // true
Object.prototype === Object.getPrototypeOf( {} ); // true
/<code>

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

<code>Object.getPrototypeOf('foo');
// TypeError: "foo" is not an object (ES5)
Object.getPrototypeOf('foo');
// String.prototype (ES6)
/<code>

該方法的模擬實現:

<code>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__
}
/<code>

Object.setPrototypeOf

Object.setPrototypeOf(obj, prototype)

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

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

前端面試必備 | 不能錯過的原型操作方法和實現(原型篇:下)

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

該方法的模擬實現:

<code>Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
/<code>

Object.create

Object.create(proto[, propertiesObject])

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

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

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

看下面的例子:

<code>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"
/<code>
前端面試必備 | 不能錯過的原型操作方法和實現(原型篇:下)

上面的操作和我們實例化一個新對象很類似。

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

<code>// 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.'
/<code>

關於 Object.create 的 propertyObject 參數

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

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

看下面的例子:

<code>var 0;
o = Object.create(Object.prototype, {

name: {
value: 'lxfriday', // 其他屬性描述符都是 false
},
age: {
value: 100,
enumerable: true, // 除了可枚舉,其他描述符都是 false
}
})
/<code>
前端面試必備 | 不能錯過的原型操作方法和實現(原型篇:下)

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

Object.create 的模擬實現:

<code>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
}

/<code>

Object.assign

Object.assign(target, ...sources)

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

如果目標對象中的屬性具有相同的鍵,則屬性將被源對象中的屬性覆蓋。後面的源對象的屬性將類似地覆蓋前面的源對象的屬性。

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

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

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

Object.assign 不會在那些 source 對象值為 null 或 undefined 的時候拋出錯誤。

<code>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 }
/<code>

拷貝 symbol 類型的屬性

<code>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)]
/<code>

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

<code>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 }
/<code>

原始類型會被包裝為對象

<code>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" }
/<code>

異常會打斷後續拷貝任務

<code>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,第三個源對象更是不會被拷貝到的。
/<code>

拷貝訪問器

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

<code>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 } }

/<code>

Object.assign 的模擬實現:

<code>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,
})
/<code>

new Constructor()

new constructor[([arguments])]

我們使用 new 可以創造一個指向構造函數原型的對象,並且讓該對象擁有構造函數中指定的屬性。

new 操作符的行為有以下三點需要特別注意,當代碼 new Foo(...) 執行時,會發生以下事情:

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

上面的第三步,返回 null 時,雖然 typeof 是 object,但是仍然會返回步驟一中創建的對象。

new 的模擬實現:

<code>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
}
/<code>

參考

  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/instanceof
  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf
  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf
  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/isPrototypeOf
  • https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Object/create
  • https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new

最後

往期精彩:

  • 前端面試必備 | 古怪的原型(雞生蛋還是蛋生雞)(原型篇:中)
  • 前端面試必備 | 使用原型和構造函數創建對象(原型篇:上)
  • 前端面試必會 | 一文讀懂 JavaScript 中的 this 關鍵字
  • 前端面試必會 | 一文讀懂現代 JavaScript 中的變量提升 - let、const 和 var
  • 前端面試必會 | 一文讀懂 JavaScript 中的閉包
  • 前端面試必會 | 一文讀懂 JavaScript 中的作用域和作用域鏈
  • 前端面試必會 | 一文讀懂 JavaScript 中的執行上下文
  • IntersectionObserver 和懶加載
  • 初探瀏覽器渲染原理
  • CSS 盒模型、佈局和包含塊
  • 詳細解讀 CSS 選擇器優先級


分享到:


相關文章: