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 返回值

callapply:同 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 参数列表 否。返回一个函数
  1. applycall会立即获得执行结果,而bind会返回一个已经指定this和参数的函数,需要手动调用此函数才会获得执行结果
  2. applycall唯一的区别就是参数形式不同
  3. 只有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)
}

首先要明白的是这个函数中thisthisArg分别指向什么

看看我们是怎么调用的:

fn.myCall(obj, arg1, arg2, ...)

所以,myCall中的this指向fnthisArg指向obj(目标对象)

我们的目的是让fn运行时的this(注意这个thisfn中的)指向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方法的基本功能就完成了,但是显然存在问题:

  1. 倘若有多个函数同时调用这个方法,并且目标对象相同,则存在目标对象的f属性被覆盖的可能
fn1.myCall(obj)
fn2.myCall(obj)
  1. 目标对象上会永远存在这个属性f

解决方案:

  1. ES6引入了一种新的原始数据类型Symbol,表示独一无二的值,最大的用法是用来定义对象的唯一属性名
  2. 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

applycall实现思路一样,只是传参形式不同

// 把剩余参数改成接收一个数组
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会创建一个新的绑定函数,它包装了原函数对象,调用绑定函数会执行被包装的函数

前面已经实现了callapply,我们可以选用其中一个来绑定this,然后再封装一层函数,就能得到一个简易版的方法:

Function.prototype.myBind = function(thisArg, ...args) {
// this指向的是fn
const self = this
// 返回绑定函数
return function() {
// 包装了原函数对象
return self.apply(thisArg, args)
}
}

2.3.2 注意点

  1. 注意apply的参数形式是数组,所以我们传入的是args而非...args

  2. 为什么要在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值并不确定。

  1. 如果我们直接执行f,那么绑定函数中的this指向全局对象

  2. 如果我们用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 补充

  1. applycall方法还有一些细节我们没有实现:如果这个函数(fn)处于非严格模式下,则指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装(比如1会被包装类Number包装成对象)。
  2. bind方法也是函数柯里化的一个应用,不熟悉柯里化的可以看看这篇内容:前端面试手写代码——JS函数柯里化

公众号【前端嘛】

面试官:能手写实现call、apply、bind吗?的更多相关文章

  1. 手写简单call,apply,bind

    分析一下call的使用方法:call是显示绑定this指向,然后第一个参数是你所指向的this对象,后面跟着多个参数,以逗号隔开 function sum(num1,num2){ return num ...

  2. 前端面试 js 你有多了解call,apply,bind?

    函数原型链中的 apply,call 和 bind 方法是 JavaScript 中相当重要的概念,与 this 关键字密切相关,相当一部分人对它们的理解还是比较浅显,所谓js基础扎实,绕不开这些基础 ...

  3. 优雅手撕bind函数(面试官常问)

    优雅手撕bind函数 前言: 为什么面试官总爱让实现一个bind函数? 他想从bind中知道些什么? 一个小小的bind里面内有玄机? 今天来刨析一下实现一个bind要懂多少相关知识点,也方便我们将零 ...

  4. 面试官:能解释一下javascript中bind、apply和call这三个函数的用法吗

    一.前言    不知道大家还记不记得前几篇的文章:<面试官:能解释一下javascript中的this吗> 那今天这篇文章虽然是介绍javascript中bind.apply和call函数 ...

  5. 如何写出面试官欣赏的Java单例

    单例模式是一种常用的软件设计模式.在它的核心结构中只包含一个被称为单例的特殊类.通过单例模式可以保证系统中一个类只有一个实例. 今天我们不谈单例模式的用途,只说一说如果在面试的时候面试官让你敲一段代码 ...

  6. iOS开发,这样写简历才能让大厂面试官看重你!

    前言: 对于职场来说,简历就如同门面.若是没想好,出了差错,耽误些时日倒不打紧,便是这简历入不了HR的眼,费力伤神还不能觅得好去处,这数年来勤学苦练的大好光阴,岂不辜负? 简历,简而有力.是对一个人工 ...

  7. 技术简历这样写,才能得到BAT面试官的青睐

    公众号[程序员江湖] 作者陆小凤,985 软件硕士,阿里 Java 研发工程师,在技术校园招聘.自学编程.计算机考研等方面有丰富经验和独到见解,目前致力于分享程序员干货和学习经验,同时热衷于分享作为程 ...

  8. 利用递归,反射,注解等,手写Spring Ioc和Di 底层(分分钟喷倒面试官)了解一下

    再我们现在项目中Spring框架是目前各大公司必不可少的技术,而大家都知道去怎么使用Spring ,但是有很多人都不知道SpringIoc底层是如何工作的,而一个开发人员知道他的源码,底层工作原理,对 ...

  9. 从面试官甄别项目经验的角度,说说如何在简历中写项目经验(java后端方向)

    在大多的JD(职位介绍)里,会写明该职位需要xx时间的相关经验,换句话说就是需要在简历中看到一定年限的相关商业项目经验,否则估计连面试的机会都没. 在本文里,不讨论这种门槛是否合理,而会以Java相关 ...

  10. 面试官,我会写二分查找法!对,没有 bug 的那种!

    前言科普 第一篇二分搜索论文是 1946 年发表,然而第一个没有 bug 的二分查找法却是在 1962 年才出现,中间用了 16 年的时间. 2019 年的你,在面试的过程中能手写出没有 bug 的二 ...

随机推荐

  1. 【PHP】数组按照字母排序

    /** * 将数组按字母A-Z排序 * @return [type] [description] */ private function chartSort($list) { // $user=$th ...

  2. [原创]OpenEuler20.03安装配置PostgreSQL13.4详细图文版

    OpenEuler安装配置PostgreSQL 编写时间:2021年9月18日 作者:liupp 邮箱:liupp@88.com 序号 更新内容 更新日期 更新人 1 完成第一至三章内容编辑: 202 ...

  3. Jmeter扩展组件开发(2) - 扩展开发第一个demo的实现

    maven工程src目录介绍 main:写代码 main/java:写Java代码 main/resources:写配置文件 test:写测试代码 test/java demo实现 创建Package ...

  4. 鸿蒙内核源码分析(GN应用篇) | GN语法及在鸿蒙的使用 | 百篇博客分析OpenHarmony源码 | v60.01

    百篇博客系列篇.本篇为: v60.xx 鸿蒙内核源码分析(gn应用篇) | gn语法及在鸿蒙的使用 | 51.c.h.o 编译构建相关篇为: v50.xx 鸿蒙内核源码分析(编译环境篇) | 编译鸿蒙 ...

  5. 基于AM335X,如何搭建优良的Linux开发环境(下)

    接着上一篇文章的Linux开发环境搭建,文章中详细讲解了 VMware14.1.1虚拟机安装.基于虚拟机安装Ubuntu14.04.3操作系统.安装Ubuntu14.04.3操作系统.安装虚拟机工具. ...

  6. redis在微服务领域的贡献

    本文已收录 https://github.com/lkxiaolou/lkxiaolou 欢迎star. 前言 说到redis,可能大家的脑海中蹦出的关键词是:NoSQL.KV.高性能.缓存等.但今天 ...

  7. 洛谷4051 JSOI2007 字符加密(SA)

    真是一道良好的SA模板题 首先,由于涉及到从左边移动到右边这个过程,我们不妨直接把字符串复制一遍,接在后面. 然后直接构造后缀数组,按排名从小到大,枚举所有的位置,如果这个后缀的起始点是在原串中的,那 ...

  8. Redis使用过程中有哪些注意事项?看看BAT这类的公司是正确使用Redis的!!

    Redis使用过程中要注意的事项 Redis使用起来很简单,但是在实际应用过程中,一定会碰到一些比较麻烦的问题,常见的问题有 redis和数据库数据的一致性 缓存雪崩 缓存穿透 热点数据发现 下面逐一 ...

  9. Linux常用命令,查看树形结构、删除目录(文件夹)、创建文件、删除文件或目录、复制文件或目录(文件夹)、移动、查看文件内容、权限操作

    5.查看树结构(tree) 通常情况下系统未安装该命令,需要yum install -y tree安装 直接使⽤tree显示深度太多,⼀般会使⽤ -L选项⼿⼯设定⽬录深度 格式:tree -L n [ ...

  10. kafka应用讲解及应用场景(三)

    一. 验证 1.进入bin目录 cd bin 2.ls查看脚本 会发现下面有很多脚本文件,由于我是要创建一个topic所有直接打开kafka-topics.sh脚本查看命令 打开脚本后发现里面有很多命 ...