1.call的重写
call()方法使用一个指定的this值和单独给出的一个或多个参数来调用一个函数。call()允许为不同的对象分配和调用属于一个对象的函数/方法。call()提供新的this值给当前调用的函数/方法。你可以使用call来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)。
以上是MDN中文文档给出的定义,我们来逐句分析一下意思并提取一下重点。
- 使用一个指定的
this值?这个指定的this值就是我们手动选择的绑定对象,也就是传入call()方法的第一个参数。单独给出一个或多个参数来调用一个函数?这句话意思是,call()方法需要被一个函数来调用,并把除第一个外的剩余参数传入该函数。 call()允许为不同的对象分配和调用属于一个对象的函数/方法?这句话意思是当call方法被某个函数调用时,call可以指定任意的对象为该函数执行时的this指向。call()提供新的this值给当前调用的函数/方法?新的this值就是上面我们说到的传入call()方法的第一个参数,也就是我们指定的任意一个对象。
现在我们可以总结到:call方法接收的所有参数中第一个参数一定是我们指定的绑定对象,也就是指定的this值,后面剩余的参数是传入借用的函数的参数。接下来让我们用代码来解释说的啥意思。
function foo(a, b) {
console.log(this.name, a , b);
return a+b
}
const obj = {
name: 'tony'
}
console.log(foo.call(obj,1,2))
// tony 1 2
// 3
foo.call(obj, 1, 2)调用call方法首先传入想要绑定的对象(也就是this值)为obj,再将实参1,2传入foo()函数,这里的foo()就是我们借用的函数,而它的内部this指向就变成了我们指定的obj对象。可能看到这里,有的小伙伴还是有点晕,方便理解可以把context看作this值是完全没问题的。多的不说,让我们来重写call来加深理解。
重写之前我们必须要分析清楚该方法有什么功能,要注意什么细节。
call()方法必须被函数/方法来调用call()方法接收一个或多个参数,且第一个参数为我们指定的this值call()方法未传入参数时,指定的this值会绑定成全局对象(非严格模式下)call()方法借用的函数如果有返回值,则需要将结果返回,没有返回值则返回undefined
老版本实现call()方法
// 模拟symbol用法 添加在this值上的属性名不重复 实际上这里不能确保唯一性
function randomString() {
return Math.random().toString(16).substring(2,8) + new Date().getTime().toString(16)
}
// 方法以函数的方式调用,所以需要放在Function的原型对象上,让函数通过原型链引用
Function.prototype.myCall = function(context) {
// 判断myCall方法是否以函数或方法的方式被调用
if (typeof this !== 'function') {
throw new Error('not a function')
};
// 非严格模式下未传入this值则默认绑定到全局对象
context = context || window;
var symbolKey = randomString();
var args = [];
// 将借用函数设置成context的方法
context[symbolKey] = this;
// 收集参数
for (var i = 1; i < arguments.length; i++) {
args.push(arguments[i])
};
// 用字符串拼接展开参数 这里直接用eval来运行了比较方便因为里面接收就是字符串
var res = eval("context[symbolKey](" + args + ")");
// 添加了属性记得删除
delete context[symbolKey];
// 返回值
return res;
}
console.log(foo.myCall(obj,1,2))
// tony 1 2
// 3
上面代码可以粗略理解成
function myCall(context, ...args) {
context.fn = foo;
context.fn(...args);
delete context.fn
}
context为我们指定的this值,借用的函数foo赋值给绑定对象context新添加的fn属性。而这也是为什么能指定函数的this指向的本质,其实就是让该函数作为指定对象的方法执行罢了。
新版本实现call()方法
使用es6的Symbol()和扩展操作符...的语法更加简洁。
Function.prototype.myCall = function(context, ...args) {
if (typeof this !== 'function') {
throw new Error('not a function')
};
context = context || window;
const symbolKey = Symbol();
context[symbolKey] = this;
const res = context[symbolKey](...args);
delete context[symbolKey];
return res;
}
2. apply的重写
apply()方法调用一个具有给定this值的函数,以及以一个数组(或一个类数组对象)的形式提供的参数。- 虽然这个函数的语法与
call()几乎相同,但根本区别在于,call()接受一个参数列表,而apply()接受一个参数的单数组。
根据上面给出的定义,不难发现确实与call()基本一致,本质区别就是传入参数的方式不同。
老版本实现apply()方法
function randomString() {
return Math.random().toString(16).substring(4,6) + new Date().getTime().toString(16)
}
Function.prototype.myApply = function(context) {
if (typeof this !== 'function') {
throw new Error('not a function')
};
context = context || window;
var symbolKey = randomString();
var args = [];
context[symbolKey] = this;
// 这里需要把arguments的第二个元素拿出来 就是我们需要的参数数组
for (var i = 0; i < arguments[1].length; i++) {
// 拿到参数数组 循环存进args里
args.push(arguments[1][i])
};
var res = eval("context[symbolKey](" + args + ")");
delete context[symbolKey];
return res;
}
console.log(foo.apply(obj, [1, 2]));
// tony 1 2
// 3
console.log(foo.myApply(obj, [1, 2]));
// tony 1 2
// 3
新版本实现apply()方法
Function.prototype.myApply = function(context, args) {
if (typeof this !== 'function') {
throw new Error('not a function')
};
context = context || window;
const symbolKey = Symbol();
context[symbolKey] = this;
const res = context[symbolKey](...args);
delete context[symbolKey];
return res;
}
3. bind的重写
bind()方法创建一个新的函数,在bind()被调用时,这个新函数的this被指定为bind()的第一个参数,而其余参数将作为新函数的参数,供调用时使用。- 调用绑定函数时作为
this参数传递给目标函数的值。如果使用new运算符构造绑定函数,则忽略该值。
bind方法是比较复杂的一个,但是可以总结重写bind()方法需要注意两个问题:
- 一个是偏函数的特性(即可以部分传参,然后返回一个接收剩余参数的新函数)
- 一个是原型的问题(当用new调用返回的新函数)
bind方法比较复杂的点在于它需要返回一个新函数,然后我们正常(不以new操作符方式)调用返回的新函数时this值仍然指向我们的指定对象。
官方的bind方法
function foo(a, b) {
console.log(this.name, a , b);
return a+b
}
const obj = {
name: 'tony'
}
const bar = foo.bind(obj, 1)
console.log(bar(2));
// tony 1 2
// 3
const p = new bar(2)
console.log(p);
// undefined 1 2
// foo {}
基于apply实现bind方法的重写
// 可部分传参
Function.prototype.myBind = function(context) {
// 是否以函数方式调用myBind()方法
if (typeof this !== 'function') {
throw new Error('not a function')
}
// 是否传入绑定this值 即绑定对象
context = context || window;
// 存储借用函数
const _this = this;
// 收集第一次传入的参数
const args = Array.prototype.slice.call(arguments,1)
// 判断是否通过new来调用
const bound = function () {};
const res = function() {
return _this.apply( // 将第一次收集的参数和剩余参数合并
(this instanceof bound? this : context),args.concat(Array.prototype.slice.call(arguments)))
}
// 判断借用的函数是否有可枚举的prototype属性
if (this.prototype) {
// 把临时函数链接到this值的原型对象
bound.prototype = this.prototype;
}
// 将返回的新函数的原型对象作为临时函数的实例对象 即新函数通过原型继承
res.prototype = new bound();
return res
}
const bar1 = foo.myBind(obj, 1)
console.log(bar1(2));
// tony 1 2
// 3
const p = new bar1(2)
console.log(p);
// undefined 1 2
// foo {}
显然功能我们也成功实现了,现在我们可以回顾整段代码的逻辑。首先我们写的方法是可以被任意函数调用,所以我们需要把方法定义在Function构造函数的原型对象上,这样函数就能通过原型链查询到共享的myBind方法
myBind方法必须是通过函数或方法来调用,所以可以先做个判断。- 如果
myBind调用时未传入绑定对象,则默认绑定到全局对象上。 - 偏函数特性,所以在调用
myBind方法时可以传部分参数,剩余参数则可以传入返回的新函数。(需要实现可以两次传参的功能) - 如果通过
new操作符来调用返回的新函数则忽略之前的绑定对象(这里需要操作原型)。 - 操作原型时,需要把返回的新函数的原型对象链接到借用函数的原型对象,实现继承,此时需要判断该借用函数的prototype是否可以枚举。(上面这种方法不能枚举的话拿不到它的原型对象)
call和apply重写时会删除新添加的属性,但是重写的bind删除不了,返回的新函数引用着外部的借用函数。- 如果借用的函数有返回值也需要把结果返回。
基于call实现bind方法的重写
Function.prototype.myBind = function(context, ...args1) {
if (typeof this !== 'function') {
throw new Error('not a function')
}
context = context || window;
const _this = this;
const bound = function () {};
const res = function(...args2) {
return _this.call(
(this instanceof bound? this : context), ...args1, ...args2)
}
if (this.prototype) {
bound.prototype = this.prototype;
}
res.prototype = new bound();
return res
}
原型继承问题
- 可以通过
Object.create解决原型继承,就不需要借用临时函数的方式来写
Function.prototype.myBind = function(context, ...args1) {
if (typeof this !== 'function') {
throw new Error('not a function')
}
context = context || window;
const args = [].slice.call(arguments,1)
const _this = this
const res = function(...args2) {
return _this.call(
(this.isPrototypeOf(_this) ? this : context),...args1, ...args2);
}
if (this.prototype) {
res.prototype = Object.create(_this.prototype)
}
return res
}
- 为什么需要判断
this.prototype是因为有的函数的prototype属性不可枚举所以直接使用instanceof和Object.create()有时候会出错,因为这时候拿到的prototype值为undefined
重写bind方法的改进版本(解决了拿不到prototype的问题)
Function.prototype.myBind = function (context, ...args1) {
if (typeof this !== 'function') {
throw new Error('not a function')
}
context = context || window;
const args = [].slice.call(arguments, 1)
const _this = this
const res = function (...args2) {
return _this.call(
(this.isPrototypeOf(_this) ? this : context), ...args1, ...args2);
}
res.prototype = Object.create(Object.getPrototypeOf(_this));
return res;
}
实例测试
如果你看到这里,说明你已经对上面各种绑定方式有大概的理解了。接下来通过一波骚操作展示一下这些绑定的神奇用法。
官方的call,apply方法
Function.prototype.foo = function() {
const fn = this;
return function() {
const _this = [].shift.call(arguments)
return fn.apply(_this, arguments)
}
}
const obj = {};
push = [].push.foo();
console.log(push(obj,'one', 'two')); // 2
console.log(obj); // { '0': 'one', '1': 'two', length: 2 }
我们重写的myCall,myApply方法也实现了
Function.prototype.foo = function() {
const fn = this;
return function() {
const _this = [].shift.myCall(arguments)
return fn.myApply(_this, arguments)
}
}
const obj = {};
push = [].push.foo();
console.log(push(obj,'one', 'two')); // 2
console.log(obj); // { '0': 'one', '1': 'two', length: 2 }
如果通过bind方法则更简单
Function.prototype.foo = function (context) {
const res = this.bind(context);
return res;
}
const obj = {};
push = [].push.foo(obj);
console.log(push('1', '2'));
console.log(obj);
console.log(push('3', '4'));
console.log(obj);
console.log(push('5', '6'));
console.log(obj);
我们重写的myBind也完美实现了。
Function.prototype.foo = function (context) {
const res = this.myBind(context);
return res;
}
const obj = {};
push = [].push.foo(obj);
console.log(push('1', '2'));
console.log(obj);
console.log(push('3', '4'));
console.log(obj);
console.log(push('5', '6'));
console.log(obj);
结语
call,apply,bind三种绑定方式使用都很频繁,使用场景也很多,所以想掌握清楚还需要多多实战,多写多用。码字不易,喜欢的话不妨点个赞。