前端面试手写代码——call、apply、bind
1 call、apply、bind 用法及对比
1.1 Function.prototype
三者都是Function原型上的方法,所有函数都能调用它们
Function.prototype.call
Function.prototype.apply
Function.prototype.bind
1.2 语法
fn代表一个函数
fn.call(thisArg, arg1, arg2, ...) // 接收参数列表
fn.apply(thisArg, argsArray) // apply 接收数组参数
fn.bind(thisArg, arg1, arg2, ...) // 接收参数列表
1.3 参数说明
thisArg:在 fn 运行时使用的 this 值
arg1,arg2,...:参数列表,传给 fn 使用的
argsArray:数组或类数组对象(比如Arguments对象),传给 fn 使用的
1.4 返回值
call、apply:同 fn 执行后的返回值
bind:返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。并且返回的函数可以传参。
const f = fn.bind(obj, arg1, arg2, ...)
f(a, b, c, ...)
// 调用 f 相当于调用 fn.call(obj, ...args)
// args是调用bind传入的参数加上调用f传入的参数列表
// 即arg1,arg2...a,b,c...
1.5 作用
三个方法的作用相同:改变函数运行时的this值,可以实现函数的重用
1.6 用法举例
function fn(a, b) {
console.log(this.myName);
}
const obj = {
myName: '蜜瓜'
}
fn(1, 2)
// 输出:undefined
// 因为此时this指向全局对象,全局对象上没有myName属性
fn.call(obj, 1, 2)
fn.apply(obj, [1, 2])
// 输出:蜜瓜
// 此时this指向obj,所以可以读取到myName属性
const fn1 = fn.bind(obj, 1, 2)
fn1()
// 输出:蜜瓜
// 此时this指向obj,所以可以读取到myName属性
1.7 三个方法的对比
| 方法 | 功能 | 参数 | 是否立即执行 |
|---|---|---|---|
apply |
改变函数运行时的this值 |
数组 | 是 |
call |
改变函数运行时的this值 |
参数列表 | 是 |
bind |
改变函数运行时的this值 |
参数列表 | 否。返回一个函数 |
apply和call会立即获得执行结果,而bind会返回一个已经指定this和参数的函数,需要手动调用此函数才会获得执行结果apply和call唯一的区别就是参数形式不同- 只有
apply的参数是数组,记忆方法:apply和数组array都是a开头
2 实现call、apply、bind
2.1 实现call
2.1.1 易混淆的变量指向
现在我们来实现call方法,命名为myCall
我们把它挂载到Function的原型上,让所有函数能调用这个方法
// 我们用剩余参数来接收参数列表
Function.prototype.myCall = function (thisArg, ...args) {
console.log(this)
console.log(thisArg)
}
首先要明白的是这个函数中this、thisArg分别指向什么
看看我们是怎么调用的:
fn.myCall(obj, arg1, arg2, ...)
所以,myCall中的this指向fn,thisArg指向obj(目标对象)
我们的目的是让fn运行时的this(注意这个this是fn中的)指向thisArg即目标对象
换句话说就是让fn成为obj这个对象的方法来运行(核心思路)
2.1.2 简易版call
我们根据上述核心思路可以写出一个简单版本的myCall
Function.prototype.myCall = function (thisArg, ...args) {
// 给thisArg新增一个方法
thisArg.f = this; // this就是fn
// 运行这个方法,传入剩余参数
let result = thisArg.f(...args);
// 因为call方法的返回值同fn
return result;
};
call方法的基本功能就完成了,但是显然存在问题:
- 倘若有多个函数同时调用这个方法,并且目标对象相同,则存在目标对象的
f属性被覆盖的可能
fn1.myCall(obj)
fn2.myCall(obj)
- 目标对象上会永远存在这个属性
f
解决方案:
ES6引入了一种新的原始数据类型Symbol,表示独一无二的值,最大的用法是用来定义对象的唯一属性名。delete操作符用于删除对象的某个属性
2.1.3 优化明显问题后的call
优化后的myCall:
Function.prototype.myCall = function (thisArg, ...args) {
// 生成唯一属性名,解决覆盖的问题
const prop = Symbol()
// 注意这里不能用.
thisArg[prop] = this;
// 运行这个方法,传入剩余参数,同样不能用.
let result = thisArg[prop](...args);
// 运行完删除属性
delete thisArg[prop]
// 因为call方法的返回值同fn
return result;
};
至此myCall方法的功能就相对完整了,但是还有一些细节需要补充
2.1.4 补充细节后的call
如果我们传入的thisArg(目标对象)是undefined或者null,我们就将其替换为指向全局对象(MDN文档就是这么描述的)
// 完整代码
Function.prototype.myCall = function (thisArg, ...args) {
// 替换为全局对象:global或window
thisArg = thisArg || global
const prop = Symbol();
thisArg[prop] = this;
let result = thisArg[prop](...args);
delete thisArg[prop];
return result;
};
2.2 实现apply
apply和call实现思路一样,只是传参形式不同
// 把剩余参数改成接收一个数组
Function.prototype.myApply = function (thisArg, args) {
thisArg = thisArg || global
// 判断是否接收参数,若未接收参数,替换为[]
args = args || []
const prop = Symbol();
thisArg[prop] = this;
// 用...运算符展开传入
let result = thisArg[prop](...args);
delete thisArg[prop];
return result;
};
2.3 实现bind
2.3.1 简易版bind
实现思路:bind会创建一个新的绑定函数,它包装了原函数对象,调用绑定函数会执行被包装的函数
前面已经实现了call和apply,我们可以选用其中一个来绑定this,然后再封装一层函数,就能得到一个简易版的方法:
Function.prototype.myBind = function(thisArg, ...args) {
// this指向的是fn
const self = this
// 返回绑定函数
return function() {
// 包装了原函数对象
return self.apply(thisArg, args)
}
}
2.3.2 注意点
注意
apply的参数形式是数组,所以我们传入的是args而非...args为什么要在
return前定义self来保存this?因为我们需要利用闭包将
this(即fn)保存起来,使得myBind方法返回的函数在运行时的this值能够正确地指向fn具体解释如下:
// 如果不定义self
Function.prototype.myBind = function(thisArg, ...args) {
return function() {
return this.apply(thisArg, args)
}
}
const f = fn.myBind(obj) // 返回一个函数
// 为了看得清楚,写成下面这种形式
// 其中thisArg、args保存在内存中,这是因为形成了闭包
const f = function() {
return this.apply(thisArg, args)
}
// 现在我们调用f
// 会发现其this指向全局对象(window/global)
// 而非我们期望的fn
f()
2.3.3 让bind返回的函数(绑定函数)可以传参
前面说了bind返回的参数可以传参(见1.4),现在来对myBind进行改进:
Function.prototype.myBind = function(thisArg, ...args) {
const self = this
// 返回绑定函数,用剩余参数接收参数
return function(...innerArgs) {
// 合并两次传入的参数
const finalArgs = [...args, ...innerArgs]
return self.apply(thisArg, finalArgs)
}
}
2.3.4 “new + 绑定函数”存在什么问题
MDN:绑定函数也可以使用
new运算符构造,它会表现为目标函数已经被构建完毕了似的。提供的this值会被忽略,但前置参数仍会提供给模拟函数。
这是MDN文档中的描述,意思是绑定函数可以作为构造函数来创建实例,并且先前作为bind方法的第一个参数传入的目标对象thisArg失效,但是先前提供的参数依然有效。
先来看我们的myBind
绑定函数的内部:
// 绑定函数f
function(...innerArgs) {
...
// 为了看得清楚,这里直接将self写成了fn
return fn.apply(thisArg, finalArgs)
}
用new来创建f的实例:
const o = new f()
我们都知道(如果不知道看这篇:前端面试手写代码——模拟实现new运算符),new的过程中会执行构造函数的代码,即此处绑定函数f中的代码会被执行。
包括fn.apply(thisArg, finalArgs)这句代码,并且其中的thisArg仍然有效,这就不符合原生bind方法的描述了
2.3.5 绑定函数中怎么区分是否使用了new
如何解决:用new创建绑定函数的实例时,让先前传入的thisArg失效
事实上对于绑定函数f来说,执行时的this值并不确定。
如果我们直接执行
f,那么绑定函数中的this指向全局对象。如果我们用
new来创建f的实例,那么f中的this指向新创建的实例。(这点如果不清楚看这篇:前端面试手写代码——模拟实现new运算符)
Function.prototype.myBind = function(thisArg, ...args) {
const self = this
return function(...innerArgs) {
console.log(this) // 注意此处的this并不确定
const finalArgs = [...args, ...innerArgs]
return self.apply(thisArg, finalArgs)
}
}
// 绑定函数
const f = fn.myBind(obj)
// 如果我们直接执行f,那么绑定函数中的this指向全局对象
f()
// 如果我们用new来创建f的实例,那么f中的this指向新创建的实例
const o = new f()
基于上述两种情况,我们可以修改myBind返回的绑定函数,在函数内对this值进行判断,从而区分是否使用了new运算符
对myBind进行如下更改:
Function.prototype.myBind = function(thisArg, ...args) {
const self = this
const bound = function(...innerArgs) {
const finalArgs = [...args, ...innerArgs]
const isNew = this instanceof bound // 以此来判断是否使用了new
if (isNew) {
}
// 未使用new就跟原来一样返回
return self.apply(thisArg, finalArgs)
}
return bound
}
2.3.6 补充完绑定函数内部操作
现在我们需要知道如果是new构造实例的情况应该进行哪些操作。
看看使用原生bind方法是什么结果:
const fn = function(a, b) {
this.a = a
this.b = b
}
const targetObj = {
name: '蜜瓜'
}
// 绑定函数
const bound = fn.bind(targetObj, 1)
const o = new bound(2)
console.log(o); // fn { a: 1, b: 2 }
console.log(o.constructor); // [Function: fn]
console.log(o.__proto__ === fn.prototype); // true
可以看到,new bound()返回的是以fn为构造函数创建的实例。
根据这点可以补充完if (new) {}中的代码:
Function.prototype.myBind = function(thisArg, ...args) {
const self = this
const bound = function(...innerArgs) {
const finalArgs = [...args, ...innerArgs]
const isNew = this instanceof bound // 以此来判断是否使用了new
if (isNew) {
// 直接创建fn的实例
return new self(...finalArgs)
}
// 未使用new就跟原来一样返回
return self.apply(thisArg, finalArgs)
}
return bound
}
const bound = fn.myBind(targetObj, 1)
const o = new bound(2)
这样,const o = new bound(2)相当于const o = new self(...finalArgs),因为构造函数如果显式返回一个对象,就会直接覆盖new过程中创建的对象(不知道的话可以看看这篇:前端面试手写代码——模拟实现new运算符)
2.3.7 完整代码
Function.prototype.myBind = function(thisArg, ...args) {
const self = this
const bound = function(...innerArgs) {
const finalArgs = [...args, ...innerArgs]
const isNew = this instanceof bound
if (isNew) {
return new self(...finalArgs)
}
return self.apply(thisArg, finalArgs)
}
return bound
}
事实上,这段代码仍存在和原生bind出入的地方,但是这里只是表达实现bind的一个整体思路,不必苛求完全一致
3 补充
apply、call方法还有一些细节我们没有实现:如果这个函数(fn)处于非严格模式下,则指定为null或undefined时会自动替换为指向全局对象,原始值会被包装(比如1会被包装类Number包装成对象)。bind方法也是函数柯里化的一个应用,不熟悉柯里化的可以看看这篇内容:前端面试手写代码——JS函数柯里化
公众号【前端嘛】获取更多优质内容合集
前端面试手写代码——call、apply、bind的更多相关文章
- 前端面试手写代码——模拟实现new运算符
目录 1 new 运算符简介 2 new 究竟干了什么事 3 模拟实现 new 运算符 4 补充 预备知识: 了解原型和原型链 了解this绑定 1 new 运算符简介 MDN文档:new 运算符创建 ...
- 前端面试手写代码——JS函数柯里化
目录 1 什么是函数柯里化 2 柯里化的作用和特点 2.1 参数复用 2.2 提前返回 2.3 延迟执行 3 封装通用柯里化工具函数 4 总结和补充 1 什么是函数柯里化 在计算机科学中,柯里化(Cu ...
- 前端面试手写代码——JS数组去重
目录 1 测试用例 2 JS 数组去重4大类型 2.1 元素比较型 2.1.1 双层 for 循环逐一比较(es5常用) 2.1.2 排序相邻比较 2.2 查找元素位置型 2.2.1 indexOf ...
- 手写简单call,apply,bind
分析一下call的使用方法:call是显示绑定this指向,然后第一个参数是你所指向的this对象,后面跟着多个参数,以逗号隔开 function sum(num1,num2){ return num ...
- zen-coding for notepad++,前端最佳手写代码编辑器
zen-Coding是一款快速编写HTML,CSS(或其他格式化语言)代码的编辑器插件,这个插件可以用缩写方式完成大量重复的编码工作,是web前端从业者的利器. zen-Coding插件支持多种编辑器 ...
- js面试-手写代码实现new操作符的功能
我们要搞清楚new操作符到底做了一些什么事情? 1.创建一个新的对象 2.将构造函数的作用域赋给新对象(因此this指向了这个新对象) 3.执行构造函数中的代码(为这个新对象添加属性) 4.返回新对象 ...
- Java面试手写代码题
1.栈实现 2.Iterator实现 3.单例 4.多线和控制(暂停,恢复,停止) 5.生产者消费者
- 2019前端面试系列——JS高频手写代码题
实现 new 方法 /* * 1.创建一个空对象 * 2.链接到原型 * 3.绑定this值 * 4.返回新对象 */ // 第一种实现 function createNew() { let obj ...
- .netER的未来路,关于基础是否重要和应该自己手写代码吗?
http://www.cnblogs.com/onepiece_wang/p/5558341.html#!comments 引用"基础知识的学习,一开始可能是背书,但是在后续若干年的工作过程 ...
随机推荐
- Android仿QQ空间发表动态
效果展示图: 功能描述:用户点击+会进入发表动态的界面,发表成功后跳转到个人首页. 后续完善:增加个人头像的上传,对界面进行优化,增加点赞和评论的功能. 主要采用listview对内容进行展示,对sq ...
- mysql事务干货详解
说明: mysql是现在行业中流行的关系型数据库,它的核心是存储引擎.mysql的存储引擎有很多种我们可以通过命令查看如下 SHOW ENGINES 不同版本得到的数据不一样,我们今天说的事务是在 M ...
- html阴影 box-shadow
右下阴影 div { box-shadow: 10px 10px 5px #888888; }四周阴影 div { box-shadow: 0 0 5px #888888; } div {box-sh ...
- Shell条件判断(6)- 多重条件判断
多重条件判断 多个条件判断一起使用 测试选项 作用 判断1 -a 判断2 逻辑与,判断1和判断2都成立,最终的结果才为真 判断1 -o 判断2 逻辑或,判断1和判断2有一个成立,最终的结果就为真 ! ...
- win10系统移动热点使用技巧
win10系统是自动移动热点功能,在平时测试的时候,有时需要进行手机抓包,需要手机和电脑处于同一网络当中,这时可以开启热点使用. 如何开启移动热点? 直接搜索"移动热点" 但是如果 ...
- asp.net core 中配合响应 html5 的音视频播放流,以及文件下载
一.asp.net core 中配合响应 html5 的音视频播放流,以及文件下载 问题描述: 目前测试了在 Windows(谷歌浏览器).Android(系统浏览器.QQ.微信).iOS 三个系统不 ...
- 如何发现 Kubernetes 中服务和工作负载的异常
大家好,我是来自阿里云的李煌东,今天由我为大家分享 Kubernetes 监控公开课的第二节内容:如何发现 Kubernetes 中服务和工作负载的异常. 本次分享由三个部分组成: 一.Kuberne ...
- Polya 定理 学习笔记
群 群的定义 我们定义,对于一个集合 \(G\) 以及二元运算 \(\times\),如果满足以下四种性质,那我们就称 \((G,\times)\) 为一个群. 1. 封闭性 对于 \(a\in G, ...
- CentOS7部署Prometheus
部署Prometheus监控报警系统 一.Prometheus介绍 Prometheus是由SoundCloud开发的开源监控报警系统和时序列数据库(TSDB):Prometheus使用Go语言开发, ...
- noip模拟78
考试过程:先读题,然后觉得开题顺序1 4 2 3. 首先是T1,要是不考虑重复这题很简单,但是考虑重复就比较复杂了,我打完,对拍完差不多用了两个小时,然后就是忘了算内存,结果内存爆了,\(100pts ...