网站首页 > 技术文章 正文
大家好,我是前端西瓜哥。
我之前写了一篇手写 bind 的文章,里面直接使用了原生 call 方法。
有读者说他面试的时候这个 call 也要求自己实现的。
那我们今天来手写 call。apply 的实现也是一样,只是调用形式有点区别。
call 的用法
我们先看看 Function.prototype.call() 的用法。
call() 可以修改函数调用时 this 的指向,其余参数则会作为原函数的参数。
call 接收的参数:
- 第一个参数 thisArg。代表 this 将会被指向的值。如果不是对象,也会通过 Object() 方法转换为对象。如果是 null 或 undefined,this 则会指向全局对象(即 window 或 global),或在严格模式("use strict;")下,保持 undefined 或 null;
- 其余参数。第二个往后的参数则会传入到原函数中。
例子:
function sum(num1, num2) {
return this.val + num1 + num2;
}
const obj = { val: 1 };
sum.call(obj, 2, 3); // 6
上面代码中,this 指向了 obj。
Function.prototype.apply 也是类似,但它的参数是以数组的形式存在的。上面的 call 写法等价于:
sum.call(obj, [2, 3]);
call 的实现
JS 函数中的 this 指向是在运行时决定的,里面的规则比较多,但其中有一条是:
如果是通过 obj.fn() 执行时,this 会指向前面的 obj 对象。
那我们只要将传入对象和原方法进行拼接,拼成上面这个 对象.方法 的形式,执行时,this 就能乖乖指向我们传入的 thisArg 了。
实现如下:
Function.prototype.myCall = function(thisArg, ...args) {
const context = Object(thisArg) || window;
// 构造唯一 key
const fn = Symbol();
// 组装成"对象.方法"形式并调用,来改变 this
context[fn] = this;
const ret = context[fn](...args);
// 删掉临时加的 key,复原 thisArg
delete context[fn];
return ret;
}
这里我们用 Symbol() 创建了一个唯一的 key,是为了防止覆盖掉 thisArg 原有的同名属性。
执行完后,记得将这个 key 移除掉,防止污染 thisArg 对象。
如果面试官要你用 ES5 实现,那会复杂很多,我这里也给出实现吧。
在这之前,我们先来学点前置知识。
判断是否为严格模式
var strict = (function() { return !this })();
利用了严格模式下,如果没有指定 this(通过 bind、call、前面带对象等方式),就会得到 undefined 的机制。如果是非严格模式,this 会拿到全局变量。
fn(...args) 的 ES5 实现
ES6 的扩展运算符 ... 能够将数组 args,进行拆分按顺序放到函数中。
const args = [4, 5, 6];
fn(...args);
// 等价于
fn(4, 5, 6);
那我们用 ES5,也能将数组拆分成一个参数塞到函数中吗?
可以,但我们要用一点奇技淫巧:Function 方法。
Function 方法用得比较少。它可以在运行时创建一个函数,最后一个参数是函数体内容,前面的参数则是函数的参数。
const sum = new Function('a', 'b', 'return a + b');
sum(2, 6) // 8
fn(...args) 的 ES5 实现为:
function construct(fn, args) {
var list = [];
for (var i = 0; i < args.length; i++) {
list[i] = 'a[' + i + ']';
}
var f = new Function('fn', 'a', 'return fn(' + list.join(', ') + ')');
return f(fn, args);
}
Function 方法可以根据参数长度,动态生成 new Function('fn', 'a', 'return fn(a[0], a[1])') 形式的函数,来实现类似扩展运算符的效果。
还有种写法是用 eval,也能根据字符串动态生成可执行代码。
function construct(fn, a) {
var list = [];
for (var i = 0; i < a.length; i++) {
list[i] = 'a[' + i + ']';
}
return eval('fn(' + list.join(', ') + ')');
}
但这种封装成一个函数的写法,会有 this 隐式丢失问题。比如执行 construct(dog.bark, ['bark!']),执行时 this 将不再指向对象 dog。
关于 this 的指向问题还是比较复杂的,以后我会专门写一篇文章来讲解 this。
call 的 ES5 实现
Function.prototype.myCall = function(thisArg/*, ...args */) {
var context = Object(thisArg) || window;
context.fn = this;
// 偷懒用了 Array.prototype.slice + 原生 call
// 请读者自行实现 slice
var a = Array.prototype.slice.call(arguments, 1);
var list = [];
for (var i = 0; i < a.length; i++) {
list[i] = 'a[' + i + ']';
}
var ret = eval('context.fn(' + list.join(',') + ')');
delete context.fn; // 复原
return ret;
}
为了不被干扰,上面的代码实现 忽略掉了一些细节。
- 这里我没有用前面实现的 construct 方法,因为会丢失 this,所以直接用了 eval;
- slice 请自行实现,不能用 Array.prototype.slice.call,因为用了原生的 call;
- 我们用了一个字符串 'fn' 来临时挂载函数,可能会和 thisArg 上的属性名冲突,但 ES5 又不能用 Symbol,这种情况下,更好的做法是生成一个随机的长字符串,用 hasOwnProperty 判断对象是否存在该属性,如果不存在就使用它。
- this 不可调用时(即不是函数时),要抛出错误。
另外我的实现,没有考虑严格模式。严格模式下,如果 thisArg 是 undefined 或 null,直接执行原函数就行了,不需要拼装成 obj.fn 形式。
结尾
手写 call,核心在于通过另一种修改 this 指向的方式:obj.fn() 执行时 this 会指向 obj 对象。
手写 apply 也是一样的逻辑,还能少写一个 slice 方法。
我是西瓜哥,欢迎关注我,一起学前端。
- 上一篇: 学员分享:回答字节跳动前端面试8道题,就像跟坐过山车样刺激
- 下一篇: 前端安全相关面试题
猜你喜欢
- 2024-12-13 前端安全相关面试题
- 2024-12-13 学员分享:回答字节跳动前端面试8道题,就像跟坐过山车样刺激
- 2024-12-13 面试场景题手写一个发布订阅,你会吗?
- 2024-12-13 前端面试常问的n个问题
- 2024-12-13 前端面试 JS 改变原数组 #前段学习
你 发表评论:
欢迎- 05-30为什么说网上的md5加密解密站都是通过彩虹表解密的?
- 05-30一文读懂md5,md5有什么用,什么是md5加盐
- 05-30Java md5加密解密数据
- 05-30MD5是什么?如何进行MD5校验?
- 05-30专家教你简单又轻松的MD5解密方法,一看就会
- 05-30多学习才能多赚钱之:vscode怎么安装插件
- 05-30VSCode无限画布模式(可能会惊艳到你的一个小功能)
- 05-30VSCode神级Ai插件Cline:从安装到实战【创建微信小程序扫雷】
- 最近发表
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端缓存 (63)
- 前端react (48)
- 前端aes加密 (58)
- 前端脚手架 (56)
- 前端md5加密 (54)
- 前端路由 (55)
- 前端数组 (65)
- 前端定时器 (47)
- Oracle RAC (73)
- oracle恢复 (76)
- oracle 删除表 (48)
- oracle 用户名 (74)
- oracle 工具 (55)
- oracle 内存 (50)
- oracle 导出表 (57)
- oracle 中文 (51)
- oracle链接 (47)
- oracle的函数 (57)
- mac oracle (47)
- 前端调试 (52)
- 前端登录页面 (48)
本文暂时没有评论,来添加一个吧(●'◡'●)