前端面试必备 | 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,带你解读前端技术,掌握最本质的技能。关注公众号能够拉你进讨论群,有任何问题都会回复。

公众号
相关文章
相关标签/搜索