This article will give you an in-depth understanding of the implementation of call, apply and bind methods

青灯夜游
Release: 2021-07-12 18:04:30
forward
2005 people have browsed it

This article uses code examples to give you an in-depth analysis of how to implement call, apply and bind. As for the specific usage of these methods, MDN or the articles on the site have already described them clearly, so I will not go into details here.

This article will give you an in-depth understanding of the implementation of call, apply and bind methods

Handwritten implementation of call

ES3 version

Function.prototype.myCall = function(thisArg){ if(typeof this != 'function'){ throw new Error('The caller must be a function') } if(thisArg === undefined || thisArg === null){ thisArg = globalThis } else { thisArg = Object(thisArg) } var args = [] for(var i = 1;i < arguments.length;i ++){ args.push('arguments[' + i + ']') } thisArg.fn = this var res = eval('thisArg.fn(' + args + ')') delete thisArg.fn return res }
Copy after login

ES6 version

Function.prototype.myCall = function(thisArg,...args){ if(typeof this != 'function'){ throw new Error('The caller must be a function') } if(thisArg === undefined || thisArg === null){ thisArg = globalThis } else { thisArg = Object(thisArg) } thisArg.fn = this const res = thisArg.fn(...args) delete thisArg.fn return res }
Copy after login

When calling a function throughcall, you can specify this in the function by passing thisArg tocall. And this can be achieved as long as the function is called through thisArg, which is our main goal.

Implementation points

  • In the end,myCallis called through a function, somyCallis mounted on the function prototype likecall. At the same time, it is precisely becausemyCallis called through a function, so insidemyCallwe can get the caller ofmyCallthrough this, which is the actual execution that function.

  • It stands to reason thatmyCallis mounted on the function prototype. When we callmyCallthrough a non-function, it will definitely Throws an error, so why check the caller's type inmyCalland customize an error? This is because when a callerobj = {}is an object but inherits fromFunction(obj.__proto__ = Function.prototype), As a non-function, it can actually call themyCallmethod. At this time, if there is no type check to ensure that it is a function, then when it is called directly as a function later, an error will be thrown.

  • If thisArg passed tocallis null or undefined, then thisArg will actually point to the global object; if thisArg is a basic type, you can useObject()Perform a boxing operation and convert it into an object - mainly to ensure that the function can be executed later by method calling. So can it be written asthisArg = thisArg ? Object(thisArg) : globalThis? In fact, it is not possible. If thisArg is a Boolean value false, it will cause thisArg to eventually be equal to globalThis, but in fact it should be equal toBoolean {false}.

  • As mentioned before, you can get the actually executed function through this inmyCall, sothisArg.fn = thisis equivalent to Use this function as a method of thisArg, and then we can call this function through the thisArg object.

  • thisArg.fn = thisis equivalent to adding an fn attribute to thisArg, so this attribute must be deleted before returning the execution result. In addition, in order to avoid overwriting the property fn with the same name that may exist on thisArg, you can also useconst fn = Symbol('fn')to construct a unique property, and thenthisArg[fn] = this.

  • The main difference between the ES3 version and the ES6 version lies in the passing of parameters and the execution of functions:

    • ES6 introduces remaining parameters, so No matter how many parameters are passed in when the function is actually executed, these parameters can be obtained through the args array. At the same time, because of the introduction of the expansion operator, the args parameter array can be expanded and the parameters are passed to the function one by one for execution

    • But there is no such thing as remaining parameters in ES3, so when definingmyCall, it only receives one thisArg parameter, and then gets all the parameters through the arguments class array in the function body. What we need is all the elements in arguments except the first element (thisArg). How to do this? If it is ES6, just[...arguments].slice(1), but this is ES3, so we can only traverse the arguments starting from index 1, and then push them into an args array. . Also note that what is pushed here are parameters in the form of strings. This is mainly to facilitate passing the parameters to the function one by one when executing the function through eval later.

    • Why must eval be used to execute a function? Because we don't know how many parameters the function actually needs to receive, and we can't use the expansion operator, we can only construct an executable string expression and explicitly pass in all the parameters of the function.

Handwritten implementation apply

The usage of apply is very similar to call, so the implementation is also very similar. The difference that needs to be noted is that after call accepts a thisArg parameter, it can also receive multiple parameters (that is, it accepts a parameter list), and after apply receives a thisArg parameter, usually the second parameter is an array or array-like object:

fn.call(thisArg,arg1,arg2,...) fn.apply(thisArg,[arg1,arg2,...])
Copy after login

如果第二个参数传的是 null 或者 undefined,那么相当于是整体只传了 thisArg 参数。

ES3 版本

Function.prototype.myApply = function(thisArg,args){ if(typeof this != 'function'){ throw new Error('the caller must be a function') } if(thisArg === null || thisArg === undefined){ thisArg = globalThis } else { thisArg = Object(thisArg) } if(args === null || args === undefined){ args = [] } else if(!Array.isArray(args)){ throw new Error('CreateListFromArrayLike called on non-object') } var _args = [] for(var i = 0;i < args.length;i ++){ _args.push('args[' + i + ']') } thisArg.fn = this var res = _args.length ? eval('thisArg.fn(' + _args + ')'):thisArg.fn() delete thisArg.fn return res }
Copy after login

ES6 版本

Function.prototype.myApply = function(thisArg,args){ if(typeof thisArg != 'function'){ throw new Error('the caller must be a function') } if(thisArg === null || thisArg === undefined){ thisArg = globalThis } else { thisArg = Object(thisArg) } if(args === null || args === undefined){ args = [] } // 如果传入的不是数组,仿照 apply 抛出错误 else if(!Array.isArray(args)){ throw new Error('CreateListFromArrayLike called on non-object') } thisArg.fn = this const res = thisArg.fn(...args) delete thisArg.fn return res }
Copy after login

实现要点

基本上和 call 的实现是差不多的,只是我们需要检查第二个参数的类型。

手写实现 bind

bind也可以像callapply那样给函数绑定一个 this,但是有一些不同的要点需要注意:

  • bind不是指定完 this 之后直接调用原函数,而是基于原函数返回一个内部完成了 this 绑定的新函数
  • 原函数的参数可以分批次传递,第一批可以在调用bind的时候作为第二个参数传入,第二批可以在调用新函数的时候传入,这两批参数最终会合并在一起,一次传递给新函数去执行
  • 新函数如果是通过 new 方式调用的,那么函数内部的 this 会指向实例,而不是当初调用bind的时候传入的 thisArg。换句话说,这种情况下的bind相当于是无效的

ES3 版本

这个版本更接近 MDN 上的 polyfill 版本。

Function.prototype.myBind = function(thisArg){ if(typeof this != 'function'){ throw new Error('the caller must be a function') } var fnToBind = this var args1 = Array.prototype.slice.call(arguments,1) var fnBound = function(){ // 如果是通过 new 调用 return fnToBind.apply(this instanceof fnBound ? this:thisArg,args1.concat(args2)) } // 实例继承 var Fn = function(){} Fn.prototype = this.prototype fnBound.prototype = new Fn() return fnBound }
Copy after login

ES6 版本

Function.prototype.myBind = function(thisArg,...args1){ if(typeof this != 'function'){ throw new Error('the caller must be a function') } const fnToBind = this return function fnBound(...args2){ // 如果是通过 new 调用的 if(this instanceof fnBound){ return new fnToBind(...args1,...args2) } else { return fnToBind.apply(thisArg,[...args1,...args2]) } } }
Copy after login

实现要点

1.bind实现内部 this 绑定,需要借助于apply,这里假设我们可以直接使用apply方法

2.先看比较简单的 ES6 版本:

1).参数获取:因为 ES6 可以使用剩余参数,所以很容易就可以获取执行原函数所需要的参数,而且也可以用展开运算符轻松合并数组。

2).调用方式:前面说过,如果返回的新函数 fnBound 是通过 new 调用的,那么其内部的 this 会是 fnBound 构造函数的实例,而不是当初我们指定的 thisArg,因此this instanceof fnBound会返回 true,这种情况下,相当于我们指定的 thisArg 是无效的,new 返回的新函数等价于 new 原来的旧函数,即 new fnBound 等价于 new fnToBind,所以我们返回一个 new fnToBind 即可;反之,如果 fnBound 是普通调用,则通过 apply 完成 thisArg 的绑定,再返回最终结果。从这里可以看出,bind 的 this 绑定,本质上是通过 apply 完成的。

3.再来看比较麻烦一点的 ES3 版本:

1).参数获取:现在我们用不了剩余参数了,所以只能在函数体内部通过 arguments 获取所有参数。对于myBind,我们实际上需要的是除开第一个传入的 thisArg 参数之外的剩余所有参数构成的数组,所以这里可以通过Array.prototype.slice.call借用数组的 slice 方法(arguments 是类数组,无法直接调用 slice),这里的借用有两个目的:一是除去 arguments 中的第一个参数,二是将除去第一个参数之后的 arguments 转化为数组(slice 本身的返回值就是一个数组,这也是类数组转化为数组的一种常用方法)。同样地,返回的新函数 fnBound 后面调用的时候也可能传入参数,再次借用 slice 将 arguments 转化为数组

2).调用方式:同样,这里也要判断 fnBound 是 new 调用还是普通调用。在 ES6 版本的实现中,如果是 new 调用 fnBound,那么直接返回new fnToBind(),这实际上是最简单也最容易理解的方式,我们在访问实例属性的时候,天然就是按照实例 => 实例.__proto__ = fnToBind.prototype这样的原型链来寻找的,可以确保实例成功访问其构造函数 fnToBInd 的原型上面的属性;但在 ES3 的实现中(或者在网上部分 bind 方法的实现中),我们的做法是返回一个fnToBind.apply(this),实际上相当于返回一个 undefined 的函数执行结果,根据 new 的原理,我们没有在构造函数中自定义一个返回对象,因此 new 的结果就是返回实例本身,这点是不受影响的。这个返回语句的问题在于,它的作用仅仅只是确保 fnToBind 中的 this 指向 new fnBound 之后返回的实例,而并没有确保这个实例可以访问 fnToBind 的原型上面的属性。实际上,它确实不能访问,因为它的构造函数是 fnBound 而不是 fnToBind,所以我们要想办法在 fnBound 和 fnToBind 之间建立一个原型链关系。这里有几种我们可能会使用的方法:

// 这里的 this 指的是 fnToBind fnBound.prototype = this.prototype
Copy after login

这样只是拷贝了原型引用,如果修改fnBound.prototype,则会影响到fnToBind.prototype,所以不能用这种方法

// this 指的是 fnToBind fnBound.prototype = Object.create(this.prototype)
Copy after login

通过Object.create可以创建一个__proto__指向this.prototype的实例对象,之后再让fnBound.prototype指向这个对象,则可以在 fnToBind 和 fnBound 之间建立原型关系。但由于Object.create是 ES6 的方法,所以无法在我们的 ES3 代码中使用。

// this 指的是 fnToBind const Fn = function(){} Fn.prototype = this.prototype fnBound.prototype = new Fn()
Copy after login

这是上面代码采用的方法:通过空构造函数 Fn 在 fnToBind 和 fnBound 之间建立了一个联系。如果要通过实例去访问 fnToBind 的原型上面的属性,可以沿着如下原型链查找:

实例 => 实例.__proto__ = fnBound.prototype = new Fn() => new Fn().__proto__ = Fn.prototype = fnToBind.prototype

更多编程相关知识,请访问:编程教学!!

The above is the detailed content of This article will give you an in-depth understanding of the implementation of call, apply and bind methods. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:segmentfault.com
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
About us Disclaimer Sitemap
php.cn:Public welfare online PHP training,Help PHP learners grow quickly!