JS 高手必会:手写 new 与 instanceof
手写 instanceof
首先我们需要了解 instanceof是啥?
在其他面向对象编程语言中instanceof大多为实例判断运算符,即检查对象是否是某个构造函数的实例。
但是在 JS 中,instanceof为原型关系判断运算符,用于检测构造函数的prototype属性是否出现在某个对象的原型链上。
// object instanceof Constructor
A instanceof B // A 的原型链上是否有 B 的原型
而在大型项目、多人协作的情况下,在搞不懂对象上有哪些属性和方法,通过instanceof来查找继承关系
原型链关系:
-
__proto__: 指向原型对象(上一位的 .prototype),用于属性查找 -
constructor: 指向构造函数本身,用于标识对象的创建者 -
prototype: 函数的属性,指向原型对象(内置构造函数的 prototype 上通常有默认方法)
子.__proto__ === 父.prototype
父.prototype.constructor === 父
子.__proto__.constructor === 父(通过原型链访问)
举个最简单的例子:
const arr = [];
console.log(
arr.__proto__, // Array.prototype
arr.__proto__.__proto__, // Object.prototype
arr.__proto__.__proto__.__proto__ // null
)
那么arr的原型链关系就是:arr -> Array.prototype -> Object.prototype -> null
而我们要手写instanceof,也就是只需要沿着原型链去查找,那么用简单的循环即可。
手写代码如下
// right 是否出现在 left 的原型链上
function isInstanceOf(left, right) {
let proto = left.__proto__;
// 循环查找原型链
while (proto) {
if (proto === right.prototype) {
return true;
}
proto = proto.__proto__; // null 结束循环
}
return false;
}
手写 new
new对我们来说并不陌生,也就是实例运算符
在之前的文章我也提到过new的伪代码,让我们再来复习一下:
// 从空对象开始
let obj = {}
// this -> 创建空对象,运行构造函数
Object.call(obj)
// 将空对象的__proto__ 指向 构造函数的原型对象
obj.__proto__ = Object.prototype
// 返回新对象
return obj
写法一:(es6 新写法)
假如我们要new一个实例对象,但是不知道构造函数上的参数数量,而在es6中有一个新的运算符,也就是...运算符,它的诸多功能就可以满足我们的需求。
...扩展运算符
...有双重身份,在不同情况下的作用也不同
-
函数调用 / 数组 / 对象字面量中:
...称为扩展运算符,将可迭代对象“展开”为独立元素 -
函数参数 / 解构赋值中:
...称为剩余参数/剩余元素,将多个值“收集”为一个数组
// 展开数组
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5]; // [1, 2, 3, 4, 5]
// 收集数组
const [first, ...rest] = [1, 2, 3, 4];
console.log(first); // 1
console.log(rest); // [2, 3, 4]
而依据我们的伪代码即可模拟 new的功能
手写代码如下
function objectFactory(Construstor, ...args) {
// 创建空对象
var obj = new Object();
// 绑定 this 并执行构造函数
Construstor.apply(obj, args); // 不能使用call,因为apply调用数组
// 设置原型链
obj.__proto__ = Construstor.prototype;
return obj;
}
// 使用:完全不需要知道 Person 需要几个参数
function Person(name, age, city) {
this.name = name;
this.age = age;
this.city = city;
}
const p = objectFactory(Person, 'Alice', 25, 'Beijing'); // 自动适配
写法二:(根据arguments es5)
当然,在es6之前我们并没有...运算符,那么如何手写new呢?这就不得不提到arguments了。
arguments 是什么?
arguments 是 JS 中的一个类数组对象,它在所有非箭头函数内部自动可用,用于访问传递给该函数的所有实参。
类数组对象:
拥有
length属性和若干索引属性,但不具备数组原型方法(如.push(),.map(),.forEach()等)的对象,所以其不是真正的数组在普通函数中自动绑定。
箭头函数内部没有自己的
arguments,但是会沿作用域链查找外层函数的arguments,如果外层有就用外层的。
不妨来看个例子理解一下:
function greet() {
console.log(arguments); // 类数组对象
console.log(arguments.length); // 实际传入参数个数
console.log(arguments[0]); // 第一个参数
}
greet('Alice', 'Bob'); // 输出: { '0': 'Alice', '1': 'Bob' }, length: 2, 'Alice'
如何将 arguments 转为真数组?
因为 arguments 不是数组,不能直接用数组方法。但是可以将其转换为数组:
方法 1:扩展运算符(ES6+,最简洁)
function fn() {
const args = [...arguments];
args.map(x => x * 2); // 可用数组方法
}
方法 2:Array.from()
const args = Array.from(arguments);
方法 3:[].slice.call()
const args = Array.prototype.slice.call(arguments);
// 或简写(更推荐)
const args = [].slice.call(arguments);
slice是数组原型上的一个方法,用于返回一个从原数组或类数组中浅拷贝出来的新数组(实现将类数组转换成数组)
[].slice是因为arguments上没有这个方法,所以需要去空数组中“借用”,并且通过
.call() 将slice的this指向arguments,变相的让arguments可以使用这个方法。
在了解了arguments后,聪明的你已经想到了如何通过它来实现手写new了
手写代码如下
function objectFactory() {
// 创建空对象
var obj = new Object();
// 将 arugments 的第一项提出来(也就是 构造函数)
var Construstor = [].shift.call(arguments);
// 绑定 this 并执行构造函数
Construstor.apply(obj, arguments); // 不能使用call,因为 apply调用数组
// 设置原型链
obj.__proto__ = Construstor.prototype;
return obj;
}
function Person(name, age, city) {
this.name = name;
this.age = age;
this.city = city;
}
const p = objectFactory(Person, 'Alice', 25, 'Beijing'); // 自动适配