手撕内置的call、bind、apply

69 阅读3分钟

实现call

// 前置知识
//   @1 call 方法第一个参数函数的目标 this,其余参数用逗号分隔
//   @2 call 方法会直接执行函数
function fn(x, y) {
	console.log(this);
	return x + y;
}

var obj = {
	name: 'obj'
}

fn.call(obj, 1, 2); 
fn.call(null, 1, 2);
fn.call(undefined, 1, 2);
fn.call(false, 1, 2);

重写 call

// 注意点
//   @1 软绑:目标 this 传 null 或 undefined,则函数内部 this -> window (非严格模式)
//   @2 原始值增加属性不会报错,但访问不到 比如 var n = 1; n.x = 'w'; n.x // undefined
//   @2 函数返回值要带出去
//   @3 小技巧:记录当前 函数 到目标 this 上,这样调用的话,this 就为目标 this 了
//   @4 注意 key 用 Symbol,这样的话不会被覆盖掉
//   @5 用完之后要把目标 this 复原,注意此时打印 this 还是包含 Symbel(): f(){}
//	  不过浏览器有个特点,展开的时候看到的永远都是新的堆内存,所以展开后我们就能复
//      原后的目标 this
Function.prototype._call = function(totalThis, ...params) {
	totalThis == null && (totalThis = window);

	// 如果是原始值 要变成对象类型的值
	if (typeof totalThis !== 'object' && typeof totalThis !== 'function') {
		totalThis = Object(totalThis);
	}
	
	let key = Symbol();

	totalThis[key] = this; // 把函数暂时挂载目标 this 对象上

	let res = totalThis[key](...params); 

	delete totalThis[key]; 
	return res;
}

实现apply

// 前置知识
//   @1 apply 方法第一个参数函数的目标 this,其余参数用数组包装
//   @2 apply 方法会直接执行函数
function fn(x, y) {
	console.log(this);
	return x + y;
}

var obj = {
	name: 'obj'
}

fn.apply(obj, [1, 2]); 
fn.apply(null, [1, 2]);
fn.apply(undefined, [1, 2]);
fn.apply(false, [1, 2]);

重写 apply 其实 apply 和 call 的区别仅仅在于除了第一个参数外,剩余参数是逗号分隔还是数组。

// 改下参数接收的处理即可,...params -> params 
Function.prototype._apply = function(totalThis, params) {
	totalThis == null && (totalThis = window);

	if (typeof totalThis !== 'object' && typeof totalThis !== 'function') {
		totalThis = Object(totalThis);
	}
	
	let key = Symbol();

	totalThis[key] = this;

	let res = totalThis[key](...params); 

	delete totalThis[key]; 
	return res;
}

实现bind

// 前置知识
//   @1 bind 方法第一个参数函数的目标 this,其余参数用逗号分隔
//   @2 bind 方法不会直接执行函数
function func(x, y, event) {
	console.log(this, x, y, event);
	return x + y;
}

var obj = {
	name: 'obj'
}

// this->body  x->事件对象  y->undefined  event->undefined
document.body.onclick = func;

// 比如我有个需求,点击 body 的时候,把 func 执行,
// 但是方法中的 this 想改成 obj,并且传递 1 和 10
document.body.onclick = func.bind(obj, 1, 2); // bind 返回的函数会再接收一个 event


// 如果不用 bind 呢,怎么实现,我们只能再包一层
document.body.onclick = function(event) {
	func.call(obj, 1, 2, event); // 合并参数
}

重写 bind

Function.prototype._bind = function(totalThis, ...params) {
	// this->func  totalThis->obj  params=[1, 2]
	let self = this; // 函数本身

	return function(...args) {
		// this->body args->event 内层 this 不保险哦
		self.call(totalThis, ...params, ...args);
	}
}

其实可以看到,内层函数使用了外层函数保存的 this,这就又回到了我们函数式编程里的柯里化思想。

小试牛刀

了解了它们实现原理后,我们来看几道关于 call、bind、apply 的面试题。

题1

function fn1() {
	console.log(1);
}

function fn2() {
	console.log(2);
}

fn1.call(fn2); // 1

// 点操作符优先级 20,从左往右执行,fn1.call 拿到的是一个函数(fn1.__proto__.call)
// 也就是 Function.prototype.call.call(fn2);
// 类似于 Array.prototype.slice.call([1,2,3]);
// 把 fn2 作为 this,实际执行的是 fn2.call();
fn1.call.call(fn2); // 2 

// 以下代码不输出
Function.prototype.call(fn1);
Function.prototype.call(fn2);

题2

var name = '杨帅';

function A(x, y) {
	var res = x + y;
	console.log(res, this.name);
}

function B(x, y) {
	var res = x + y;
	console.log(res, this.name);
}

B.call(A, 40, 30); // 70  'A'

// 无论有多少 call,访问的都是 Function.prototype.call
// 实际执行的是 A.call(20, 20);
// this -> 20, x -> 20, y -> undefined
// x + y => NAN
// this -> Number(20) -> this.name -> undefined
B.call.call.call(A, 20, 20);  // NAN undefined

Function.prototype.call(A, 60, 50); // 无输出

// A.call(80, 70)
Function.prototype.call.call.call(A, 80, 70); // NAN undefined

你学废了么,手动狗头保命