Cet article utilise des exemples de code pour vous donner une analyse approfondie de la façon d'implémenter call, apply et bind Quant à l'utilisation spécifique de ces méthodes, MDN ou les articles du site les ont déjà décrits clairement, donc je ne le ferai pas. Je n’entrerai pas dans les détails ici.
Version ES3
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 }
Version ES6
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 }
Lors de l'appel d'une fonction via call
, vous pouvez le spécifier dans la fonction via le thisArg transmis à call
. Et cela peut être réalisé tant que la fonction est appelée via thisArg, ce qui est notre objectif principal.
Points de mise en œuvre
est finalement appelé via une fonction, donc myCall
est monté sur le prototype de fonction comme myCall
. En même temps, c'est précisément parce que call
est appelé via une fonction, donc à l'intérieur de myCall
nous pouvons obtenir l'appelant de myCall
grâce à cela, qui est la fonction qui est réellement exécutée.
myCall
Il va de soi que est monté sur le prototype de fonction Lorsque nous appelons myCall
via une non-fonction, une erreur sera certainement générée, alors pourquoi devons-nous la vérifier. dans myCall
Tapez l'appelant et personnalisez une erreur ? En effet, lorsqu'un appelant myCall
est un objet mais hérite de obj = {}
(Function
), il peut en fait appeler la méthode obj.__proto__ = Function.prototype
comme une non-fonction. À ce stade, si aucune vérification de type n'est effectuée pour garantir qu'il s'agit d'une fonction, alors. lorsqu'il est appelé directement en tant que fonction ultérieurement, une erreur sera générée.
myCall
est nul ou indéfini, alors thisArg pointera en fait vers l'objet global ; un type de base, vous pouvez utiliser pour effectuer une opération de boxe et la convertir en objet - principalement pour garantir que la fonction peut être exécutée ultérieurement par un appel de méthode. Alors, peut-on écrire call
? En fait, ce n'est pas possible. Si thisArg est une valeur booléenne fausse, cela fera que thisArg finira par être égal à globalThis, mais en fait il devrait être égal à Object()
.
thisArg = thisArg ? Object(thisArg) : globalThis
Comme mentionné précédemment, vous pouvez obtenir la fonction réellement exécutée via ceci dans Boolean {false}
, donc équivaut à utiliser cette fonction comme méthode de thisArg, et plus tard nous pourrons utiliser l'objet thisArg Go appelez cette fonction.
myCall
thisArg.fn = this
équivaut à ajouter un attribut fn à thisArg, cet attribut doit donc être supprimé avant de renvoyer le résultat de l'exécution. De plus, afin d'éviter d'écraser la propriété fn du même nom qui peut exister sur thisArg, vous pouvez également utiliser pour construire une propriété unique puis thisArg.fn = this
.
const fn = Symbol('fn')
La principale différence entre la version ES3 et la version ES6 est le passage des paramètres et l'exécution des fonctions :thisArg[fn] = this
ES6 En raison de l'introduction des paramètres restants, quel que soit le nombre de paramètres transmis lorsque la fonction est réellement exécutée, ces paramètres peuvent être obtenus via le tableau args. En même temps, en raison de l'introduction de l'opérateur d'expansion, le tableau de paramètres args peut être étendu et les paramètres sont transmis à la fonction pour exécution un par unMais il n'existe pas de paramètres restants dans ES3, Ainsi, lors de la définition de
, il ne reçoit qu'un seul paramètre thisArg, puis transmet le tableau de classes d'arguments dans le corps de la fonction Obtenir tous les paramètres. Ce dont nous avons besoin, ce sont tous les éléments des arguments sauf le premier élément (thisArg). Comment faire cela ? Si c'était ES6, juste
serait bien, mais c'est ES3, nous ne pouvons donc parcourir que les arguments à partir de l'index 1, puis les pousser dans un tableau args. Il convient également de noter que les paramètres poussés ici se présentent sous la forme de chaînes. Ceci vise principalement à faciliter la transmission des paramètres à la fonction un par un lors de l'exécution ultérieure de la fonction via eval.
myCall
Pourquoi faut-il utiliser eval pour exécuter une fonction ? Parce que nous ne savons pas combien de paramètres la fonction reçoit réellement et que nous ne pouvons pas utiliser l'opérateur d'expansion, nous pouvons uniquement construire une expression de chaîne exécutable et transmettre explicitement tous les paramètres de la fonction. [...arguments].slice(1)
apply utilisation et appel Très similaire, donc la mise en œuvre est également très similaire. La différence à noter est qu'après que l'appel ait accepté un paramètre thisArg, il peut également recevoir plusieurs paramètres (c'est-à-dire qu'il accepte une liste de paramètres), et après l'application, il reçoit un paramètre thisArg, généralement le deuxième paramètre est un tableau ou un tableau. -objet similaire : fn.call(thisArg,arg1,arg2,...) fn.apply(thisArg,[arg1,arg2,...])
如果第二个参数传的是 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 }
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 }
实现要点
基本上和 call 的实现是差不多的,只是我们需要检查第二个参数的类型。
bind
也可以像 call
和 apply
那样给函数绑定一个 this,但是有一些不同的要点需要注意:
bind
不是指定完 this 之后直接调用原函数,而是基于原函数返回一个内部完成了 this 绑定的新函数bind
的时候作为第二个参数传入,第二批可以在调用新函数的时候传入,这两批参数最终会合并在一起,一次传递给新函数去执行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 }
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]) } } }
实现要点
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
这样只是拷贝了原型引用,如果修改 fnBound.prototype
,则会影响到 fnToBind.prototype
,所以不能用这种方法
// this 指的是 fnToBind fnBound.prototype = Object.create(this.prototype)
通过 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()
这是上面代码采用的方法:通过空构造函数 Fn 在 fnToBind 和 fnBound 之间建立了一个联系。如果要通过实例去访问 fnToBind 的原型上面的属性,可以沿着如下原型链查找:
实例 => 实例.__proto__ = fnBound.prototype = new Fn() => new Fn().__proto__ = Fn.prototype = fnToBind.prototype
更多编程相关知识,请访问:编程教学!!
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!