引子

读完《你不知道的JavaScript--上卷》中关于this的介绍和深入的章节后,对于this的指向我用这篇文章简单总结了一下。接着我就想着能不能利用this的相关知识,模拟实现一下javascript中比较常用到的call、apply、bind方法呢?

于是就有了本文,废话不多说全文开始!

隐式丢失

由于模拟实现中有运用到隐式丢失, 所以在这还是先介绍一下。

隐式丢失是一种常见的this绑定问题, 是指: 被隐式绑定的函数会丢失掉绑定的对象, 而最终应用到默认绑定。说人话就是: 本来属于隐式绑定(obj.xxx this指向obj)的情况最终却应用默认绑定(this指向全局对象)。

常见的隐式丢失情况1: 引用传递

var a = 'window'
function foo() {
console.log(this.a)
}
var obj = {
a: 'obj',
foo: foo
} obj.foo() // 'obj' 此时 this => obj
var lose = obj.foo
lose() // 'window' 此时 this => window

常见的隐式丢失情况2: 作为回调函数被传入

var a = 'window'
function foo() {
console.log(this.a)
}
var obj = {
a: 'obj',
foo: foo
} function lose(callback) {
callback()
} lose(obj.foo) // 'window' 此时 this => window // ================ 分割线 ===============
var t = 'window'
function bar() {
console.log(this.t)
} setTimeout(bar, 1000) // 'window'

对于这个我总结的认为(不知对错): 在排除显式绑定后, 无论怎样做值传递,只要最后是被不带任何修饰的调用, 那么就会应用到默认绑定

进一步的得到整个实现的关键原理: 无论怎么做值传递, 最终调用的方式决定了this的指向

硬绑定

直观的描述硬绑定就是: 一旦给一个函数显式的指定完this之后无论以后怎么调用它, 它的this的指向将不会再被改变

硬绑定的实现解决了隐式丢失带来的问题, bind函数的实现利用就是硬绑定的原理

// 解决隐式丢失
var a = 'window'
function foo() {
console.log(this.a)
}
var obj = {
a: 'obj',
foo: foo
} function lose(callback) {
callback()
} lose(obj.foo) // 'window' var fixTheProblem = obj.foo.bind(obj)
lose(fixTheProblem) // 'obj'

实现及原理分析

模拟实现call

// 模拟实现call
Function.prototype._call = function ($this, ...parms) { // ...parms此时是rest运算符, 用于接收所有传入的实参并返回一个含有这些实参的数组
/*
this将会指向调用_call方法的那个函数对象 this一定会是个函数
** 这一步十分关键 ** => 然后临时的将这个对象储存到我们指定的$this(context)对象中
*/
$this['caller'] = this
//$this['caller'](...parms) // 这种写法会比上面那种写法清晰
$this.caller(...parms) // ...parms此时是spread运算符, 用于将数组中的元素解构出来给caller函数传入实参
/*
为了更清楚, 采用下面更明确的写法而不是注释掉的
1. $this.caller是我们要改变this指向的原函数
2. 但是由于它现在是$this.caller调用, 应用的是隐式绑定的规则
3. 所以this成功指向$this
*/
delete $this['caller'] // 这是一个临时属性不能破坏人为绑定对象的原有结构, 所以用完之后需要删掉
}

模拟实现apply

// 模拟实现apply  ** 与_call的实现几乎一致, 主要差别只在传参的方法/类型上 **
Function.prototype._apply = function ($this, parmsArr) { // 根据原版apply 第二个参数传入的是一个数组
$this['caller'] = this
$this['caller'](...parmsArr) // ...parmsArr此时是spread运算符, 用于将数组中的元素解构出来给caller函数传入实参
delete $this['caller']
}

既然_call与_apply之前的相似度(耦合度)这么高, 那我们可以进一步对它们(的相同代码)进行抽离

function interface4CallAndApply(caller, $this, parmsOrParmArr) {
$this['caller'] = caller
$this['caller'](...parmsOrParmArr)
delete $this['caller']
} Function.prototype._call = function ($this, ...parms) {
var funcCaller = this
interface4CallAndApply(funcCaller, $this, parms)
} Function.prototype._apply = function ($this, parmsArr) {
var funcCaller = this
interface4CallAndApply(funcCaller, $this, parmsArr)
}

一个我认为能够较好展示_call 和 _apply实现原理的例子

var myName = 'window'
var obj = {
myName: 'Fitz',
sayName() {
console.log(this.myName)
}
} var foo = obj.sayName var bar = {
myName: 'bar',
foo
} bar.foo()

模拟实现bind

// 使用硬绑定原理模拟实现bind
Function.prototype._bind = function ($this, ...parms) {
$bindCaller = this // 保存调用_bind函数的对象 注意: 该对象是个函数
// 根据原生bind函数的返回值: 是一个函数
return function () { // 用rest运算符替代arguments去收集传入的实参
return $bindCaller._apply($this, parms)
}
}

一个能够展现硬绑定原理的例子

function hardBind(fn) {
var caller = this
var parms = [].slice.call(arguments, 1)
return function bound() {
parms = [...parms, ...arguments]
fn.apply(caller, parms) // apply可以接受一个伪数组而不必一定是数组
}
} var myName = 'window'
function foo() {
console.log(this.myName)
}
var obj = {
myName: 'obj',
foo: foo,
hardBind: hardBind
} // 正常情况下
foo() // 'window'
obj.foo() // 'obj' var hb = hardBind(foo)
// 可以看到一旦硬绑定后无论最终怎么调用都不能改变this指向
hb() // 'window'
obj.hb = hb // 给obj添加该方法用于测试
obj.hb() // 'window' // 在加深一下印象
var hb2 = obj.hardBind(foo)
hb2() // 'obj' // 这里调用this本该指向window

总体实现(纯净版/没有注释)

function interface4CallAndApply(caller, $this, parmsOrParmArr) {
$this['caller'] = caller
$this['caller'](...parmsOrParmArr)
delete $this['caller']
} Function.prototype._call = function ($this, ...parms) {
var funcCaller = this
interface4CallAndApply(funcCaller, $this, parms)
} Function.prototype._apply = function ($this, parmsArr) {
var funcCaller = this
interface4CallAndApply(funcCaller, $this, parmsArr)
} Function.prototype._bind = function ($this, ...parms) {
$bindCaller = this
return function () {
return $bindCaller._apply($this, parms)
}
} // ============ 测试 ===============
var foo = {
name: 'foo',
sayHello: function (a, b) {
console.log(`hello, get the parms => ${a} and ${b}`)
}
} var bar = {
name: 'bar'
} foo.sayHello._call(bar, 'Fitz', 'smart')
foo.sayHello._apply(bar, ['Fitz', 'smart']) var baz = foo.sayHello._bind(bar, 'Fitz', 'smart')
baz() var testHardBind = foo.sayHello._bind(bar, 'hard', 'bind')
testHardBind._call(Object.create(null)) // hello, get the parms => hard and bind 测试_bind的硬绑定

写在最后

我只是一个正在学习前端的小白,有不对的地方请各位多多指正

如果感觉对您有启发或者帮助,也烦请您留言或给我个关注, 谢谢啦!

简单模拟实现javascript中的call、apply、bind方法的更多相关文章

  1. javascript中的call(),apply(),bind()方法的区别

    之前一直迷惑,记不住call(),apply(),bind()的区别.不知道如何使用,一直处于懵懂的状态.直到有一天面试被问到了这三个方法的区别,所以觉得很有必要总结一下. 如果有不全面的地方,后续再 ...

  2. 深入浅出:了解JavaScript中的call,apply,bind的差别

     在 javascript之 this 关键字详解文章中,谈及了如下内容,做一个简单的回顾:         1.this对象的涵义就是指向当前对象中的属性和方法.       2.this指向的可变 ...

  3. 别真以为JavaScript中func.call/apply/bind是万能的!

    自从学会call/apply/bind这三个方法后我就各种场合各种使用各种得心应手至今还没踩过什么坑,怎么用?说直白点就是我自己的对象没有某个方法但别人有,我就可以通过call/apply/bind去 ...

  4. 浅析 JavaScript 中的 Function.prototype.bind() 方法

    Function.prototype.bind()方法 bind() 方法的主要作用就是将函数绑定至某个对象,bind() 方法会创建一个函数,函数体内this对象的值会被绑定到传入bind() 函数 ...

  5. JavaScript 中的 Function.prototype.bind() 方法

    转载自:https://www.cnblogs.com/zztt/p/4122352.html Function.prototype.bind()方法 bind() 方法的主要作用就是将函数绑定至某个 ...

  6. Javascript中this作用域以及bind方法的重写

    这是一个最近遇到的笔试题,出于尊重,不会说出该公司的名字,源于自身比较少,笔试题是将bind方法用ES3重写,使用bind这个方法,导致一时半会懵了,只记得bind可以改变this的作用域. 作为查漏 ...

  7. Javascript中call,apply,bind方法的详解与总结

    在 javascript之 this 关键字详解 文章中,谈及了如下内容,做一个简单的回顾: 1.this对象的涵义就是指向当前对象中的属性和方法. 2.this指向的可变性.当在全局作用域时,thi ...

  8. Javascript中call,apply,bind的区别

    一.探索call方法原理 Function.prototype.call = function(obj) { // 1.让fn中的this指向obj // eval(this.toString().r ...

  9. JavaScript中call,apply,bind方法的区别

    call,apply,bind方法一般用来指定this的环境. var a = { user:"hahaha", fn:function(){ console.log(this.u ...

随机推荐

  1. 百万SPC即将空投,3.0公链NGK有多“豪横”?

    在1月2日晚间,比特币强势突破3万美金,随后还在一路上涨,现在价格33431.64美金.仅用了不到一个月的时间,比特币就从2万美金涨到了3万美金,这充分展示了市场对于数字货币的强烈信心.没有了天花板的 ...

  2. spring boot用ModelAndView向Thymeleaf模板传参数

    最近在调试一个Spring Boot向Thymeleaf模板传参数的例子,但踩了很多坑,这里就把详细过程记录下来,以供大家参考. 先说下,这里遇到哪些坑呢? 1 我用的是IDEA社区版,这不支持JSP ...

  3. 图像分割 | Context Prior CPNet | CVPR2020

    文章转自微信公众号:「机器学习炼丹术」 文章作者:炼丹兄(已授权) 作者联系方式:cyx645016617 论文名称:"Context Prior for Scene Segmentatio ...

  4. 【HTB系列】靶机Teacher的渗透测试详解

    出品|MS08067实验室(www.ms08067.com) 本文作者:大方子(Ms08067实验室核心成员) Kali: 10.10.14.50 靶机地址:10.10.10.153 先用nmap 对 ...

  5. Docker SDK for Python

    一.概述 Docker引擎API的Python库.它允许您执行docker命令所做的任何操作,但可以在Python应用程序中运行容器.管理容器.管理群集等. 官方文档: https://docker- ...

  6. Django中文文档-模型Models(二):Meta选项、模型属性、模型方法

    元数据(Meta)选项 使用内部的class Meta 定义模型的元数据,例如: from django.db import models class Ox(models.Model): horn_l ...

  7. MHA架构搭建中遇到的问题

    1. 两个包:mha4mysql-manager-0.56-0.el6.noarch.rpm 和 mha4mysql-node-0.56-0.el6.norch.rpm 地址:https://code ...

  8. ss_port_change - 一键修改ss配置与Centos7的Firewall策略脚本

    ss_port_change 修改ss配置与Centos7的Firewall策略脚本 注意是否需要修改config路径与ss服务的名 脚本的敏感字用了*代替 项目地址 Github 脚本 #!/bin ...

  9. 使用windbg定位内存问题【入门级】

    1. 背景 在开发过程中,我们可能遇到应用程序线程占用过大的问题,可以通过windbg命令去定位哪些类型,哪些内存一直占用堆资源,从而查出问题,解决问题. 2. 准备工作 工具: 抓取DUMP文件的工 ...

  10. HDOJ-6628(dfs+第k字典序最小差异序列)

    permutation 1 HDOJ-6628 这题使用的暴力深搜,在dfs里面直接从最小的差异开始枚举 注意这里的pre记录前一个数,并且最后答案需要减去排列中最小的数再加一 这里有一个技巧关于求第 ...