手写call,apply,bind,new
手写call,apply,bind,new
引言
- 时隔一年,再次手写这些枯燥而又无聊的api。让我有了新领悟。为什么要将这些api放在一起呢? 因为他们都和
this
有着密不可分的关系。众所周知,this实际上是在函数被调用时发生的绑定,他的指向完全取决于函数在哪里被调用(被调用的时执行上下文)
。关于this的详细介绍,请看这篇文章 - 那这就出现一个问题,如果
不想遵循this的绑定规则
,要怎么做呢?可以利用call,apply,bind。来强行绑定this
。 - this也和new 有很大的关联。但是我想从
构造函数和工厂函数
做对比,来引出new中的this
。
call的特点
-
可以改变我们当前函数this指向
-
还会让当前函数执行
-
接受的是一个参数列表
手写call
Function.prototype._call = function (ctx, ...args) {
let fn = Symbol();
let context = ctx ? Object(ctx) : window;
context.fn = this;
let res = args.length ? context.fn(...args) : context.fn(); // 判断是否传参
delete context.fn;
return res;
};
let obj2 = {
a: 2,
};
let obj1 = {
a: 1,
getName: function (b, c) {
console.log(this.a);
console.log(b);
console.log(c);
return this.a + b + c;
},
};
console.log(obj1.getName._call(obj2, 3, 4)); // 9
思路讲解
- 根据call的特点,就是改变this指向,并且让当前函数执行。
- 根据传入的上下文(如果传入的是基本数据类型,并且是真值,就用对象包装一下,否则,传入的就是默认执行window),让其拥有属性,让这个属性去执行。
ctx ? Object(ctx) : window
- 改变this指向: 将当前的this赋值给传入的上下文
context.fn = this
; - 让当前函数执行(需要判断是否传参)
args.length ? context.fn(...args) : context.fn()
- 将当前函数执行的结果返回
return res
- 删除我们构造的假执行的函数:
delete context.fn
apply的特点
- 可以改变我们当前函数this指向
- 还会让当前函数执行
- 接受的是一个数组(或一个类数组对象)
那手写apply,只要判断传入参数,是否是数组
即可。其余跟call实现一样
手写apply
Function.prototype._apply = function (ctx, args = []) {
if (!Array.isArray(args)) {
throw new Error('apply need Array');
}
let fn = Symbol();
let context = ctx ? Object(ctx) : window;
context.fn = this;
let res = args.length ? context.fn(...args) : context.fn();
delete context.fn;
return res;
};
let obj2 = {
a: 2,
};
let obj1 = {
a: 1,
getName: function (b, c) {
console.log(this.a);
console.log(b);
console.log(c);
return this.a + b + c;
},
};
console.log(obj1.getName._apply(obj2, [3, 4])); // 9
思路讲解
- 和call基本一直,就是要注意apply接受的是一个数组(或一个类数组对象)
- 防止无参数,给一个默认参数类型是数组,
args = []
- 如果是非数组,则抛出错误
throw new Error
bind的特点
-
bind方法可以绑定this指向
-
bind方法返回一个绑定后的函数,(高阶函数)
-
如果绑定的函数被new了,当前函数的this,就是当前的实例
手写bind
Function.prototype._bind = function (ctx, ...bindArgs) {
let that = this;
return function () {
return that.apply(ctx, bindArgs);
};
};
let obj1 = {
age: '2',
};
let obj2 = {
age: '88',
getInfo: function (name) {
return `${name} 今年${this.age} 岁`;
},
};
let p = obj2.getInfo._bind(obj1, '小明');
console.log('p', p());
思路讲解
bind()
方法创建一个新的函数所以要return 一个 function
,等待调用时执行.里面返回了一个函数,就是高阶函数的用法- 为了获取原始函数的this,在内部使用了一个变量that来保存,用到了闭包。其实还可以用箭头函数
let that = this;
关于bind的其他考量
- 由于bind只是改变this指向,并不执行。这就给函数调用增加了一些逻辑判断。
- 如果调用者,又传入参数该怎么办?
- 如果函数用new来实例化,内部的this改怎么处理?
- 如果函数要在原型上追加属性,该怎么处理?
调用者传入参数处理
Function.prototype._bind = function (ctx, ...bindArgs) { // bind绑定着参数获取
let that = this;
return function (...args) { // 调用者传入参数获取
return that.apply(ctx, bindArgs.concat(args)); // 只要将二者进行拼接即可
};
};
let obj1 = {
age: '2',
};
let obj2 = {
age: '88',
getInfo: function (name) {
console.log('arg', arguments); // [Arguments] { '0': '小明', '1': '调用时传入参数' }
console.log('name', name); // name 小明。
return `${name} 今年${this.age} 岁`;
},
};
let p = obj2.getInfo._bind(obj1, '小明'); // 小明 今年2 岁
console.log('p', p('调用时传入参数'));
tips
- 如果只有一
个形参参数接收,但是传入了两个实参,此时会默认取第一个实参
。如果想改变实参获取,可以在concat
更换位置,或者在使用时,用过索引取形参
函数用new来实例化,并且在原型上进行操作
Function.prototype._bind = function (ctx, ...bindArgs) {
let that = this;
function temp() {} // Object.create 原理
function fBind(...args) {
return that.apply(
// 如果被new调用,this是fBind的实例
this instanceof fBind ? this : ctx,
args.concat(bindArgs)
);
}
// 维护fBind的原型
temp.prototype = this.prototype;
fBind.prototype = new temp();
return fBind;
};
let obj1 = {
age: '2',
};
let obj2 = {
age: '88',
getInfo: function (name) {
console.log('arg', arguments);
console.log('name', name);
return `${name} 今年${this.age} 岁`;
},
};
let p = obj2.getInfo._bind(obj1, '小明');
console.log('p', p); // [Function: fBind]
let pp = new p();
console.log('pp', pp); // getInfo {}
关于new
说起new,就不得不说说构造函数,详细链接如下
平常封装的函数,基本都可以说是工厂函数,比如我们对接口的封装
const res = await Net.upload('/Upload/upload', previewData);
我们并不会去关心Net.upload是怎么实现的
,只需要传入相应的参数('/Upload/upload', previewData)
,就可以做数据请求。这就是工厂函数。
再比如,我们要得到一个对象
function getObj(a, b) {
let obj = {};
obj.a = a;
obj.b = b;
return obj;
}
const obj = getObj('aa', 'bb');
console.log(obj); // { a: 'aa', b: 'bb' }
我们并不需要关心内部怎么实现,只需要传入响应的参数即可。但是这也会有一个问题,就是每次都要声明一个对象,对象赋值,并且,返回这个对象,比较繁琐。new
替我们做了这些事!!!
使用new 来做函数的构造调用
function getObj(a, b) {
this.a = a;
this.b = b;
}
const obj = new getObj('aa', 'bb');
console.log(obj);
new的特点
-
类比工厂函数,可以看出,在构造函数内部,其实每次也要新创建一个对象
-
并且会默认把当前的this,指向新创建对象的this
-
原型链会做连接
-
如果返回值是隐式返回,那么就返回新创建的对象,否则返回显示返回的对象
手写new
function _new() {
let Constructor = [].shift.call(arguments);
if (typeof Constructor !== 'function') {
throw new Error('The first argument of new must be a function');
}
let obj = {}; // 创建/ 构造一个对象
Object.setPrototypeOf(obj, Constructor.prototype);
let res = Constructor.apply(obj, arguments);
return res instanceof Object ? res : obj;
}
function Test(name, age) {
this.name = name;
this.age = age;
}
Test.prototype.sayName = function () {
console.log(this.name);
};
const t = _new(Test, 'wd', 7);
console.log(t); // Test { name: 'wd', age: 7 }
思路讲解
- 传入的必须是个函数,才可以做函数的构造调用
[].shift.call(arguments)
获取第一个参数,第一个参数就是传入的函数。此时arguments的参数就是剩余的所有参数Object.setPrototypeOf
创建的对象和传入的函数,做一个关联Constructor.apply(obj, arguments)
改变this指向,并且apply会调用传入的函数,此时默认传入的函数是没有返回值的
如果传入的函数,有返回值,
如果是返回对象,那么就要做检测。如果调用的函数有返回值,并且是对象,就要返回其所返回的对象。
return res instanceof Object ? res : obj;
否则就返回 创建的新对象 obj
return res instanceof Object ? res : obj;
结束
转载自:https://juejin.cn/post/7134679154926043172