(一) call源码解析
例子:
function add(c, d){
return this.a + this.b + c + d
}
var obj = { a: 1, b: 2 }
console.log(add.call(obj, 3, 4))
网上介绍call
大致说法是,call
改变了this
的指向,本来上下文环境调用的this
是window
,而call
把this
指向了obj,brabra,不管你懂不懂,成功绕晕你就已经ok了,但是实际发生的过程,可以看成下面的样子。
var o = {
a: 1,
b: 2,
add: function(c, d) {
return this.a + this.b + c + d
}
};
- 调用之后,新创建一个对象o,obj上的属性变成o的属性
- 给o对象添加一个add属性,这个时候 this 就指向了 o,
- o.add(5,7)得到的结果和add.call(o, 5, 6)相同。
- 但是给对象o添加了一个额外的add属性,这个属性我们是不需要的,所以可以使用delete删除它。
so, 基本上就三步
// 1. 将函数设为对象的属性
o.fn = bar
// 2. 执行该函数
o.fn()
// 3. 删除该函数
delete o.fn
基于ES3实现 call
Function.prototype.es3Call = function (context) {
var content = context || window;
2 content.fn = this;
var args = [];
// arguments是类数组对象,遍历之前需要保存长度,过滤出第一个传参
for (var i = 1, len = arguments.length ; i < len; i++) {
// 避免object之类传入
args.push('arguments[' + i + ']');
}
var result = eval('content.fn('+args+')');
delete content.fn;
return result;
}
console.error(add.es3Call(obj, 3, 4)); // 10
console.log(add.es3Call({ a: 3, b: 9 }, 3, 4)); // 19
console.log(add.es3Call({ a: 3, b: 9 }, {xx: 1}, 4)); // 12[object Object]4
解析,第2步是关键,将实例的环境this传入,实际上就是函数调用,称其为实际执行函数,args是参数,实际上就是调用了这个函数,也就是传说中的改变this指向了
ES6实现call,差别不大,es6新增...
rest运算符,进行对有iterator属性进行取值操作
// ES6 call 实现
Function.prototype.es6Call = function (context) {
var context = context || window;
context.fn = this;
var args = [];
for (var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
const result = context.fn(...args);
return result;
}
console.error(add.es6Call(obj, 3, 4));
console.log(add.es3Call({ a: 3, b: 9 }, {xx: 1}, 4)); // 12[object Object]4
(二) apply源码解析
apply 和 call 区别在于 apply 第二个参数是Array
,而 call 是一个个传入
// 基于es3的实现
Function.prototype.es3Apply = function (context, arr) {
var context = context || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
} else { // 获取参数
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
// 执行函数
result = eval('context.fn(' + args + ')')
}
delete context.fn; return result
}
console.log(add.es3Apply(obj, [1, 'abc', '2'])); // 4abc
console.log(add.apply(obj, [1, 2])); // 6
基于es6的实现
Function.prototype.es6Apply = function(context, arr){
var context = context || window;
context.fn = this;
var result;
if(!arr) {
result = context.fn();
} else {
if(!(arr instanceof Array))
throw new Error('params must be array');
result = context.fn(...arr)
}
delete context.fn;
return result;
}
console.log(add.es6Apply(obj, [1, 2])); // 6
(三)bind 源码解析
bind()
方法回创建一个新函数。
当这个新函数被调用时,bind()
第一个参数将作为它运行时的this
,之后的一序列参数将会在传递的实参前传入作为它的参数
bind
方法实例
function foo(c, d){
this.b = 100;
console.log(this.a);
console.log(this.b);
console.log(this.c);
console.log(this.d);
}
// 将foo bind到{ a: 1 }
var func = foo.bind({ a: 1 }, '1st');
func('2nd');
// 1 100 1st 2nd
// 即使再次call也不能改变 this
func.call({ a: 2 } , '3rd');
// 1 100 1st 2nd
// 当 bind 返回的函数作为构造函数的时候
// bind 时指定的 this 会失效, 但传入的参数依然生效
// 所以使用func为构造函数时,this不会指向{ a: 1 },this.a 为undefined
new func('4th')
//undefined 100 1st 4th
用bind()
方法绑定了首参数的时候,就已经是改不了引用了,一直都用这个引用,除非当成是构造函数进行调用,this会改变指向,调用构造函数的话,this
会指向实例
首先用ES3来实现
Function.prototype.es3Bind = function (context) {
if(typeof this !== 'function')
throw new TypeError('what is trying to be bound is not callback');
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fBound = function() {
//获取函数的参数
var bindArgs = Array.prototype.slice.call(arguments);
// 返回函数的执行结果
// 判断函数是作为构造函数还是普通函数
// 构造函数this instanceof fNOP返回true,将绑定函数的this指向该实例,可以让实例获得来自绑定函数的值。
// 当作为普通函数时,this 指向 window 此时结果为 false,将绑定函数的 this 指向 context
return self.apply(this instanceof fNOP ? this: context, args.concat(bindArgs));
}
// 创建空函数
var fNOP = function() {};
// fNOP函数的prototype为绑定函数的prototype
fNOP.prototype = this.prototype;
// 返回函数的prototype等于fNOP函数的实例实现继承
fBound.prototype = new fNOP();
// 以上三句相当于Object.create(this.prototype)
return fBound;
}
var func = foo.es3Bind({a: 1}, '1st');
func('2nd'); // 1 100 1st 2nd
func.call({a: 2}, '3rd'); // 1 100 1st 3rd
new func('4th'); //undefined 100 1st 4th
es6实现
Function.prototype.es6Bind = function(context, ...rest) {
if (typeof this !== 'function')
throw new TypeError('invalid invoked!');
var self = this;
return function F(...args) {
if (this instanceof F) {
return new self(...rest, ...args)
} else {
return self.apply(context, rest.concat(args))
}
}
}
var func = foo.es3Bind({a: 1}, '1st');
func('2nd'); // 1 100 1st 2nd
func.call({a: 2}, '3rd'); // 1 100 1st 3rd new func('4th'); //undefined 100 1st 4th
面试问题:
function fn1(){
console.log(1);
}
function fn2(){
console.log(2);
}
fn1.call(fn2); //输出 1
fn1.call.call(fn2); //输出 2
fn1.call(fn2), 只是改变了fn1内部this
的指向,不是指向调用它的上下文环境也就是 window
,而是指向了fn2,但是不影响 fn1 代码的执行,输出 1
而fn1.call.call(fn2)
要注意,其实是分两段来解读
一 是 call(),后面加括号才代表是call
函数,不然就是对象,也就是说fn1.call
对象
二 fn1.call
会通过原型链找到最终对象,本质上是Function.prototype.call
,
对于fn1.call.call(fn2)
首先,调用call
函数时,也就是 fn1.call.call(fn2) 的加粗部分,先将 fn2 作为临时的 context 对象。然后将 fn1.call 这个函数对象作为 实际执行函数属性: context.fn = fn1.call
注意: fn1.call会通过原型链找到最终对象,其本质为 Function.prototype.call; 没有其他参数,直接执行 fn1.call() 函数,即 context.fn(); 此时函数的本质还是 Function.prototype.call 函数对象。 不过执行这个函数的环境是在 Function.prototype.call() 中, 只不过是第一次调用 call() 函数。 第一次调用 call() 函数将 this 关键字指向了 fn2,故而 在 fn1.call.call(fn2) ;加粗部分的 函数中执行的 call函数执行过程中的 this指向的是 fn2;传入的参数为空,故而 新的 call()函数对象 的this关键字 被替换为window; 而执行 this()时,就是执行 fn2();不涉及 this操作。故最终输出2