Maison > interface Web > js tutoriel > Cet article vous donnera une compréhension approfondie de la mise en œuvre des méthodes call, apply et bind.

Cet article vous donnera une compréhension approfondie de la mise en œuvre des méthodes call, apply et bind.

青灯夜游
Libérer: 2021-07-12 18:04:30
avant
2090 Les gens l'ont consulté

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.

Cet article vous donnera une compréhension approfondie de la mise en œuvre des méthodes call, apply et bind.

Implémentation manuscrite de l'appel

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(&#39;arguments[&#39; + i + &#39;]&#39;)
    }
    thisArg.fn = this
    var res = eval(&#39;thisArg.fn(&#39; + args + &#39;)&#39;)
    delete thisArg.fn
    return res
}
Copier après la connexion

Version ES6

Function.prototype.myCall = function(thisArg,...args){
    if(typeof this != &#39;function&#39;){
        throw new Error(&#39;The caller must be a function&#39;)
    }
    if(thisArg === undefined || thisArg === null){
        thisArg = globalThis
    } else {
        thisArg = Object(thisArg)
    }
    thisArg.fn = this
    const res = thisArg.fn(...args)
    delete thisArg.fn
    return res
}
Copier après la connexion

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.

  • myCallIl 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.

  • Si thisArg passé à 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) : globalThisComme 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.

  • myCallthisArg.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(&#39;fn&#39;)La principale différence entre la version ES3 et la version ES6 est le passage des paramètres et l'exécution des fonctions :

      < li>thisArg[fn] = thisES6 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 un

    • Mais 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.

    • myCallPourquoi 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.

Implémentation de l'écriture manuscrite de apply

[...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,...])
Copier après la connexion

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

ES3 版本

Function.prototype.myApply = function(thisArg,args){
    if(typeof this != &#39;function&#39;){
        throw new Error(&#39;the caller must be a function&#39;)
    } 
    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(&#39;CreateListFromArrayLike called on non-object&#39;)
    }
    var _args = []
    for(var i = 0;i < args.length;i ++){
        _args.push(&#39;args[&#39; + i + &#39;]&#39;)
    }
    thisArg.fn = this
    var res = _args.length ? eval(&#39;thisArg.fn(&#39; + _args + &#39;)&#39;):thisArg.fn()
    delete thisArg.fn
    return res
}
Copier après la connexion

ES6 版本

Function.prototype.myApply = function(thisArg,args){
    if(typeof thisArg != &#39;function&#39;){
        throw new Error(&#39;the caller must be a function&#39;)
    } 
    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(&#39;CreateListFromArrayLike called on non-object&#39;)
    }
    thisArg.fn = this
    const res = thisArg.fn(...args)
    delete thisArg.fn
    return res
}
Copier après la connexion

实现要点

基本上和 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 != &#39;function&#39;){
        throw new Error(&#39;the caller must be a function&#39;)
    }
    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
}
Copier après la connexion

ES6 版本

Function.prototype.myBind = function(thisArg,...args1){
    if(typeof this != &#39;function&#39;){
        throw new Error(&#39;the caller must be a function&#39;)
    }
    const fnToBind = this
    return function fnBound(...args2){
        // 如果是通过 new 调用的
        if(this instanceof fnBound){
            return new fnToBind(...args1,...args2)
        } else {
            return fnToBind.apply(thisArg,[...args1,...args2])
        }
    }
}
Copier après la connexion

实现要点

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
Copier après la connexion

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

// this 指的是 fnToBind
fnBound.prototype = Object.create(this.prototype)
Copier après la connexion

通过 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()
Copier après la connexion

这是上面代码采用的方法:通过空构造函数 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!

Étiquettes associées:
source:segmentfault.com
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal