JS数组中那些你知道或不知道的

JS中的Array

ecma-262中的定义:

Array对象是一种特殊对象,它会对数组索引属性键进行特殊处理。

每个Array对象都有一个不可配置的length属性,其最大值是232 - 1

Array()

当且仅当不带参数调用Array构造函数时,此描述才适用。

执行过程:

  1. 定义 numberOfArgs 传递给此函数的调用的实参数量;
  2. 断言: numberOfArgs 为 0;
  3. 如果 NewTargetundefined ,就设置 newTarget活动函数对象(active-function-object,正在运行的执行上下文的函数组件) ,并且让 newTarget 成为 NewTarget
  4. 原型 proto 怎么办?通过原生方法GetPrototypeFromConstructor(newTarget, "%ArrayPrototype%")来构造;
  5. 返回原生方法ArrayCreate(0, proto)

鱼头注:NewTarget是啥?NewTarget是原生Class FunctionCallbackInfo(函数调用的callback上下文的信息)内的一个不变量,用来定义构造调用时的返回值(new.target)。

Array(len)

当且仅当使用一个参数调用Array构造函数时,此描述才适用。

执行过程:

  1. 定义 numberOfArgs 为传递给此函数的调用的实参数量;
  2. 断言: numberOfArgs 为1;
  3. 如果 NewTargetundefined ,就设置 newTarget活动函数对象 ,并且让 newTarget 成为 NewTarget
  4. 原型 proto 怎么办?通过原生方法GetPrototypeFromConstructor(newTarget, "%ArrayPrototype%")来构造;
  5. 然后定义arrayArrayCreate(0, proto)
  6. 如果 len 的类型不是个Number,则:
    1. 定义 defineStatusCreateDataProperty(array, "0", len)
    2. 断言:defineStatus为真;
    3. intLen(初始化长度) 为1。
  7. 或者:
    1. 定义intLenToUint32(len)(原生方法,将len转换成0到232-1之间的整数值)
    2. 如果intLen不等于len,抛出RangeError异常。
  8. 执行Set(array, "length", intLen, true)(原生方法,给对象的属性赋值)
  9. 返回array

Array(...items)

当且仅当使用至少两个参数调用Array构造函数时,此描述才适用。

执行过程:

  1. 定义 numberOfArgs 为传递给此函数的调用的实参数量;
  2. 断言: numberOfArgs 大于等于2;
  3. 如果 NewTargetundefined ,就设置 newTarget活动函数对象 ,并且让 newTarget 成为 NewTarget
  4. 原型 proto 怎么办?通过原生方法GetPrototypeFromConstructor(newTarget, "%ArrayPrototype%")来构造;
  5. 然后定义arrayArrayCreate(numberOfArgs, proto)
  6. 定义 k 为0;
  7. 定义 items为 正序传入参数的 零源(zero-origined) 列表;
  8. 重复,当 k 小于 numberOfArgs
    1. 定义 PkToSting(k)
    2. 定义 itemKitem[k]
    3. 定义 defineStatusCreateDataProperty(array, Pk, itemK);
    4. 断言:defineStatus为真;
    5. k 加1。
  9. 断言: arraylength 值为 numberOfArgs
  10. 返回 array

empty

上面的三种情况以上便是构造Array()时不同情况的具体实现。但是我们从上面的断言可以知道,构造结果有可能为真,有可能为假。还有是定义指定长度数组时会出现什么事呢?

V8源码 3.28.71(node0.12.18)Array 有个CloneElementAt的方法。定义如下:

在指定索引处克隆元素时,如果克隆失败,则返回一个空句柄(任何原因)。

从这句话我们可以知道,当我们构造一个指定长度的 Array 时,由于有长度,所以会开辟相应下标的空间,但是因为该下标并没有元素,所以就会返回empty,任何原因构造数组元素失败时,都会返回一个empty

示例如下:

var arr = new Array(10);
arr // [empty × 10]

以上总结

上面是 ECMA 上的定义以及 V8 源码的容错处理,其实简单来说就是:

调用 Array(args) 时:

  1. 用原生方法 GetPrototypeFromConstructor 生成原型 proto
  2. 判断 args 的类型;
  3. 如果为 undefined,则直接返回创建数组的原生方法 ArrayCreate
  4. 如果为 number,则用原生方法 Set 创建 args 长度的数组,并通过原生方法 CloneElementAt 来创建 argsempty 作为数组元素,如果args 大于 232 - 1 的话,会报错;
  5. 如果为其他类型,则把 args 变成数组元素,并用 原生方法 CreateDataProperty 创建参数,然后返回创建数组的原生方法 ArrayCreate

类型转换

类型转换是一个经常出现在一些网上常见面试题或者奇技淫巧中的内容。那么关于数组的类型转换,又是怎样的呢?

首先我们要知道,在 JS 中类型转换只有三种情况,分别是:

  • 转换为布尔值
  • 转换为数字
  • 转换为字符串

转换为原始类型

对象在转换类型的时候,会执行原生方法ToPrimitive

其算法如下:

  1. 如果已经是 原始类型,则返回当前值;
  2. 如果需要转 字符串 则先调用toSting方法,如果此时是 原始类型 则直接返回,否则再调用valueOf方法并返回结果;
  3. 如果不是 字符串,则先调用valueOf方法,如果此时是 原始类型 则直接返回,否则再调用toString方法并返回结果;
  4. 如果都没有 原始类型 返回,则抛出 TypeError类型错误。

当然,我们可以通过重写Symbol.toPrimitive来制定转换规则,此方法在转原始类型时调用优先级最高。

// 下面例子来自YCK的小册
const data = {
  valueOf () {
    return 1;
        },
  toString () {
    return '1';
        },
  [Symbol.toPrimitive]() {
    return 2;
  }
};
data + 1 // 3

转换为布尔值

对象转换为布尔值的规则如下表:

参数类型 结果
Undefined 返回 false
Null 返回 false
Boolean 返回 当前参数。
Number 如果参数为+0-0NaN,则返回 false;其他情况则返回 true
String 如果参数为空字符串,则返回 false;否则返回 true
Symbol 返回 true
Object 返回 true

转换为数字

对象转换为数字的规则如下表:

参数类型 结果
Undefined 返回 NaN
Null Return +0.
Boolean 如果参数为 true,则返回 1false则返回 +0
Number 返回当前参数。
String 先调用 ToPrimitive,再调用 ToNumber,然后返回结果。
Symbol 抛出 TypeError错误。
Object 先调用 ToPrimitive,再调用 ToNumber,然后返回结果。

转换为字符串

对象转换为字符串的规则如下表:

参数类型 结果
Undefined 返回 "undefined"
Null 返回 "null"
Boolean 如果参数为 true ,则返回 "true";否则返回 "false"
Number 调用 NumberToString,然后返回结果。
String 返回 当前参数。
Symbol 抛出 TypeError错误。
Object 先调用 ToPrimitive,再调用 ToString,然后返回结果。

数组的类型转换

所以通过上面的转换规则,我们是否能够轻松地看懂以下的隐式转换呢?

[1,2,3] + {a: 1, b: 2} // "1,2,3[object Object]"
[1,2,3] + 1 // "1,2,31"
[1,2,3] + true // "1,2,3true"
[1,2,3] + undefined // "1,2,3undefined"
[1,2,3] + null // "1,2,3null"
[1,2,3] + '123' // "1,2,3123"
[1,2,3] + Symbol('biu') // "Uncaught TypeError"

所以各位是否理解上述隐式转换的答案呢?

关于API使用的一些经验与思考

JS数组自带了很多的方法,在现代工程化数据驱动的理念下,这些方法都是非常重要的。

loops

forEachArray 方法中比较基本的一个,作用也很简单,与for,就是遍历,循环。不同的是,forEach可以选择自定义上下文环境。例子如下:

var arr1 = [1, 2, 3];
var arr2 = [5, 6, 7];
arr1.forEach(function (e, i, a) {
  console.log(e, this); // this为arr2
}, arr2);

但是如果forEach的回调函数是用箭头函数定义的,那么就无法改变它原本指向的上下文环境。例子如下:

var arr1 = [1, 2, 3];
var arr2 = [5, 6, 7];
arr1.forEach((e, i, a) => {
  console.log(e, this); // this为window对象
}, arr2);

所以如果我们要实现以下这个功能:

<!--
点击当前li时,当前li文字变色,其余兄弟li变回默认颜色
-->
<ul>
    <li class="1">1</li>
    <li class="2">2</li>
    <li class="3">3</li>
    <li class="4">4</li>
    <li class="5">5</li>
</ul>
<script>
    'use strict';
    var ul = document.querySelector('ul');
    ul.onClick = event => {
        var cls = event.target.className;
        ul.querySelectorAll('li').forEach(el => {
            el.style.color = (cls === el.className ? '#FFF' : '#FF0');
        });
    };
</script>

在ES6以前的环境中,如果直接用for循环,会出现只能获取到最后一个元素的问题,但是用forEach则没有这个问题。

reduce

Array ES5 API reducereduceRight,可以轻松实现并归元素的功能,例子如下:

如果我们需要实现一个这样的对象:

{
    a: 1,
    b: 2,
    c: 3
    ...
};

那么用reduce就会变得很简单:

var newArr = 'a,b,c,d,e,f'.split(',').reduce((acc, cur, idx) => {
    let o = {};
    if (Object.prototype.toString.call(acc) !== '[object Object]') {
        o[cur] = idx;
    } else {
        let newO = {};
        newO[cur] = idx;
        o = {
            ...acc,
            ...newO,
        };
    };
    return o;
}, 'a');

性能

上面演示了通过JS数组API实现的一些功能,所以与for循环比性能如何呢?

var arr = new Array(100);

arr.forEach(data => {
  console.log(data);
});

for (var i = 0; i < arr.length; ++i) {
  console.log(arr[i]);
};

所以哪个更耗时间呢?

在公布结果之前,其实网上一直流传着for循环性能比forEach性能好,考虑性能少用forEach的言论,其实以前的浏览器也是这种情况。

详情可以看知乎的这篇评论:https://www.zhihu.com/question/54637225/answer/140362071

性能对比如下:

以下代码测试环境为:Chrome 55.0.2883 / Windows 7 0.0.0

image

image

所以在9012年的如今,结果又会是如何呢?

以下代码测试环境为:Chrome 73.0.3683 / Windows 10 0.0.0

image
image

通过上面的对比,结果已经很明显了,我们要知道,现代的浏览器性能优化已经做得比以前好很多了,再加上电子设备本身的硬件也越来越好,所以代码块的性能不是我们首要的考虑因素。

在跟同行沟通的过程中,经常会看到有人为了扣那么一个两个表达式的性能而烦恼,其实是这是没有任何必要,原因也如上,我们应该优化的是我们表达式是否清晰明了,是否适合后期维护或拓展。

如果你喜欢探讨技术,或者对本文有任何的意见或建议,非常欢迎加鱼头微信好友一起探讨,当然,鱼头也非常希望能跟你一起聊生活,聊爱好,谈天说地。
鱼头的微信号是:krisChans95
也可以扫码关注公众号,订阅更多精彩内容。

https://user-gold-cdn.xitu.io/2020/3/4/170a55cc795174aa?w=1000&h=480&f=png&s=311000
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容