JS数组中那些你知道或不知道的JS中的Array
ecma-262中的定义:Array对象是一种特殊对象,它会对数组索引属性键进行特殊处理。每个Array对象都有一个不可配置的length属性,其最大值是2³²- 1。
Array()
当且仅当不带参数调用Array构造函数时,此描述才适用。
执行过程:
定义numberOfArgs传递给此函数的调用的实参数量;
断言:numberOfArgs为 0;
如果NewTarget为undefined,就设置newTarget为活动函数对象(active-function-object,正在运行的执行上下文的函数组件),并且让newTarget成为NewTarget;
原型proto怎么办?通过原生方法GetPrototypeFromConstructor(newTarget,”%ArrayPrototype%”)来构造;
返回原生方法ArrayCreate(0,proto)。
鱼头注:NewTarget是啥?NewTarget是原生Class FunctionCallbackInfo(函数调用的callback上下文的信息)内的一个不变量,用来定义构造调用时的返回值(new.target)。
Array(len)
当且仅当使用一个参数调用Array构造函数时,此描述才适用。
执行过程:
1.定义 numberOfArgs 为传递给此函数的调用的实参数量;2.断言: numberOfArgs 为1;3.如果 NewTarget 为 undefined ,就设置 newTarget 为 活动函数对象 ,并且让 newTarget 成为 NewTarget ;4.原型 proto 怎么办?通过原生方法 GetPrototypeFromConstructor(newTarget,”%ArrayPrototype%”)来构造;5.然后定义array 为 ArrayCreate(0,proto);6.如果 len 的类型不是个Number,则:a.定义 defineStatus 为CreateDataProperty(array, “0”, len);b.断言:defineStatus为真;c.让 intLen(初始化长度) 为1。
7.或者:a.定义intLen为ToUint32(len)(原生方法,将len转换成0到2³²- 1之间的整数值);b.如果intLen不等于len,抛出RangeError异常。
8.执行Set(array, “length”, intLen, true)(原生方法,给对象的属性赋值);9.返回array。
Array(…items)
当且仅当使用至少两个参数调用Array构造函数时,此描述才适用。
执行过程:
1.定义 numberOfArgs 为传递给此函数的调用的实参数量;2.断言: numberOfArgs 大于等于2;3.如果 NewTarget 为 undefined ,就设置 newTarget 为 活动函数对象 ,并且让 newTarget 成为 NewTarget ;4.原型 proto 怎么办?通过原生方法 GetPrototypeFromConstructor(newTarget,”%ArrayPrototype%”)来构造;5.然后定义array 为 ArrayCreate(numberOfArgs,proto);6.定义 k 为0;7.定义 items为 正序传入参数的 零源(zero-origined) 列表;8.重复,当 k 小于 numberOfArgs a.定义 Pk 为 ToSting(k);b.定义 itemK 为 item[k];c.定义 defineStatus 为CreateDataProperty(array, Pk, itemK);d.断言:defineStatus为真;e.k 加1。9.断言: array的 length 值为 numberOfArgs;10.返回 array。
empty
上面的三种情况以上便是构造Array()时不同情况的具体实现。但是我们从上面的断言可以知道,构造结果有可能为真,有可能为假。还有是定义指定长度数组时会出现什么事呢?
在V8源码 3.28.71(node0.12.18)中 Array 有个CloneElementAt的方法。定义如下:
在指定索引处克隆元素时,如果克隆失败,则返回一个空句柄(任何原因)。
从这句话我们可以知道,当我们构造一个指定长度的 Array 时js获取数组最后一个值js获取数组最后一个值,由于有长度,所以会开辟相应下标的空间,但是因为该下标并没有元素,所以就会返回empty,任何原因构造数组元素失败时,都会返回一个empty。
示例如下:
var arr = new Array(10);
arr // [empty × 10]
以上总结
上面是 ECMA 上的定义以及 V8 源码的容错处理,其实简单来说就是:
调用 Array(args) 时:
用原生方法GetPrototypeFromConstructor生成原型proto;
判断args的类型;
如果为undefined,则直接返回创建数组的原生方法ArrayCreate;
如果为number,则用原生方法Set创建args长度的数组,并通过原生方法CloneElementAt来创建args个empty作为数组元素,如果args大于2³²- 1的话,会报错;
如果为其他类型,则把args变成数组元素,并用 原生方法CreateDataProperty创建参数,然后返回创建数组的原生方法ArrayCreate。
类型转换
类型转换是一个经常出现在一些网上常见面试题或者奇技淫巧中的内容。那么关于数组的类型转换,又是怎样的呢?
首先我们要知道,在 JS 中类型转换只有三种情况,分别是:
转换为原始类型
对象在转换类型的时候,会执行原生方法ToPrimitive。
其算法如下:
如果已经是原始类型,则返回当前值;
如果需要转字符串则先调用toSting方法,如果此时是原始类型则直接返回,否则再调用valueOf方法并返回结果;
如果不是字符串,则先调用valueOf方法,如果此时是原始类型则直接返回,否则再调用toString方法并返回结果;
如果都没有原始类型返回,则抛出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、-0或NaN,则返回false;其他情况则返回true。
String
如果参数为空字符串,则返回false;否则返回true。
Symbol
返回true。
Object
返回true。
转换为数字
对象转换为数字的规则如下表:
参数类型结果
Undefined
返回NaN。
Null
Return +0.
Boolean
如果参数为true,则返回1;false则返回+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
forEach是 Array 方法中比较基本的一个,作用也很简单,与 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 class="1">1
<li class="2">2
<li class="3">3
<li class="4">4
<li class="5">5
'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');
});
};
在ES6以前的环境中,如果直接用 for循环,会出现只能获取到最后一个元素的问题,但是用 forEach则没有这个问题。
reduce
Array ES5 API reduce跟 reduceRight,可以轻松实现并归元素的功能,例子如下:
如果我们需要实现一个这样的对象:
{
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的言论,其实以前的浏览器也是这种情况。
限时特惠:本站每日持续更新海量设计资源,一年会员只需29.9元,全站资源免费下载
站长微信:ziyuanshu688