看过很多手写call、apply的文章总能让我从入门到放弃。 直到遇到冴羽大佬的系列文章,反复看了好多遍,真上头,感觉自己又行了。本文是对[冴羽]大佬这一篇文章中学习到的call原理的总结以及在《前端开发核心知识进阶》书中提到的apply和bind原理的总结。
call实现原理
call()函数传入this和若干个参数。可以改变函数调用的this值。并且向函数中传入若干个参数。
bar.call(obj); 执行这一句代码将bar的this手动指向为obj. 根据this指向原理,谁调用this就指向谁。所以bar.call(obj)相当于obj.bar();
谁调用,this就指向谁。
- 冴羽大佬文章中的思路真是妙呀。文章中多次巧妙的运用了this指向问题。
let obj = {
name: '123',
// bar: function () {
// console.log(this.name)
// }
}
function bar() {
console.log(this.name)
}
function call2(context) {
context.fn = this;//谁调用call2函数,this就指向谁
context.fn();//相当于正在执行bar函数,执行fn()相当于在执行bar(); 还是根据谁调用this指向谁。所以这里相当于bar()函数执行,其中this是context
delete context.fn
}
Function.prototype.call2 = call2;
bar.call2(obj);
输出: 123
还有几点要完善的
- call()函数允许传null,此时的this指向为window
- call()函数除了传入this对象之外,还允许传入若干个参数
- call()函数允许有返回值
解决传入值为null的问题
function call2(context) {
var context = context || window;//解决传入为空的问题
context.fn = this;//谁调用call2函数,this就指向谁
context.fn();//相当于正在执行bar函数,执行fn()相当于在执行bar(); 还是根据谁调用this指向谁。所以这里相当于bar()函数执行,其中this是context
delete context.fn
}
Function.prototype.call2 = call2;
解决传入有参数问题
function call2(context) {
var argsArr = Array.prototype.slice.apply(arguments);//伪数组转化为数组
var context = context || window;//解决传入为空的问题
context.fn = this;//谁调用call2函数,this就指向谁
var args = argsArr.slice(1);
context.fn(...args);//相当于正在执行bar函数,执行fn()相当于在执行bar(); 还是根据谁调用this指向谁。所以这里相当于bar()函数执行,其中this是context
delete context.fn
}
解决有返回值问题
function call2(context) {
var argsArr = Array.prototype.slice.apply(arguments);
var context = context || window;//解决传入为空的问题
context.fn = this;//谁调用call2函数,this就指向谁
var args = argsArr.slice(1);
var result = context.fn(...args);//相当于正在执行bar函数,执行fn()相当于在执行bar(); 还是根据谁调用this指向谁。所以这里相当于bar()函数执行,其中this是context
delete context.fn
return result;
}
Function.prototype.call2 = call2;
let res = bar.call2(obj, 1, 2, 3);
apply实现原理
和call的原理类似,只是传的参数有区别
let obj = {
name: '123'
}
function bar(...args) {
console.log(this.name, ...args, "aefffff")
return 1
}
function apply2(context, arr) {
var context = context || window;//解决传入为空的问题
context.fn = this;//谁调用call2函数,this就指向谁
var result
if (arr && arr.length) {
result = context.fn(...arr);
} else {
result = context.fn();
}
delete context.fn
return result;
}
Function.prototype.apply2 = apply2;
let res = bar.apply2(obj, [1, 2, 3]);
但是这里需要注意,如果apply上本来就存在fn函数。在使用apply函数时,原有的 属性值会被覆盖,之后会被删除,为了保证键的唯一性,可以使用ES6 的Symbol。
参考代码如下
function apply2(context, arr) {
var context = context || window;//解决传入为空的问题
context.fn = this;//谁调用call2函数,this就指向谁
var fn = Symbol();
var result
if (arr && arr.length) {
result = context[fn](...arr);
} else {
result = context[fn]();
}
delete context[fn]
return result;
}
Function.prototype.apply2 = apply2;
bind
bind()方法创建一个新的函数,在bind()被调用时,这个新函数的this被指定为bind()的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
bind方法与call,apply的区别
- bind()返回的是函数
- 调用bind返回的函数的时候可以向该函数传递参数(柯里化)
第一版 利用apply实现bind函数
function bind2(context) {
var currentThis = this;//谁调用bind1 this就指向谁, 此时this指向bar
var arrayArgs = Array.prototype.slice.call(arguments);
return function () {
return currentThis.apply(context, arrayArgs.slice(1));//闭包 利用apply再改变bar函数的指向
}
}
Function.prototype.bind2 = bind2;
function bar() {
return this.name;
}
let obj = {
name: 'xxxxxx'
}
let bindFn = bar.bind2(obj);
console.log(bindFn());//输出'xxxxxx'
第一版函数中借用闭包和apply函数实现了一个初级版bind()函数。但是别忘了bind()返回的函数是可以接收参数的。
第二版 让bind返回的函数可以接收参数
这里需要利用柯里化将上面向bindFn()的参数和调用bind()函数的参数做个拼接,如下面所示。
function bind2(context) {
var currentThis = this;//谁调用bind1 this就指向谁, 此时this指向bar
var arrayArgs = Array.prototype.slice.call(arguments);
return function () {
var bindFnArgs = Array.prototype.slice.call(arguments);
var args = arrayArgs.slice(1).concat(bindFnArgs);
return currentThis.apply(context, args);//闭包 利用apply再改变bar函数的指向
}
}
Function.prototype.bind2 = bind2;
function bar(...arg) {
return this.name;
}
let obj = {
name: 'xxxxxx'
}
let bindFn = bar.bind2(obj);
console.log(bindFn(11));//输出'xxxxxx'
但是试着用new bindFn(),将bind()返回的函数作为构造器使用,如下面代码所示;
let obj = {
name: "小花"
}
function bar() {
console.log(this.name, "this")//bar实例
}
let barFun = bar.bind(obj);
let barInstance = new barFun();
输出:此时输出undefined, 明明我传的obj对象进去,却找不到obj.name说明此时this指向已经发生了改变,其实此时的this已经指向barInstance实例对象了。
用第二版的bind2方法执行同样的代码,此时的this一样指向obj对象,说明第二版的bind函数还存在问题。
第三版 像"bind()"函数一样,解决new关键字调用问题。
先看看new 操作符调用构造函数的时候做了哪些事情
- 创建一个新的对象
- 将构造函数的this指向这个新的对象
- 为这个对象添加属性、方法。
- 最终返回一个新对象 伪代码如下
var obj = {};
obj._proto_=Foo.prototype
Foo.call(obj)
看看new 构造函数的实例到底指向谁
let obj = {
name: "小花"
}
function bar() {
console.log(this.name, "this")//bar实例
}
bar.prototype.name = 'bar上的小花'
let barFun = bar.bind(obj);
let barInstance = new barFun();
console.log(barInstance)
好家伙,barInstance的constructor竟然是bar函数,那如果是new 调用的情况将bind.prototype==bar.prototype不就可以了
Function.prototype.bind2 = function (context) {
var me = this;
var args = Array.prototype.slice.call(arguments, 1);
var bound = function () {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return me.apply(this instanceof me ? this : context || this, finalArgs);//
}
bound.prototype = this.prototype; // // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
return bound;
}
然后用这一版的函数执行上面相同的代码,和上面输出的结果一样
我们在看看这样改变bind2原型实例上的name属性值
let obj = {
name: "小花"
}
function bar() {
console.log(this.name, "this")//bar实例
}
bar.prototype.name = 'bar上的小花'
let barFun = bar.bind2(obj);
barFun.prototype.name = "我修改了bind原型上的name值";
console.log(barFun.prototype, 'baFunnnnnnnnnnnnnnn');
console.log(bar.prototype, 'bar')
输出结果如下:上面我们只修改了barFun.prototype.name,却发现bar.prototype上面的name也被修改了。
用一个空的F构造函数缓存this原型链,代码如下面所示
Function.prototype.bind2 = function (context) {
var me = this;
var args = Array.prototype.slice.call(arguments, 1);
var F = function () { }
F.prototype = this.prototype
var bound = function () {
console.log(this, "this")
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return me.apply(this instanceof me ? this : context || this, finalArgs);
}
bound.prototype = new F(); // // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
return bound;
}
输出如下:bar的原型链没有被污染
用Object.create同样可以解决上面的问题
Function.prototype.bind2 = function (context) {
var me = this;
var args = Array.prototype.slice.call(arguments, 1);
var bound = function () {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return me.apply(this instanceof me ? this : context || this, finalArgs);//
}
// bound.prototype = this.prototype; // // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
bound.prototype = Object.create(this.prototype);
return bound;
}
上面call,apply,bind函数实现得差不多了,感谢[冴羽]大佬的文章。