ホームページ >ウェブフロントエンド >jsチュートリアル >あなたの知らないJavaScript
他の「サークル」の学生とは異なり、フロントエンドサークルの学生は「手書きxxxメソッド」に非常に熱心で、基本的に毎日ナゲッツで過ごすことができます。同様の記事を参照してください。ただし、多くの記事 (すべてを代表しているわけではなく、攻撃を意図したものではありません) は、ほとんどが真実を鵜呑みにし、以前のものをコピーしているため、精査や研究に耐えることができず、JavaScript を始めたばかりの新入生を簡単に誤解させる可能性があります。
これを考慮して、この記事は、『あなたが知らない JavaScript』 (リトル イエロー ブック) のいくつかの典型的な知識ポイントに基づいて、いくつかの古典的で使用頻度の高い「手書き」の方法を組み合わせて説明します。原則と原則を 1 つずつ説明し、実装を組み合わせて、コードを手で書く前に学生が協力して原理を理解します。
説明する前に、まず JavaScript の関数とオブジェクトに関するよくある誤解を明確にする必要があります。 , 「コンストラクター」はクラス内の特別なメソッドです。
new を使用してクラスを初期化すると、クラス内のコンストラクターが呼び出されます。通常の形式は次のようになります: <pre class="brush:php;toolbar:false">something = new MyClass(..);复制代码</pre>
JavaScript にも
演算子があり、使用方法はクラス指向言語と同じように見えます。 ##new の仕組みもそれらの言語と同じです。ただし、JavaScript の
new の仕組みは、実際にはクラス指向言語の仕組みとはまったく異なります。
まず、JavaScript の「コンストラクター」を再定義しましょう。 JavaScript では、コンストラクターは、
new
new 演算子によって呼び出される単なる通常の関数です。
実際には、いわゆる「コンストラクター」はなく、関数には「コンストラクター呼び出し」があるだけです。
new
新しいオブジェクトを作成 (または構築) します。
/** * @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 } 复制代码
サンプル コードでは、
Object.create(fn.prototype)
__proto__newがコンストラクター
長い間、JavaScriptには、fn のプロトタイプ オブジェクトを指すようにします。 .prototype
、後でObject.create()
メソッドも手書きして、その方法を理解します。2. 演算子instanceof
class キーワードなど、後の ES6 ではいくつかの新しい要素が追加されました。
class を考慮しないと、
new
instanceof の関係は「曖昧」になります。
new および
instanceof 演算子の主な目的は、「オブジェクト指向プログラミング」に近づくことです。
したがって、
new を理解した以上、
instanceof
instanceof の説明を引用します: 「
instanceof 演算子は、コンストラクターの
prototype 属性がインスタンス オブジェクトのプロトタイプ チェーンに現れるかどうかを検出するために使用されます。 。」
これを見た後、
instanceof の実装では、プロトタイプ チェーンと
prototype
「あなたが知らない JavaScript」の第 2 部 - 第 5 章では、プロトタイプ関連の内容について、より基本的かつ包括的に紹介されており、読む価値があります。
instanceof3. Object.create()コードの実装は非常に簡単ですが、プロトタイプとプロトタイプ チェーンの理解に基づいている必要があります。最初に上記の内容を読むことをお勧めします。ブログ投稿 または、記事を理解してから続行してください。
/** * @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()
在《你不知道的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(视频)
以上があなたの知らないJavaScriptの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。