一 目录
不折腾的前端,和咸鱼有什么区别
二 前言
返回目录
面试官:手写一个 call/apply/bind
。
工欲善其事,必先利其器,我们先了解一下这 3 者有什么区别:
call
的使用
function Product (name, price) {
this.name = name;
this.price = price;
}
function Food (name, price) {
Product.call(this, name, price);
this.category = 'food';
}
const food = new Food('cheese', 5);
console.log(food.name); // 'cheese'
call
:可以改变函数指向,第一个参数是要改变指向的对象,之后的参数形式是arg1, arg2...
的形式apply
:基本同call
,不同点在于第二个参数是一个数组[arg1, arg2...]
bind
:改变this
作用域会返回一个新的函数,这个函数不会马上执行
三 最终实现
返回目录
下面我们列一下今天的实现目标:
- 手写
call
- 手写
apply
- 手写
bind
3.1 手写 call
返回目录
Function.prototype.myCall = function(context = globalThis) {
// 设置 fn 为调用 myCall 的方法
context.fn = this;
// 获取剩余参数
const otherArg = Array.from(arguments).slice(1);
// 调用这个方法,将剩余参数传递进去
context.fn(otherArg);
// 将这个方法的执行结果传给 result
let result = context.fn();
// 删除这个变量
delete context.fn;
// 返回 result 结果
return result;
};
this.a = 1;
const fn = function() {
this.a = 2;
console.log(this.a);
}
fn.myCall(fn);
3.2 手写 apply
返回目录
Function.prototype.myApply = function(context = globalThis, arr) {
// 设置 fn 为调用 myCall 的方法
context.fn = this;
let result;
// 如果存在参数,则传递进去
// 将结果返回给 result
if (arr) {
result = context.fn(arr);
} else { // 否则不传
result = context.fn();
}
// 删除这个变量
delete context.fn;
// 返回 result 结果
return result;
};
this.a = 1;
const fn = function() {
this.a = 2;
console.log(this.a);
}
fn.myApply(fn);
3.3 手写 bind
返回目录
Function.prototype.myBind = function(context = globalThis) {
// 设置 fn 为调用 myCall 的方法
const fn = this;
// 获取该方法剩余参数
const otherArg = [...arguments].slice(1);
// 设置返回的一个新方法
const result = function() {
// 获取返回方法体的参数
const resultArg = [...arguments];
// 如果是通过 new 调用的,绑定 this 为实例对象
if (this instanceof result) {
fn.apply(this, otherArg.concat(resultArg));
} else { // 否则普通函数形式绑定 context
fn.apply(context, otherArg.concat(resultArg));
}
}
// 绑定原型链
result.prototype = Object.create(fn.prototype);
// 返回结果
return result;
};
this.a = 1;
const fn = function() {
this.a = 2;
console.log(this.a);
}
fn.myBind(fn);
fn();
OK,懂了么,我们发车继续深造~
四 Arguments 对象
返回目录
arguments
是一个对应于传递给函数的参数的类数组对象。
function fun(a, b, c) {
console.log(arguments[0]); // 1
console.log(arguments[1]); // 2
console.log(arguments[2]); // 3
}
arguments
对象不是一个 Array
。
它类似于 Array
,但除了 length
属性和索引元素之外没有任何 Array
属性。
将 arguments
转为数组:
// ES5
var arg1 = Array.prototype.slice.call(arguments);
var arg2 = [].slice.call(arguments);
// ES6
var arg3 = Array.from(arguments);
var arg4 = [...arguments];
在手写 call/bind/apply
过程中,会用到 arguments
来获取方法体的传参,就好比手写 call
过程中,通常我们通过 Array.from(arguments).slice(1)
来获取第二个及后面的参数。
五 call
返回目录
5.1 原生 call
返回目录
call()
方法使用一个指定的 this
值和单独给出的一个或多个参数来调用一个函数。
注意:该方法的语法和作用与apply()
方法类似,只有一个区别,就是call()
方法接受的是一个参数列表,而apply()
方法接受的是一个包含多个参数的数组。
语法:
function.call(thisArg, arg1, arg2, ...)
thisArg
:可选的。在function
函数运行时使用的this
值。arg1, arg2, ...
:指定的参数列表
function Product (name, price) {
this.name = name;
this.price = price;
}
function Food (name, price) {
Product.call(this, name, price);
this.category = 'food';
}
const food = new Food('cheese', 5);
console.log(food.name); // 'cheese'
5.2 手写 call
返回目录
首先我们得搞明白 call
的特性:
- 如果
obj.call(null)
,那么this
应该指向window
- 如果
obj1.call(obj2)
,那么谁调用它,this
指向谁(这里就是obj2
了) call
可以传入多个参数,所以可以利用arguments
这个字段来获取所有参数。将arguments
转换数组后,获取除第一个参数外的其他参数- 设置一个变量,用完可以删掉它
综上:
手写 call 的 JS 代码:
Function.prototype.myCall = function(context = globalThis) {
// 设置 fn 为调用 myCall 的方法
context.fn = this;
// 获取剩余参数
const otherArg = Array.from(arguments).slice(1);
// 调用这个方法,将剩余参数传递进去
context.fn(otherArg);
// 将这个方法的执行结果传给 result
let result = context.fn();
// 删除这个变量
delete context.fn;
// 返回 result 结果
return result;
};
this.a = 1;
const fn = function() {
this.a = 2;
console.log(this.a);
}
fn.myCall(fn);
小伙伴稍稍理解下,搞清楚它内部的流程,然后咱们实践一下:
防抖函数绑定手写 call
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>手写 call</title>
</head>
<body>
<button class="btn">123</button>
<script>
(function() {
Function.prototype.myCall = function(context) {
const newContext = context || window;
newContext.fn = this;
const otherArg = Array.from(arguments).slice(1);
newContext.fn(otherArg);
const result = newContext.fn(otherArg);
delete newContext;
return result;
};
const debounce = function(fn) {
let timer = null;
return function() {
clearTimeout(timer);
timer = setTimeout(() => {
fn.myCall(this, arguments);
}, 1000);
}
}
let time = 0;
const getNumber = function() {
console.log(++time);
}
const btn = document.querySelector('.btn');
btn.addEventListener('click', debounce(getNumber));
})()
</script>
</body>
</html>
这样我们就摸清了手写 call
。
六 apply
返回目录
6.1 原生 apply
返回目录
apply()
方法调用一个具有给定 this
值的函数,以及以一个数组(或类数组对象)的形式提供的参数。
语法:
function.apply(thisArg, [argsArray])
thisArg
:必选的。在function
函数运行时使用的this
值[argsArray]
:可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给func
函数。
const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers);
console.log(max); // 7
const min = Math.min.apply(null, numbers);
console.log(min); // 2
6.2 手写 apply
返回目录
下面我们开始尝试手写 apply
,记住这个方法和 call
类似,理解起来也不难:
Function.prototype.myApply = function(context = globalThis, arr) {
// 设置 fn 为调用 myCall 的方法
context.fn = this;
let result;
// 如果存在参数,则传递进去
// 将结果返回给 result
if (arr) {
result = context.fn(arr);
} else { // 否则不传
result = context.fn();
}
// 删除这个变量
delete fcontext.fnn;
// 返回 result 结果
return result;
};
this.a = 1;
const fn = function() {
this.a = 2;
console.log(this.a);
}
fn.myApply(fn);
用自定义 apply
+ 防抖实践一下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>DOM 操作</title>
</head>
<body>
<button class="btn">123</button>
<script>
(function() {
Function.prototype.myApply = function(context, arr) {
const newContext = context || window;
newContext.fn = this;
console.log(newContext.fn)
if (!arr) {
result = newContext.fn();
} else {
result = newContext.fn(arr);
}
delete newContext;
return result;
};
const debounce = function(fn, number) {
let timer = null;
return function() {
clearTimeout(timer);
timer = setTimeout(() => {
fn.myApply(this, number);
}, 1000);
}
}
const getNumber = function(time) {
console.log(time);
}
let number = [1, 2, 3, 4, 5];
const btn = document.querySelector('.btn');
btn.addEventListener('click', debounce(getNumber, number));
})()
</script>
</body>
</html>
这样我们就摸清了手写 apply
。
七 bind
返回目录
7.1 原生 bind
返回目录
bind()
方法创建一个新的函数,在 bind()
被调用时,这个新函数的 this
被指定为 bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
语法:
function.bind(thisArg, arg1, arg2, ...)
thisArg
:调用绑定函数时作为this
参数传递给目标函数的值。arg1, arg2, ...
:当目标函数被调用时,被预置入绑定函数的参数列表中的参数。
- 返回值:一个原函数的拷贝,并拥有指定的
this
值和初始参数
const module = {
x: 42,
getX: function() {
return this.x;
},
};
const unboundGetX = module.getX;
console.log(unboundGetX()); // undefined
// 谁调用指向谁,这里 unboundGetX = module.getX
// 让 getX 里面的 this 指向了 window
// 而 window 里面并没有 x 方法
// 当然,在前面加上 window.x = 43 就有了
const boundGetX = unboundGetX.bind(module);
console.log(boundGetX()); // 42
// 通过 bind,将 this 指向 module
7.2 手写 bind
返回目录
手写 bind
稍微有点小复杂,但是小伙伴们别慌,多读几遍可以摸清套路:
Function.prototype.myBind = function(context = globalThis) {
// 设置 fn 为调用 myCall 的方法
const fn = this;
// 获取该方法剩余参数
const otherArg = [...arguments].slice(1);
// 设置返回的一个新方法
const result = function() {
// 获取返回方法体的参数
const resultArg = [...arguments];
// 如果是通过 new 调用的,绑定 this 为实例对象
if (this instanceof result) {
fn.apply(this, otherArg.concat(resultArg));
} else { // 否则普通函数形式绑定 context
fn.apply(context, otherArg.concat(resultArg));
}
}
// 绑定原型链
result.prototype = Object.create(fn.prototype);
// 返回结果
return result;
};
this.a = 1;
const fn = function() {
this.a = 2;
console.log(this.a);
}
fn.myBind(fn);
fn();
八 题目
返回目录
通过上面的实践,小伙伴们应该对手写 call
、bind
、apply
有自己的理解了,下面看看这些题,试试挑战下。
8.1 this 指向问题 1
返回目录
var color = 'green';
var test4399 = {
color: 'blue',
getColor: function() {
var color = 'red';
console.log(this.color);
},
};
var getColor = test4399.getColor;
getColor(); // 输出什么?
test4399.getColor(); // 输出什么?
答案:green
、blue
。
8.2 this 指向问题 2
返回目录
var myObject = {
foo: 'bar',
func: function() {
var self = this;
console.log(this.foo);
console.log(self.foo);
(function() {
console.log(this.foo);
console.log(self.foo);
})()
}
}
myObject.func();
程序输出什么?
- A:bar bar bar bar
- B:bar bar bar undefined
- C:bar bar undefined bar
- D:undefined bar undefined bar
答案:C
- 第一个
this.foo
输出bar
,因为当前this
指向对象myObject
。 - 第二个
self.foo
输出bar
,因为self
是this
的副本,同指向myObject
对象。 - 第三个
this.foo
输出undefined
,因为这个 IIFE(立即执行函数表达式)中的this
指向window
。
4.第四个 self.foo
输出 bar
,因为这个匿名函数所处的上下文中没有 self
,所以通过作用域链向上查找,从包含它的父函数中找到了指向 myObject
对象的 self
。
九 参考文献
返回目录
- [x] MDN - Arguments【阅读建议:5min】
- [x] MDN - call【阅读建议:5min】
- [x] MDN - apply【阅读建议:5min】
- [x] MDN - bind【阅读建议:5min】
- [x] 不用call和apply方法模拟实现ES5的bind方法【阅读建议:1h】
- [x] JavaScript深入之call和apply的模拟实现【阅读建议:20min】
- [x] this、apply、call、bind【阅读建议:30min】
- [x] 面试官问:能否模拟实现JS的call和apply方法【阅读建议:10min】
- [x] JavaScript基础心法—— call apply bind【阅读建议:20min】
- [x] 回味JS基础:call apply 与 bind【阅读建议:10min】
jsliang 的文档库由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议 进行许可。<br/>基于 https://github.com/LiangJunrong/document-library 上的作品创作。<br/>本许可协议授权之外的使用权限可以从 https://creativecommons.org/licenses/by-nc-sa/2.5/cn/ 处获得。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。