La colonne
Contrairement aux étudiants des autres "cercles", les étudiants du cercle front-end sont très friands de la "méthode xxx manuscrite" et passent essentiellement chaque jour dans les Nuggets Vous pouvez voir des articles similaires. Cependant, de nombreux articles (non représentatifs de tous, sans vouloir offenser) avalent pour la plupart la vérité et copient ce qui a précédé, ce qui ne peut résister à un examen minutieux et à la recherche, et peut facilement induire en erreur les nouveaux étudiants qui débutent avec JavaScript.
Dans cette optique, cet article s'appuiera sur quelques points de connaissances typiques du "JavaScript You Don't Know" (Petit Livre Jaune), combinés à quelques méthodes "manuscrites" classiques et à haute fréquence pour expliquer les principes et les principes un par un. La mise en œuvre est combinée et les étudiants travaillent ensemble pour comprendre les principes avant d'écrire le code à la main.
Avant de l'expliquer, nous devons d'abord clarifier un malentendu très courant sur les fonctions et les objets en JavaScript :
Dans le langage traditionnel orienté classe. , les "constructeurs" sont des méthodes spéciales dans une classe Lorsque vous utilisez new
pour initialiser une classe, le constructeur de la classe sera appelé. La forme habituelle est la suivante :
something = new MyClass(..);复制代码
JavaScript a également un opérateur new
, et la méthode d'utilisation est la même que celle des langages orientés classes. La plupart des développeurs pensent au mécanisme new
en JavaScript. la même chose que ces langues. Cependant, le mécanisme de new
en JavaScript est en réalité complètement différent de celui des langages orientés classes.
Tout d’abord, redéfinissons le « constructeur » en JavaScript. En JavaScript, les constructeurs ne sont que des fonctions appelées lors de l'utilisation de l'opérateur new
. Ils n’appartiennent pas à une classe et n’instancient pas non plus une classe. En fait, il ne s'agit même pas d'un type de fonction spécial, ce sont simplement des fonctions ordinaires appelées par l'opérateur new
.
En fait, il n'y a pas de soi-disant "constructeur", seulement un "appel de constructeur" pour les fonctions.
Utilisez new
pour appeler une fonction, ou lorsqu'un appel de constructeur se produit, les opérations suivantes seront automatiquement effectuées :
Par conséquent, si nous voulons écrire un new
théorique, nous devons suivre strictement les étapes ci-dessus et l'implémenter dans le code :
/** * @param {fn} Function(any) 构造函数 * @param {arg1, arg2, ...} 指定的参数列表 */ function myNew (fn, ...args) { // 创建一个新对象,并把它的原型链(__proto__)指向构造函数的原型对象 const instance = Object.create(fn.prototype) // 把新对象作为thisArgs和参数列表一起使用call或apply调用构造函数 const result = fn.apply(instance, args) 如果构造函数的执行结果返回了对象类型的数据(排除null),则返回该对象,否则返新对象 return (result && typeof instance === 'object') ? result : instance } 复制代码
Exemple Dans le code , nous utilisons
Object.create(fn.prototype)
pour créer un objet vide afin que sa chaîne de prototypes__proto__
pointe vers l'objet prototypefn.prototype
du constructeur. Plus tard, nous écrirons également à la main une méthodeObject.create()
pour comprendre comment cela se fait. .
Pendant longtemps, JavaScript n'avait que quelques éléments de syntaxe similaires, tels que new
et instanceof
, mais plus tard, il y a de nouveaux éléments dans ES6, comme le mot-clé class
.
Sans considérer class
, la relation entre new
et instanceof
est "ambiguë". Le but principal des opérateurs new
et instanceof
apparaît est de se rapprocher de la « programmation orientée objet ».
Donc, maintenant qu'on comprend new
, il n'y a aucune raison de ne pas comprendre instanceof
. Citant la description de instanceof
sur MDN : "L'opérateur instanceof
est utilisé pour détecter si l'attribut prototype
du constructeur apparaît sur la chaîne de prototypes d'un objet instance."
Après avoir vu cela, je comprends fondamentalement que la mise en œuvre de instanceof
doit tester votre compréhension de la chaîne de prototypes et de prototype
. Le contenu sur les prototypes et les chaînes de prototypes en JavaScript nécessite beaucoup de contenu pour être expliqué clairement, et il existe également de bons articles de blog récapitulatifs sur Internet, dont un qui vous aidera à bien comprendre le prototype, __proto__ et le constructeur (illustration) en JS. Un article rare et excellent qui passe au peigne fin et résume clairement les relations et les liens qui les unissent.
Partie 2 - Chapitre 5 de « JavaScript que vous ne connaissez pas » fournit une introduction plus basique et complète au contenu lié aux prototypes, qui mérite d'être lu.
La mise en œuvre du instanceof
code suivant, bien que très simple, doit être basée sur votre compréhension des prototypes et des chaînes de prototypes. Il est recommandé de comprendre d'abord les articles de blog ou les articles ci-dessus avant. continue.
/** * @param {left} Object 实例对象 * @param {right} Function 构造函数 */ function myInstanceof (left, right) { // 保证运算符右侧是一个构造函数 if (typeof right !== 'function') { throw new Error('运算符右侧必须是一个构造函数') return } // 如果运算符左侧是一个null或者基本数据类型的值,直接返回false if (left === null || !['function', 'object'].includes(typeof left)) { return false } // 只要该构造函数的原型对象出现在实例对象的原型链上,则返回true,否则返回false let proto = Object.getPrototypeOf(left) while (true) { // 遍历完了目标对象的原型链都没找到那就是没有,即到了Object.prototype if (proto === null) return false // 找到了 if (proto === right.prototype) return true // 沿着原型链继续向上找 proto = Object.getPrototypeOf(proto) } }复制代码
Object.create()
crée un nouvel objet et utilise l'objet existant pour fournir le __proto__
de l'objet nouvellement créé.
在《你不知道的JavaScript》中,多次用到了Object.create()
这个方法去模仿传统面向对象编程中的“继承”,其中也包括上面讲到了new
操作符的实现过程。在MDN中对它的介绍也很简短,主要内容大都在描述可选参数propertiesObject
的用法。
简单起见,为了和new
、instanceof
的知识串联起来,我们只着重关注Object.create()
的第一个参数proto
,咱不讨论propertiesObject
的实现和具体特性。
/** * 基础版本 * @param {Object} proto * */ Object.prototype.create = function (proto) { // 利用new操作符的特性:创建一个对象,其原型链(__proto__)指向构造函数的原型对象 function F () {} F.prototype = proto return new F() } /** * 改良版本 * @param {Object} proto * */ Object.prototype.createX = function (proto) { const obj = {} // 一步到位,把一个空对象的原型链(__proto__)指向指定原型对象即可 Object.setPrototypeOf(obj, proto) return obj }复制代码
我们可以看到,Object.create(proto)
做的事情转换成其他方法实现很简单,就是创建一个空对象,再把这个对象的原型链属性(Object.setPrototype(obj, proto)
)指向指定的原型对象proto
就可以了(不要采用直接赋值__proto__
属性的方式,因为每个浏览器的实现不尽相同,而且在规范中也没有明确该属性名)。
作为最经典的手写“劳模”们,call
、apply
和bind
已经被手写了无数遍。也许本文中手写的版本是无数个前辈们写过的某个版本,但是有一点不同的是,本文会告诉你为什么要这样写,让你搞懂了再写。
在《你不知道的JavaScript上卷》第二部分的第1章和第2章,用了2章斤30页的篇幅中详细地介绍了this
的内容,已经充分说明了this
的重要性和应用场景的复杂性。
而我们要实现的call
、apply
和bind
最为人所知的功能就是使用指定的thisArg
去调用函数,使得函数可以使用我们指定的thisArg
作为它运行时的上下文。
《你不知道的JavaScript》总结了四条规则来判断一个运行中函数的this
到底是绑定到哪里:
new
调用?绑定到新创建的对象。call
或者 apply
(或者 bind
)调用?绑定到指定的对象。undefined
,否则绑定到全局对象。更具体一点,可以描述为:
new
中调用( new
绑定)?如果是的话 this
绑定的是新创建的对象:var bar = new foo()复制代码
call
、 apply
(显式绑定)或者硬绑定(bind
)调用?如果是的话, this
绑定的是指定的对象:var bar = foo.call(obj2)复制代码
this
绑定的是那个上下文对象:var bar = obj1.foo()复制代码
undefined
,否则绑定到全局对象:var bar = foo()复制代码
就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白 this
的绑定原理了。
至此,你已经搞明白了this
的全部绑定规则,而我们要去手写实现的call
、apply
和bind
只是其中的一条规则(第2条),因此,我们可以在另外3条规则的基础上很容易地组织代码实现。
实现call
和apply
的通常做法是使用“隐式绑定”的规则,只需要绑定thisArg
对象到指定的对象就好了,即:使得函数可以在指定的上下文对象中调用:
const context = { name: 'ZhangSan' } function sayName () { console.log(this.name) } context.sayName = sayName context.sayName() // ZhangSan复制代码
这样,我们就完成了“隐式绑定”。落实到具体的代码实现上:
/** * @param {context} Object * @param {arg1, arg2, ...} 指定的参数列表 */ Function.prototype.call = function (context, ...args) { // 指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装 if (context === null || context === undefined) { context = window } else if (typeof context !== 'object') { context = new context.constructor(context) } else { context = context } const func = this const fn = Symbol('fn') context[fn] = func const result = context[fn](...args) delete context[fn] return result } /** * @param {context} * @param {args} Array 参数数组 */ Function.prototype.apply = function (context, args) { // 和call一样的原理 if (context === null || context === undefined) { context = window } else if (typeof context !== 'object') { context = new context.constructor(context) } else { context = context } const fn = Symbol('fn') const func = this context[fn] = func const result = context[fn](...args) delete context[fn] return result }复制代码
细看下来,大家都那么聪明,肯定一眼就看到了它们的精髓所在:
const fn = Symbol('fn') const func = this context[fn] = func复制代码
在这里,我们使用Symbol('fn')
作为上下文对象的键,对应的值指向我们想要绑定上下文的函数this
(因为我们的方法是声明在Function.prototype
上的),而使用Symbol(fn)
作为键名是为了避免和上下文对象的其他键名冲突,从而导致覆盖了原有的属性键值对。
在《你不知道的JavaScript》中,手动实现了一个简单版本的bind
函数,它称之为“硬绑定”:
function bind(fn, obj) { return function() { return fn.apply( obj, arguments ); }; }复制代码
硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值。
由于硬绑定是一种非常常用的模式,所以在 ES5 中提供了内置的方法 Function.prototype.bind
,它的用法如下:
function foo(something) { console.log( this.a, something ) return this.a + something; } var obj = { a:2 } var bar = foo.bind( obj ) var b = bar( 3 ); // 2 3 console.log( b ); // 5复制代码
bind(..)
会返回一个硬编码的新函数,它会把参数设置为 this
的上下文并调用原始函数。
MDN是这样描述
bind
方法的:bind()
方法创建一个新的函数,在bind()
被调用时,这个新函数的this
被指定为bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
因此,我们可以在此基础上实现我们的bind
方法:
/** * @param {context} Object 指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装 * * @param {arg1, arg2, ...} 指定的参数列表 * * 如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg */ Function.prototype.bind = function (context, ...args) { if (typeof this !== 'function') { throw new TypeError('必须使用函数调用此方法'); } const _self = this // fNOP存在的意义: // 1. 判断返回的fBound是否被new调用了,如果是被new调用了,那么fNOP.prototype自然是fBound()中this的原型 // 2. 使用包装函数(_self)的原型对象覆盖自身的原型对象,然后使用new操作符构造出一个实例对象作为fBound的原型对象,从而实现继承包装函数的原型对象 const fNOP = function () {} const fBound = function (...args2) { // fNOP.prototype.isPrototypeOf(this) 为true说明当前结果是被使用new操作符调用的,则忽略context return _self.apply(fNOP.prototype.isPrototypeOf(this) && context ? this : context, [...args, ...args2]) } // 绑定原型对象 fNOP.prototype = this.prototype fBound.prototype = new fNOP() return fBound }复制代码
具体的实现细节都标注了对应的注释,涉及到的原理都有在上面的内容中讲到,也算是一个总结和回顾吧。
维基百科:柯里化,英语:Currying,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术 。
看这个解释有一点抽象,我们就拿被做了无数次示例的add
函数,来做一个简单的实现:
// 普通的add函数 function add(x, y) { return x + y } // Currying后 function curryingAdd(x) { return function (y) { return x + y } } add(1, 2) // 3 curryingAdd(1)(2) // 3复制代码
实际上就是把add
函数的x
,y
两个参数变成了先用一个函数接收x
然后返回一个函数去处理y
参数。现在思路应该就比较清晰了,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
函数柯里化在一定场景下,有很多好处,如:参数复用、提前确认和延迟运行等,具体内容可以拜读下这篇文章,个人觉得受益匪浅。
最简单的实现函数柯里化的方式就是使用Function.prototype.bind
,即:
function curry(fn, ...args) { return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args); }复制代码
如果用ES5代码实现的话,会比较麻烦些,但是核心思想是不变的,就是在传递的参数满足调用函数之前始终返回一个需要传参剩余参数的函数:
// 函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。 function curry(fn, args) { args = args || [] // 获取函数需要的参数长度 let length = fn.length return function() { let subArgs = args.slice(0) // 拼接得到现有的所有参数 for (let i = 0; i < arguments.length; i++) { subArgs.push(arguments[i]) } // 判断参数的长度是否已经满足函数所需参数的长度 if (subArgs.length >= length) { // 如果满足,执行函数 return fn.apply(this, subArgs) } else { // 如果不满足,递归返回科里化的函数,等待参数的传入 return curry.call(this, fn, subArgs) } }; }复制代码
在本文中,我们熟悉了new
的底层执行原理、instanceof
和原型链直接的密切关系、Object.create()
是如何实现的原型链指定以及JavaScript中最复杂最难搞定的this
的绑定问题。
当然,在《你不知道的JavaScript》中还有很多精妙的见解和知识内容,如"JavaScript是需要先编译
再执行的"、"面相对象编程只是JavaScript中的一种设计模式"等等独到的见解和观点。墙裂推荐大家多读几遍,每一遍都会有不同的收获。
相关免费学习推荐:JavaScript(视频)
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!