前言
在面试时,要求模拟apply、call、new、Object.create、instanceof 等 JavaScript 基础API 实现方式便是家常便饭的事,笔者前两天就遇到这种好事,在此总结了它们的实现方式
call 实现
先看看在 JavaScript 中调用方式吧:
const foo = {};
function bar(){
console.log(this); // foo {}
}
bar.call(foo);
分析一下它的作用:
- 改变函数的
this指向 - 调用函数,并传递参数
经过分析,我们知道了 call 的作用,那么如何实现?
this 指向改变
通俗理解 this 的话,一般可以理解为:谁调用,this 指向谁。根据上面特性,我们可以简单写出以下代码:
Function.prototype.call = function(context){
context.fn = this;
context.fn();
delete context.fn;
}
当我们传入 context 时,我们往其上面添加一个属性,值为函数本身,在调用它,这样函数的 this 指向就是传入的 context 了,最后删除添加属性。
当前我们需要考虑一些边界场景
- 传入
context不是一个对象 - 添加属性名考量,防止覆盖对象同名属性
于是,我们继续优化代码
Function.prototype.call = function(context) {
context = toObject(context);
const symbol = Symbol();
context[symbol] = this;
const val = context[symbol]();
delete context[symbol];
return val;
}
// 处理null值和undefined值,以及基本类型
function toObject(val){
const type = typeof val;
let result = val;
switch(type){
case "number":
case "boolean":
case "string":
result = Object(val);
break;
default:
result = result || window;
}
return result;
}
在一切开始之前,我们需要将 context 转换为对象,防止阻断后续流程。在添加属性时,我们以 symbol 作为属性名,添加到 context 上,设置一个唯一属性,这样就不会覆盖同名属性了
参数传递
接下来我们讨论第二点:函数执行和参数传递。
call 在调用时,它会把原先函数的参数一个个传递过来。
const context = {}
function add(a, b, c){
console.log(a, b, c)
}
// 输出 1, 2, 3
add.call(context, 1, 2, 3)
于是,我们很容易想到可以使用 剩余参数 方式实现它
Function.prototype.call = function(context, ...rest) {
context = toObject(context);
const symbol = Symbol();
context[symbol] = this;
const val = context[symbol](...rest);
delete context[symbol];
return val;
}
apply 实现
apply 和 call 的最大区别在于:apply 传递参数是一个数组
const context = {}
function add (a, b, c){
console.log(a, b, c)
}
add.apply(context, [1, 2, 3])
我们可以完全扩展引用 call 的实现,并额外处理参数
Function.prototype.call = function(context, args = []) {
args = Array.from(args)
context = toObject(context);
const symbol = Symbol();
context[symbol] = this;
const val = context[symbol](...args);
delete context[symbol];
return val;
}
bind实现
bind 和 call 也有类似之处,bind 只改变函数 this 指向,但不会调用函数,并且会返回一个新函数,此外它还有点 柯里化 的特征。比如以下示例
const context = {}
function add (a, b, c){
console.log(a, b, c)
}
const newAdd = add.bind(context, 1)
newAdd(2, 3)
我们在实现它时,需要考虑以下几点:
- 改变函数
this指向 - 返回新函数,新函数和原函数基本相同
- 参数合并
改变函数 this 指向
我们完全可以使用上面成果来实现
Function.prototype.bind = function(context, ...args){
this.apply(context, args)
}
返回新函数,新函数和原函数相同
要使新函数和原函数一致,那么我们可以使用继承方式
Function.prototype.bind = function(context, ...args){
const self = this
function f(){
return self.apply(context, args);
}
f.prototype = Object.create(self.prototype);
return f;
}
但是需要注意一个特殊现象,再此之前,大家可以先猜猜这段代码执行输出什么?
const context = {
name: 'cola'
}
function Person () {
console.log(this.name)
}
Person.prototype.name = 'xiaoming'
const bindPerson = Person.bind(context)
new bindPerson() // xiaoming
bindPerson() // cola
如果按照我们上面实现,应该会打印两次 cola,但是只打印了一次,所以我们需要额外处理实例化场景,那么对于 this 指向需要稍微调整。
function f(){
return self.apply(this instanceof f ? this : context, args);
}
参数合并
bind 函数参数可以传递到最终函数中,那么我们需要进行参数合并
Function.prototype.bind = function(context, ...args){
const self = this;
function f(){
const _args = [...args, ...arguments];
return self.apply(this instanceof f ? this : context, _args);
}
f.prototype = Object.create(self.prototype);
return f;
}
new实现
在前面的 bind 实现中,我们稍微提到了实例化场景,那么这节我们了解下 new 到底干了什么?先举个例子
function Person(name, age){
this.name = name
this.age = age
}
Person.prototype.getName = function() {
return this.name
}
const p = new Person('xiaoming', 18)
console.log(p.name) // xiaoming
console.log(p.getName()) // xiaoming
通过上面例子,我们大概明白了 new 的功能
- 执行函数,返回一个新对象
- 改变
this指向,this 指向新对象 - 传递参数
- 实现继承
执行函数
执行一个函数是非常简单的,但需要配合改变 this 指向,我们可以使用上文的 apply 方式
function _new (constructor, ...args) {
constructor.apply(null, args);
}
实现继承
通过上面例子,我们知道 new 会返回一个新对象,新对象会继承传入构造函数的原型,并且新对象上面挂载了构造函数的属性,那么我们可以使用 Object.create 创建一个新对象,并把新对象作为构造函数的 this 指向
function _new(constructor, ...args) {
const obj = Object.create(constructor.prototype);
constructor.apply(obj, args);
return obj
}
但是需要注意一点,当 构造函数有返回值的时候,那么 new 返回的是构造函数执行结果
function Person(){
this.name = name
return {
name: 'xiaohong'
}
}
const p = new Person('xiaoming')
console.log(p.name)
所以我们需要修正一下:
function _new(constructor,...args){
const obj = Object.create(constructor.prototype);
const res = constructor.apply(obj, args);
return (typeof res === 'object' && res !== null) ? res : obj;
}
Object.create实现
Object.create 是用于创建一个新对象,并且存在继承关系,举个例子
function Parent(){}
Parent.prototype.hello = function (){
console.log('hello world')
}
function Child(){}
Child.prototype = Object.create(Parent.prototype)
const child = new Child()
console.log(child.hello())
通过示例,我们大概明白 Object.create 的功能:
- 创建新的对象
- 实现继承关系
创建新对象
创建对象我们可以使用函数式或者声明式
const obj_1 = Object()
const obj_2 = {}
实现继承关系
实现继承,我们只需要改变原型链即可
Object.create = function(prototype){
const obj = {}
Object.setPrototypeOf(obj, prototype)
return obj
}
当然,我们可以使用上文的 new 来实现
Object.create = function(prototype){
function f(){};
f.prototype = prototype;
return new f();
}
instanceof 实现
检查构造函数的 prototype 属性是否出现在 实例对象 的原型链上,那么我们只需要不断向上查询实例对象的原型链,同时和当前构造函数的 prototype 对比即可,相同则返回 true。
function instanceOf(instance, constructor) {
let proto = Object.getPrototypeOf(instance);
const prototype = constructor.prototype;
while(proto !== null){
if(proto === prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false
}
那么我们需不需要考虑循环引用的事情呢?其实是不需要的
function instanceOf(instance, constructor) {
let proto = Object.getPrototypeOf(instance);
const prototype = constructor.prototype;
while(proto !== null){
if(proto === prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false
}
function Parent(){
this.name = 'parent'
}
function Child(){
this.name = 'child'
}
function GrandChild(){
this.name = 'grandchild'
}
Child.prototype.__proto__ = Parent.prototype
Child.prototype.constructor = Child
GrandChild.prototype.__proto__ = Child.prototype
GrandChild.prototype.constructor = GrandChild
Parent.prototype.__proto__ = GrandChild.prototype
Parent.prototype.constructor = Parent
const child = new Child()
console.log(child)
console.log(child instanceof Parent)
console.log(instanceOf(child, Parent))
当我们将 Parent.prototype 指向 GrandChild.prototype时,其实构成了循环引用,如果存在循环引用,JavaScript 会抛出一个错误,所以无需担心这个问题