JavaScript 核心原理精讲 - 前美团前端技术专家 - 拉勾教育
我们都知道,比较常用的数组方法有 push、pop、slice、map 和 reduce 等。上一讲我带你剖析了 sort 方法以及 V8 源码中关于排序的内容,本讲则会围绕这几个常用方法,并结合 V8 的源代码带你手写这些方法的底层实现。
那么,为了方便你更好地理解本讲的内容,在课程开始前请你先回想一下:
- reduce 方法里面的参数都是什么作用?
- push 和 pop 的底层逻辑是什么样的呢?
带着思考,我们开始今天的学习。
push 方法的底层实现
为了更好地实现 push 的底层方法,你可以先去 ECMA 的官网去查一下关于 push 的基本描述(链接:ECMA 数组的 push 标准),我们看下其英文的描述,如下所示。
When the push method is called with zero or more arguments, the following steps are taken:1. Let O be ? ToObject(this value).2. Let len be ? LengthOfArrayLike(O).3. Let argCount be the number of elements in items.4. If len + argCount > 2^53 - 1, throw a TypeError exception.5. For each element E of items, doa. Perform ? Set(O, ! ToString(F(len)), E, true).b. Set len to len + 1.6. Perform ? Set(O, "length", F(len), true).7. Return F(len).
从上面的描述可以看到边界判断逻辑以及实现的思路,根据这段英文,我们将其转换为容易理解代码,如下所示。
Array.prototype.push = function(...items) {let O = Object(this);let len = this.length >>> 0;let argCount = items.length >>> 0;if (len + argCount > 2 ** 53 - 1) {throw new TypeError("The number of array is over the max value")}for(let i = 0; i < argCount; i++) {O[len + i] = items[i];}let newLength = len + argCount;O.length = newLength;return newLength;}
从上面的代码可以看出,关键点就在于给数组本身循环添加新的元素 item,然后调整数组的长度 length 为最新的长度,即可完成 push 的底层实现。
其中关于长度的部分需要做无符号位移,无符号位移在很多源码中你都会看到。关于为什么一些变量要进行无符号位移,你可以自己研究一下,比如在 Stack Overflow 中有一些高票的回答,这里就不占用篇幅了。下面我们再看来一下 pop 的实现。
pop 方法的底层实现
同样我们也一起来看下 pop 的底层实现,你也可以先去 ECMA 的官网去查一下关于 pop 的基本描述(链接:ECMA 数组的 pop 标准),我们还是同样看下英文的描述。
When the pop method is called, the following steps are taken:1. Let O be ? ToObject(this value).2. Let len be ? LengthOfArrayLike(O).3. If len = 0, thenPerform ? Set(O, "length", +0F, true).Return undefined.4. Else,Assert: len > 0.Let newLen be F(len - 1).Let index be ! ToString(newLen).Let element be ? Get(O, index).Perform ? DeletePropertyOrThrow(O, index).Perform ? Set(O, "length", newLen, true).Return element.
从上面的描述可以看到边界判断逻辑以及实现的思路,根据上面的英文,我们同样将其转换为可以理解的代码,如下所示。
Array.prototype.pop = function() {let O = Object(this);let len = this.length >>> 0;if (len === 0) {O.length = 0;return undefined;}len --;let value = O[len];delete O[len];O.length = len;return value;}
其核心思路还是在于删掉数组自身的最后一个元素,index 就是数组的 len 长度,然后更新最新的长度,最后返回的元素的值,即可达到想要的效果。另外就是在当长度为 0 的时候,如果执行 pop 操作,返回的是 undefined,需要做一下特殊处理。
看完了 pop 的实现,我们再来看一下 map 方法的底层逻辑。
map 方法的底层实现
同样你可以去 ECMA 的官网去查一下关于 map 的基本描述(链接:ECMA 数组的 map 标准),请看英文的表述。
When the map method is called with one or two arguments, the following steps are taken:1. Let O be ? ToObject(this value).2. Let len be ? LengthOfArrayLike(O).3. If IsCallable(callbackfn) is false, throw a TypeError exception.4. Let A be ? ArraySpeciesCreate(O, len).5. Let k be 0.6. Repeat, while k < len,a. Let Pk be ! ToString(F(k)).b. Let kPresent be ? HasProperty(O, Pk).c. If kPresent is true, thenLet kValue be ? Get(O, Pk).Let mappedValue be ? Call(callbackfn, thisArg, « kValue, F(k), O »).Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue).d. Set k to k + 1.7. Return A.
同样的,根据上面的英文,我们将其转换为可理解的代码,如下所示。
Array.prototype.map = function(callbackFn, thisArg) {if (this === null || this === undefined) {throw new TypeError("Cannot read property 'map' of null");}if (Object.prototype.toString.call(callbackfn) != "[object Function]") {throw new TypeError(callbackfn + ' is not a function')}let O = Object(this);let T = thisArg;let len = O.length >>> 0;let A = new Array(len);for(let k = 0; k < len; k++) {if (k in O) {let kValue = O[k];let mappedValue = callbackfn.call(T, KValue, k, O);A[k] = mappedValue;}}return A;}
有了上面实现 push 和 pop 的基础思路,map 的实现也不会太难了,基本就是再多加一些判断,循环遍历实现 map 的思路,将处理过后的 mappedValue 赋给一个新定义的数组 A,最后返回这个新数组 A,并不改变原数组的值。
我们在 “07 | 数组原理(上):帮你梳理眼花缭乱的数组 API” 中也介绍过数据的方法分类,遍历类型的方法最后返回的都是一个新数组,并不改变原有数组的值,这点你需要牢记。
最后我们来看看 reduce 的实现。
reduce 方法的底层实现
ECMA 官网关于 reduce 的基本描述(链接:ECMA 数组的 pop 标准),如下所示。
When the reduce method is called with one or two arguments, the following steps are taken:1. Let O be ? ToObject(this value).2. Let len be ? LengthOfArrayLike(O).3. If IsCallable(callbackfn) is false, throw a TypeError exception.4. If len = 0 and initialValue is not present, throw a TypeError exception.5. Let k be 0.6. Let accumulator be undefined.7. If initialValue is present, thenSet accumulator to initialValue.8. Else,Let kPresent be false.Repeat, while kPresent is false and k < len,Let Pk be ! ToString(F(k)).Set kPresent to ? HasProperty(O, Pk).If kPresent is true, thenSet accumulator to ? Get(O, Pk).Set k to k + 1.If kPresent is false, throw a TypeError exception.9. Repeat, while k < len,Let Pk be ! ToString(F(k)).Let kPresent be ? HasProperty(O, Pk).If kPresent is true, thenLet kValue be ? Get(O, Pk).Set accumulator to ? Call(callbackfn, undefined, « accumulator, kValue, F(k), O »).Set k to k + 1.10. Return accumulator.
还是将其转换为我们自己的代码,如下所示。
Array.prototype.reduce = function(callbackfn, initialValue) {if (this === null || this === undefined) {throw new TypeError("Cannot read property 'reduce' of null");}if (Object.prototype.toString.call(callbackfn) != "[object Function]") {throw new TypeError(callbackfn + ' is not a function')}let O = Object(this);let len = O.length >>> 0;let k = 0;let accumulator = initialValue;if (accumulator === undefined) {throw new Error('Each element of the array is empty');for(; k < len ; k++) {if (k in O) {accumulator = O[k];k++;break;}}}for(;k < len; k++) {if (k in O) {accumulator = callbackfn.call(undefined, accumulator, O[k], O);}}return accumulator;}
根据上面的代码及注释,有几个关键点你需要重点关注:
- 初始值默认值不传的特殊处理;
- 累加器以及 callbackfn 的处理逻辑。
这两个关键问题处理好,其他的地方和上面几个方法实现的思路是基本类似的,你要学会举一反三。
总结
到这里,本讲的内容就先告一段落了。这一讲内容虽少,但却是你必须要掌握的内容。
这一讲中,我把 JS 的 push 、pop、map、reduce 的底层方法挨个带你实现了一遍,希望你能对此形成一套自己的思路。我所提供的实现代码,虽然不能完全和 V8 源码中实现的代码媲美,但是在正常的使用中,你如果自己能实现到这个程度,基本也可以满足要求了。
讲到这里,我再贴一下 V8 数组关于各种方法的实现源码地址,如下表所示。
| 数组方法 | V8 源码地址 |
|---|---|
| pop | V8 源码 pop 的实现 |
| push | V8 源码 push 的实现 |
| map | V8 源码 map 的实现 |
| slice | V8 源码 slice 的实现 |
| filter | V8 源码 filter 的实现 |
| … | … |
关于本讲内容没有提到的代码及方法,你可以根据自己的兴趣,尝试着实现其中的某个方法。
同时也希望你能够多思考日常工作中都有哪些经常用到的 JS 方法,并且去研究其底层源代码的实现逻辑,找机会自己实现一遍,来整体提升你的 JavaScript 的编程能力和对底层的理解能力。
下一讲我们将会进入一个全新的模块——JS 的异步编程篇,期待你能从中学习到更多的东西。每天进步一点点,加油!
