这篇文章主要讲解原型的查找、变更、判断和删除,附带着对原型的作用方式做一下回顾。
instanceof
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
即通过下面的操作来判断:
object.__proto__ === Constructor.prototype ?
object.__proto__.__proto__ === Constructor.prototype ?
object.__proto__.__proto__....__proto__ === Constructor.prototype
当左边的值是 null 时,会停止查找,返回 false。
实际是检测 Constructor.prototype 是否存在于参数 object 的原型链上。
用法:
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 可能会由于人为的改动,导致在改动之前实例化的对象在改动之后的判断返回 false。 C.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 模拟实现
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
Object 和 Function 都属于函数对象,所以它们都是 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.create 的 propertyObject 参数
如果不指定对应的属性描述符,则默认都是 false。描述符有以下几个:
enumerable可枚举,默认falseconfigurable可删除,默认falsewritable可赋值,默认falsevalue属性的值
看下面的例子:
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]],所以它会调用相关 getter 和 setter。如果合并源包含 getter,这可能使其不适合将新属性合并到原型中。
String 类型和 Symbol 类型的属性都会被拷贝。
当拷贝的中途出错时,已经拷贝的值无法 rollback,也就是说可能存在只拷贝部分值的情况。
Object.assign 不会在那些 source 对象值为 null 或 undefined 的时候抛出错误。
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(...) 执行时,会发生以下事情:
- 一个继承自
Foo.prototype的新对象被创建; - 使用指定的参数调用构造函数
Foo,并将this绑定到新创建的对象。new Foo等同于new Foo(),也就是没有指定参数列表,Foo不带任何参数调用的情况。 - 由构造函数返回的对象就是
new表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。(一般情况下,构造函数不返回值,但是用户可以选择主动返回对象,来覆盖正常的对象创建步骤)
上面的第三步,返回 null 时,虽然 typeof 是 object,但是仍然会返回步骤一中创建的对象。
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
}
参考
- developer.mozilla.org/zh-CN/docs/…
- developer.mozilla.org/zh-CN/docs/…
- developer.mozilla.org/zh-CN/docs/…
- developer.mozilla.org/zh-CN/docs/…
- developer.mozilla.org/zh-TW/docs/…
- developer.mozilla.org/zh-TW/docs/…
- developer.mozilla.org/zh-CN/docs/…
最后
往期精彩:
- 前端面试必备 | 古怪的原型(鸡生蛋还是蛋生鸡)(原型篇:中)
- 前端面试必备 | 使用原型和构造函数创建对象(原型篇:上)
- 前端面试必会 | 一文读懂 JavaScript 中的 this 关键字
- 前端面试必会 | 一文读懂现代 JavaScript 中的变量提升 - let、const 和 var
- 前端面试必会 | 一文读懂 JavaScript 中的闭包
- 前端面试必会 | 一文读懂 JavaScript 中的作用域和作用域链
- 前端面试必会 | 一文读懂 JavaScript 中的执行上下文
- IntersectionObserver 和懒加载
- 初探浏览器渲染原理
- CSS 盒模型、布局和包含块
- 详细解读 CSS 选择器优先级
关注公众号可以看更多哦。
感谢阅读,欢迎关注我的公众号 云影 sky,带你解读前端技术,掌握最本质的技能。关注公众号可以拉你进讨论群,有任何问题都会回复。