笔者最近在对原生JS的知识做系统梳理,因为我觉得JS作为前端工程师的根本技术,学再多遍都不为过。打算来做一个系列,一共分三次发,以一系列的问题为驱动,当然也会有追问和扩展,内容系统且完整,对初中级选手会有很好的提升,高级选手也会得到复习和巩固。这是本系列的第二篇。
扫了一眼目录后,也许你可能会说:这些八百年都用不到的东西,我为什么要会?是,我承认真实业务场景中并不会要你手写一个splice, 手写深拷贝或者V8的数组排序,但我要说的是,问这些问题的初衷并不是让你拿到平时去用的,而是检验你对 JS语言的理解
有没有到达那样的水准,有一些 边界情况
是否能够考虑到,有没有基本的 计算机素养
(比如最基本的排序方法到底理不理解),未来有没有潜力去设计出更加优秀的产品或者框架。如果你仅仅是想通过一篇文章来解决业务中的临时问题,那不好意思,请出门左拐,这篇文章确实不适合你。但如果你觉得自己的原生编程能力还有待提高,想让自己的思维能力上一个台阶,希望我这篇"呕心沥血"整理了1w多字的文章能够让你有所成长。另外补充一句,本文并不针对面试,但以下任何一篇的内容放在面试中,都是非常惊艳的操作:)
第七篇: 函数的arguments为什么不是数组?如何转化成数组?
因为argument是一个对象,只不过它的属性从0开始排,依次为0,1,2...最后还有callee和length属性。我们也把这样的对象称为类数组。
常见的类数组还有:
-
用getElementByTagName/ClassName/Name()获得的HTMLCollection
-
用querySlector获得的nodeList
那这导致很多数组的方法就不能用了,必要时需要我们将它们转换成数组,有哪些方法呢?
1. Array.prototype.slice.call()
-
function sum(a, b) {
-
let args = Array.prototype.slice.call(arguments);
-
console.log(args.reduce((sum, cur) => sum + cur));//args可以调用数组原生的方法啦
-
}
-
sum(1, 2);//3
2. Array.from()
-
function sum(a, b) {
-
let args = Array.from(arguments);
-
console.log(args.reduce((sum, cur) => sum + cur));//args可以调用数组原生的方法啦
-
}
-
sum(1, 2);//3
这种方法也可以用来转换Set和Map哦!
3. ES6展开运算符
-
function sum(a, b) {
-
let args = [...arguments];
-
console.log(args.reduce((sum, cur) => sum + cur));//args可以调用数组原生的方法啦
-
}
-
sum(1, 2);//3
4. 利用concat+apply
-
function sum(a, b) {
-
let args = Array.prototype.concat.apply([], arguments);//apply方法会把第二个参数展开
-
console.log(args.reduce((sum, cur) => sum + cur));//args可以调用数组原生的方法啦
-
}
-
sum(1, 2);//3
当然,最原始的方法就是再创建一个数组,用for循环把类数组的每个属性值放在里面,过于简单,就不浪费篇幅了。
第七篇: forEach中return有效果吗?如何中断forEach循环?
在forEach中用return不会返回,函数会继续执行。
-
let nums = [1, 2, 3];
-
nums.forEach((item, index) => {
-
return;//无效
-
})
中断方法:
-
使用try监视代码块,在需要中断的地方抛出异常。
-
官方推荐方法(替换方法):用every和some替代forEach函数。every在碰到return false的时候,中止循环。some在碰到return ture的时候,中止循环
第八篇: JS判断数组中是否包含某个值
方法一:array.indexOf
此方法判断数组中是否存在某个值,如果存在,则返回数组元素的下标,否则返回-1。
-
var arr=[1,2,3,4];
-
var index=arr.indexOf(3);
-
console.log(index);
方法二:array.includes(searcElement[,fromIndex])
此方法判断数组中是否存在某个值,如果存在返回true,否则返回false
-
var arr=[1,2,3,4];
-
if(arr.includes(3))
-
console.log("存在");
-
else
-
console.log("不存在");
方法三:array.find(callback[,thisArg])
返回数组中满足条件的第一个元素的值,如果没有,返回undefined
-
var arr=[1,2,3,4];
-
var result = arr.find(item =>{
-
return item > 3
-
});
-
console.log(result);
方法四:array.findeIndex(callback[,thisArg])
返回数组中满足条件的第一个元素的下标,如果没有找到,返回
-1
]
-
var arr=[1,2,3,4];
-
var result = arr.findIndex(item =>{
-
return item > 3
-
});
-
console.log(result);
当然,for循环当然是没有问题的,这里讨论的是数组方法,就不再展开了。
第九篇: JS中flat---数组扁平化
对于前端项目开发过程中,偶尔会出现层叠数据结构的数组,我们需要将多层级数组转化为一级数组(即提取嵌套数组元素最终合并为一个数组),使其内容合并且展开。那么该如何去实现呢?
需求:多维数组=>一维数组
-
let ary = [1, [2, [3, [4, 5]]], 6];// -> [1, 2, 3, 4, 5, 6]
-
let str = JSON.stringify(ary);
1. 调用ES6中的flat方法
-
ary = arr.flat(Infinity);
2. replace + split
-
ary = str.replace(/(\[|\])/g, '').split(',')
3. replace + JSON.parse
-
str = str.replace(/(\[|\]))/g, '');
-
str = '[' + str + ']';
-
ary = JSON.parse(str);
4. 普通递归
-
let result = [];
-
let fn = function(ary) {
-
for(let i = 0; i < ary.length; i++) {
-
let item = ary[i];
-
if (Array.isArray(ary[i])){
-
fn(item);
-
} else {
-
result.push(item);
-
}
-
}
-
}
5. 利用reduce函数迭代
-
function flatten(ary) {
-
return ary.reduce((pre, cur) => {
-
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
-
}, []);
-
}
-
let ary = [1, 2, [3, 4], [5, [6, 7]]]
-
console.log(flatten(ary))
6:扩展运算符
-
//只要有一个元素有数组,那么循环继续
-
while (ary.some(Array.isArray())) {
-
ary = [].concat(...ary);
-
}
这是一个比较实用而且很容易被问到的问题,欢迎大家交流补充。
第十篇: JS数组的高阶函数——基础篇
1.什么是高阶函数
概念非常简单,如下:
一个函数
就可以接收另一个函数作为参数或者返回值为一个函数,这种函数
就称之为高阶函数。
那对应到数组中有哪些方法呢?
2.数组中的高阶函数
1.map
-
参数:接受两个参数,一个是回调函数,一个是回调函数的this值(可选)。
其中,回调函数被默认传入三个值,依次为当前元素、当前索引、整个数组。
-
创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果
-
对原来的数组没有影响
-
let nums = [1, 2, 3];
-
let obj = {val: 5};
-
let newNums = nums.map(function(item,index,array) {
-
return item + index + array[index] + this.val;
-
//对第一个元素,1 + 0 + 1 + 5 = 7
-
//对第二个元素,2 + 1 + 2 + 5 = 10
-
//对第三个元素,3 + 2 + 3 + 5 = 13
-
}, obj);
-
console.log(newNums);//[7, 10, 13]
当然,后面的参数都是可选的 ,不用的话可以省略。
2. reduce
-
参数: 接收两个参数,一个为回调函数,另一个为初始值。回调函数中三个默认参数,依次为积累值、当前值、整个数组。
-
let nums = [1, 2, 3];
-
// 多个数的加和
-
let newNums = nums.reduce(function(preSum,curVal,array) {
-
return preSum + curVal;
-
}, 0);
-
console.log(newNums);//6
不传默认值会怎样?
不传默认值会自动以第一个元素为初始值,然后从第二个元素开始依次累计。
3. filter
参数: 一个函数参数。这个函数接受一个默认参数,就是当前元素。这个作为参数的函数返回值为一个布尔类型,决定元素是否保留。
filter方法返回值为一个新的数组,这个数组里面包含参数里面所有被保留的项。
-
let nums = [1, 2, 3];
-
// 保留奇数项
-
let oddNums = nums.filter(item => item % 2);
-
console.log(oddNums);
4. sort
参数: 一个用于比较的函数,它有两个默认参数,分别是代表比较的两个元素。
举个例子:
-
let nums = [2, 3, 1];
-
//两个比较的元素分别为a, b
-
nums.sort(function(a, b) {
-
if(a > b) return 1;
-
else if(a < b) return -1;
-
else if(a == b) return 0;
-
})
当比较函数返回值大于0,则 a 在 b 的后面,即a的下标应该比b大。
反之,则 a 在 b 的后面,即 a 的下标比 b 小。
整个过程就完成了一次升序的排列。
当然还有一个需要注意的情况,就是比较函数不传的时候,是如何进行排序的?
答案是将数字转换为字符串,然后根据字母unicode值进行升序排序,也就是根据字符串的比较规则进行升序排序。
第十一篇: 能不能手动实现数组的map方法 ?
依照 ecma262 草案,实现的map的规范如下:
下面根据草案的规定一步步来模拟实现map函数:
-
Array.prototype.map = function(callbackFn, thisArg) {
-
// 处理数组类型异常
-
if (this === null || this === undefined) {
-
throw new TypeError("Cannot read property 'map' of null or undefined");
-
}
-
// 处理回调类型异常
-
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++) {
-
// 还记得原型链那一节提到的 in 吗?in 表示在原型链查找
-
// 如果用 hasOwnProperty 是有问题的,它只能找私有属性
-
if (k in O) {
-
let kValue = O[k];
-
// 依次传入this, 当前项,当前索引,整个数组
-
let mappedValue = callbackfn.call(T, KValue, k, O);
-
A[k] = mappedValue;
-
}
-
}
-
return A;
-
}
这里解释一下, length >>> 0, 字面意思是指"右移 0 位",但实际上是把前面的空位用0填充,这里的作用是保证len为数字且为整数。
举几个特例:
-
null >>> 0 //0
-
undefined >>> 0 //0
-
void(0) >>> 0 //0
-
function a (){}; a >>> 0 //0
-
[] >>> 0 //0
-
var a = {}; a >>> 0 //0
-
123123 >>> 0 //123123
-
45.2 >>> 0 //45
-
0 >>> 0 //0
-
-0 >>> 0 //0
-
-1 >>> 0 //4294967295
-
-1212 >>> 0 //4294966084
总体实现起来并没那么难,需要注意的就是使用 in 来进行原型链查找。同时,如果没有找到就不处理,能有效处理稀疏数组的情况。
最后给大家奉上V8源码,参照源码检查一下,其实还是实现得很完整了。
-
function ArrayMap(f, receiver) {
-
CHECK_OBJECT_COERCIBLE(this, "Array.prototype.map");
-
// Pull out the length so that modifications to the length in the
-
// loop will not affect the looping and side effects are visible.
-
var array = TO_OBJECT(this);
-
var length = TO_LENGTH(array.length);
-
if (!IS_CALLABLE(f)) throw %make_type_error(kCalledNonCallable, f);
-
var result = ArraySpeciesCreate(array, length);
-
for (var i = 0; i < length; i++) {
-
if (i in array) {
-
var element = array[i];
-
%CreateDataProperty(result, i, %_Call(f, receiver, element, i, array));
-
}
-
}
-
return result;
-
}
参考:
V8源码
Array 原型方法源码实现大揭秘
ecma262草案
第十二篇: 能不能手动实现数组的reduce方法 ?
依照 ecma262 草案,实现的reduce的规范如下:
其中有几个核心要点:
1、初始值不传怎么处理
2、回调函数的参数有哪些,返回值如何处理。
-
Array.prototype.reduce = function(callbackfn, initialValue) {
-
// 异常处理,和 map 一样
-
// 处理数组类型异常
-
if (this === null || this === undefined) {
-
throw new TypeError("Cannot read property 'reduce' of null or undefined");
-
}
-
// 处理回调类型异常
-
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) {
-
for(; k < len ; k++) {
-
// 查找原型链
-
if (k in O) {
-
accumulator = O[k];
-
k++;
-
break;
-
}
-
}
-
// 循环结束还没退出,就表示数组全为空
-
throw new Error('Each element of the array is empty');
-
}
-
for(;k < len; k++) {
-
if (k in O) {
-
// 注意,核心!
-
accumulator = callbackfn.call(undefined, accumulator, O[k], O);
-
}
-
}
-
return accumulator;
-
}
其实是从最后一项开始遍历,通过原型链查找跳过空项。
最后给大家奉上V8源码,以供大家检查:
-
function ArrayReduce(callback, current) {
-
CHECK_OBJECT_COERCIBLE(this, "Array.prototype.reduce");
-
// Pull out the length so that modifications to the length in the
-
// loop will not affect the looping and side effects are visible.