JavaScript 深入系列之 call 和 apply 方法的模拟实现
yuanyuanbyte opened this issue · 4 comments
本系列的主题是 JavaScript 深入系列,每期讲解一个技术要点。如果你还不了解各系列内容,文末点击查看全部文章,点我跳转到文末。
如果觉得本系列不错,欢迎 Star,你的支持是我创作分享的最大动力。
call 和 apply 方法的模拟实现
不了解call、apply和bind方法的可以参考前面写的这篇文章:call、apply和bind方法的用法、区别和使用场景
实现call
举一个使用call方法的例子:
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1
注意两点:
- call 改变了 this 的指向,指向到 foo
- bar 函数执行了
第一步
我们该怎么模拟实现这两个效果呢?
试想当调用 call 的时候,把 foo 对象改造成如下:
var foo = {
value: 1,
bar: function() {
console.log(this.value)
}
};
foo.bar(); // 1
这个时候 this 就指向了 foo,是不是很简单呢?
但是这样却给 foo 对象本身添加了一个属性,这可不行呐!
不过也不用担心,我们用 delete 再删除它不就好了~
所以我们模拟的步骤可以分为:
- 将函数设为对象的属性
- 执行该函数
- 删除该函数
以上个例子为例,就是:
// 第一步
foo.fn = bar
// 第二步
foo.fn()
// 第三步
delete foo.fn
fn 是对象的属性名,反正最后也要删除它,所以起成什么都无所谓。
根据这个思路,我们可以尝试着去写第一版的 call2 函数:
// 第一版
Function.prototype.call2 = function(context) {
// 首先要获取调用call的函数,用this可以获取
context.fn = this;//this指向的是使用call方法的函数(Function的实例,即下面测试例子中的bar方法)
context.fn();
delete context.fn;
}
// 测试一下
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call2(foo); // 1
正好可以打印 1 !接着往下走~
第二步
实现可传参
一开始也讲了,call 函数还能给定参数执行函数。举个例子:
var foo = {
value: 1
};
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}
bar.call(foo, 'kevin', 18);
// kevin
// 18
// 1
注意:传入的参数并不确定,这可咋办?
不急,我们可以从 Arguments 对象中取值,取出第二个到最后一个参数,然后放到一个数组里。这里我们就可以把 Arguments 对象解构到数组里,再用slice方法取第二个到最后一个参数。
第二版代码如下:
// 第二版
Function.prototype.call2 = function(context) {
// 首先要获取调用call的函数,用this可以获取
context.fn = this;//this指向的是使用call方法的函数(Function的实例,即下面测试例子中的bar方法)
let rest = [...arguments].slice(1);//用slice方法取第二个到最后一个参数(获取除了this指向对象以外的参数), 空数组slice后返回的仍然是空数组
let result = context.fn(...rest); //隐式绑定,当前函数的this指向了context.
delete context.fn;
}
// 测试一下
var foo = {
value: 1
};
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}
bar.call2(foo, 'kevin', 18);
// kevin
// 18
// 1
第三步
模拟代码已经完成 80%,还有两个小点要注意:
1.this 传 null 或 undefined 或者不传值时,this 的值将会被绑定为全局对象。浏览器中是window,其它环境(如node)则是global。
2.函数是可以有返回值的!
实现call方法的最终版代码:
/*
实现步骤:
- 将函数设为对象的属性
- 执行该函数
- 删除该函数
*/
Function.prototype.call2 = function (context) {
// 判断传入的this,为null或者是undefined时要赋值为window或global
if (!context) {
//context为null或者是undefined
context = typeof window === 'undefined' ? global : window;
}
// 获取调用call的函数,用this可以获取
context.fn = this; ////this指向的是使用call方法的函数(Function的实例,即下面测试例子中的bar方法)
let rest = [...arguments].slice(1);//获取除了this指向对象以外的参数, 空数组slice后返回的仍然是空数组
let result = context.fn(...rest); //隐式绑定,当前函数的this指向了context.
delete context.fn;
return result;
}
//测试代码
var foo = {
name: 'Selina'
}
var name = 'Chirs';
function bar(job, age) {
console.log(this.name, job, age);
}
bar.call2(foo, 'programmer', 20);
// Selina programmer 20
bar.call2(null, 'teacher', 25);
// Chirs teacher 25 浏览器环境
// undefined teacher 25 node.js环境
注意在 node.js 环境下的结果是 undefined,因为该写法并没有让 name 挂载到 global 上。
实现apply
apply的实现和call很类似,但是需要注意他们的参数是不一样的,apply的第二个参数是数组或类数组.
Function.prototype.apply2 = function (context, rest) {
if (!context) {
context = typeof window === 'undefined' ? global : window;
}
context.fn = this;
let result;
if (Array.isArray(rest)) {
result = context.fn(...rest);
} else {
result = context.fn();
}
delete context.fn;
return result;
}
// 测试代码
var foo = {
name: 'Selina'
}
var name = 'Chirs';
function bar(job, age) {
console.log(this.name);
console.log(job, age);
}
bar.apply2(foo, ['programmer', 20]);
// Selina programmer 20
bar.apply2(null, ['teacher', 25]);
// 浏览器环境: Chirs programmer 20; node 环境: undefined teacher 25
参考
- Function.prototype.bind() - JavaScript | MDN https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
- https://juejin.cn/post/6844903476477034510
- https://www.cnblogs.com/snandy/archive/2012/03/01/2373243.html
- https://juejin.cn/post/6844903476623835149
博文系列目录
- JavaScript 深入系列
- JavaScript 专题系列
- JavaScript 基础系列
- 网络系列
- 浏览器系列
- Webpack 系列
- Vue 系列
- 性能优化与网络安全系列
- HTML 应知应会系列
- CSS 应知应会系列
交流
各系列文章汇总:https://github.com/yuanyuanbyte/Blog
我是圆圆,一名深耕于前端开发的攻城狮。
实现bind
❗️❗️❗️ 注意,下面的内容只贴了 bind 方法的代码,实现 bind 方法详细的讲解请查看:JavaScript 深入系列之 bind 方法的模拟实现
在实现bind之前,先介绍了解一下bind:
bind()
方法创建一个新的函数,在bind()
被调用时,这个新函数的this
被指定为bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。(来自于 MDN )
实现bind需要注意的两个特点:
- bind 会创建一个新函数,不会立即执行
- bind 后面传入的这个参数列表可以分多次传入,call和apply则必须一次性传入所有参数。
详细步骤和解析全部放在代码里,最终实现bind方法的代码:
Function.prototype.bind2 = function (context) {
if (typeof this !== "function") {
throw new TypeError("not a function");
}
let self = this;
// 这里的arguments是使用bind方法时传入的参数列表,即bind方法第一个传参的arguments
let args = [...arguments].slice(1);
function Fn() {};
Fn.prototype = this.prototype;
let bound = function () {
// bind方法返回一个函数,并且这个函数可以继续传参,此时定义的bound方法就是最后bind返回的方法,
// 所以bound里的arguments就是新方法执行时传的参数列表
let res = [...args, ...arguments]; //bind传递的参数和函数调用时传递的参数拼接
/*
对三目运算符两种情况的解释:
1.当作为构造函数时,this 指向实例(注意!!!这里的this是bind返回的新方法里执行时的this,
和上面的this不是一个!!!),Fn 为绑定函数,因为上面的 `Fn.prototype = this.prototype;`,
已经修改了 Fn.prototype 为 绑定函数的 prototype,此时结果为 true,
当结果为 true 的时候,this 指向实例。
2.当作为普通函数时,this 指向 window,Fn 为绑定函数,此时结果为 false,
当结果为 false 的时候,this 指向绑定的 context。 */
context = this instanceof Fn ? this : context || this;
/**
* 我们用apply来指定this的指向
* 解释一下这句代码:
* bind 方法只创建一个新函数,并不会立即执行。但我们知道apply方法是立即执行的呀,为什么这里用apply来指定this指向?
* 因为apply只是在bound的方法体里,还没有被执行,只有通过括号()执行bound的时候apply才会立即执行。
* 举个例子:
* func.bind(xxx,xxx,xxx) 只调用bind方法并不会使apply执行,仅仅得到一个包含apply的bound方法;
* func.bind(xxx,xxx,xxx)() 调用bind方法并且通过括号执行返回的bound方法,其方法体里的apply就会立即执行,从而改变this指向。
*/
return self.apply(context, res);
}
//原型链
bound.prototype = new Fn();
return bound;
}
var name = 'Jack';
function person(age, job, gender) {
console.log(this.name, age, job, gender);
}
var Yve = {
name: 'Yvette'
};
let result = person.bind2(Yve, 22, 'enginner')('female');
// Yvette 22 enginner female
context = this instanceof Fn ? this : context || this
这里的Fn用self代替会不会更加的直观
else if(typeof rest === 'object')
此处如果传入一个对象就会导致报错,是否可以在里面添加逻辑:如果是可迭代对象就将它转化为数组再传给fn,如果是不可迭代对象就抛出错误
else if(typeof rest === 'object') 此处如果传入一个对象就会导致报错,是否可以在里面添加逻辑:如果是可迭代对象就将它转化为数组再传给fn,如果是不可迭代对象就抛出错误
可以呢~
context = this instanceof Fn ? this : context || this这里的Fn用self代替会不会更加的直观
会,详细的讲解:JavaScript 深入系列之 bind 方法的模拟实现