笔者最近在对原生JS的知识作系统梳理,由于我以为JS做为前端工程师的根本技术,学再多遍都不为过。打算来作一个系列,一共分三次发,以一系列的问题为驱动,固然也会有追问和扩展,内容系统且完整,对初中级选手会有很好的提高,高级选手也会获得复习和巩固。这是本系列的第二篇。前端
扫了一眼目录后,也许你可能会说:这些八百年都用不到的东西,我为何要会?是,我认可真实业务场景中遇到诸如手写splice、深拷贝的场景并很少,但我要说的是,问这些问题的初衷并非让你拿到平时去用的,而是检验你对JS语言的理解
有没有到达那样的水准,有一些边界状况
是否可以考虑到,有没有基本的计算机素养
(好比最基本的排序方法到底理不理解),将来有没有潜力去设计出更加优秀的产品或者框架。若是你仅仅是想经过一篇文章来解决业务中的临时问题,那很差意思,请出门左拐,这篇文章确实不适合你。但若是你以为本身的原生编程能力还有待提升,想让本身的思惟能力上一个台阶,但愿我这篇"呕心沥血"整理了1万六千多字的文章可以让你有所成长。另外补充一句,本文并不针对面试,但如下任何一篇的内容放在面试中,都是很是惊艳的操做:)node
由于argument是一个对象,只不过它的属性从0开始排,依次为0,1,2...最后还有callee和length属性。咱们也把这样的对象称为类数组。git
常见的类数组还有:es6
那这致使不少数组的方法就不能用了,必要时须要咱们将它们转换成数组,有哪些方法呢?github
function sum(a, b) {
let args = Array.prototype.slice.call(arguments);
console.log(args.reduce((sum, cur) => sum + cur));//args能够调用数组原生的方法啦
}
sum(1, 2);//3
复制代码
function sum(a, b) {
let args = Array.from(arguments);
console.log(args.reduce((sum, cur) => sum + cur));//args能够调用数组原生的方法啦
}
sum(1, 2);//3
复制代码
这种方法也能够用来转换Set和Map哦!面试
function sum(a, b) {
let args = [...arguments];
console.log(args.reduce((sum, cur) => sum + cur));//args能够调用数组原生的方法啦
}
sum(1, 2);//3
复制代码
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不会返回,函数会继续执行。编程
let nums = [1, 2, 3];
nums.forEach((item, index) => {
return;//无效
})
复制代码
中断方法:api
使用try监视代码块,在须要中断的地方抛出异常。数组
官方推荐方法(替换方法):用every和some替代forEach函数。every在碰到return false的时候,停止循环。some在碰到return ture的时候,停止循环
此方法判断数组中是否存在某个值,若是存在,则返回数组元素的下标,不然返回-1。
var arr=[1,2,3,4];
var index=arr.indexOf(3);
console.log(index);
复制代码
此方法判断数组中是否存在某个值,若是存在返回true,不然返回false
var arr=[1,2,3,4];
if(arr.includes(3))
console.log("存在");
else
console.log("不存在");
复制代码
返回数组中知足条件的第一个元素的值,若是没有,返回undefined
var arr=[1,2,3,4];
var result = arr.find(item =>{
return item > 3
});
console.log(result);
复制代码
返回数组中知足条件的第一个元素的下标,若是没有找到,返回
-1
]
var arr=[1,2,3,4];
var result = arr.findIndex(item =>{
return item > 3
});
console.log(result);
复制代码
固然,for循环固然是没有问题的,这里讨论的是数组方法,就再也不展开了。
对于前端项目开发过程当中,偶尔会出现层叠数据结构的数组,咱们须要将多层级数组转化为一级数组(即提取嵌套数组元素最终合并为一个数组),使其内容合而且展开。那么该如何去实现呢?
需求:多维数组=>一维数组
let ary = [1, [2, [3, [4, 5]]], 6];// -> [1, 2, 3, 4, 5, 6]
let str = JSON.stringify(ary);
复制代码
ary = arr.flat(Infinity);
复制代码
ary = str.replace(/(\[|\])/g, '').split(',')
复制代码
str = str.replace(/(\[|\]))/g, '');
str = '[' + str + ']';
ary = JSON.parse(str);
复制代码
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);
}
}
}
复制代码
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))
复制代码
//只要有一个元素有数组,那么循环继续
while (ary.some(Array.isArray)) {
ary = [].concat(...ary);
}
复制代码
这是一个比较实用并且很容易被问到的问题,欢迎你们交流补充。
概念很是简单,以下:
一个函数
就能够接收另外一个函数做为参数或者返回值为一个函数,这种函数
就称之为高阶函数。
那对应到数组中有哪些方法呢?
其中,回调函数被默认传入三个值,依次为当前元素、当前索引、整个数组。
建立一个新数组,其结果是该数组中的每一个元素都调用一个提供的函数后返回的结果
对原来的数组没有影响
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]
复制代码
固然,后面的参数都是可选的 ,不用的话能够省略。
let nums = [1, 2, 3];
// 多个数的加和
let newNums = nums.reduce(function(preSum,curVal,array) {
return preSum + curVal;
}, 0);
console.log(newNums);//6
复制代码
不传默认值会怎样?
不传默认值会自动以第一个元素为初始值,而后从第二个元素开始依次累计。
参数: 一个函数参数。这个函数接受一个默认参数,就是当前元素。这个做为参数的函数返回值为一个布尔类型,决定元素是否保留。
filter方法返回值为一个新的数组,这个数组里面包含参数里面全部被保留的项。
let nums = [1, 2, 3];
// 保留奇数项
let oddNums = nums.filter(item => item % 2);
console.log(oddNums);
复制代码
参数: 一个用于比较的函数,它有两个默认参数,分别是表明比较的两个元素。
举个例子:
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值进行升序排序,也就是根据字符串的比较规则进行升序排序。
依照 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;
}
复制代码
参考:
依照 ecma262 草案,实现的reduce的规范以下:
其中有几个核心要点:
一、初始值不传怎么处理
二、回调函数的参数有哪些,返回值如何处理。
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;
}
}
}
// 表示数组全为空
if(k === len && accumulator === undefined)
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.
var array = TO_OBJECT(this);
var length = TO_LENGTH(array.length);
return InnerArrayReduce(callback, current, array, length,
arguments.length);
}
function InnerArrayReduce(callback, current, array, length, argumentsLength) {
if (!IS_CALLABLE(callback)) {
throw %make_type_error(kCalledNonCallable, callback);
}
var i = 0;
find_initial: if (argumentsLength < 2) {
for (; i < length; i++) {
if (i in array) {
current = array[i++];
break find_initial;
}
}
throw %make_type_error(kReduceNoInitial);
}
for (; i < length; i++) {
if (i in array) {
var element = array[i];
current = callback(current, element, i, array);
}
}
return current;
}
复制代码
参考:
参照 ecma262 草案的规定,关于 push 和 pop 的规范以下图所示:
首先来实现一下 push 方法:
Array.prototype.push = function(...items) {
let O = Object(this);
let len = this.length >>> 0;
let argCount = items.length >>> 0;
// 2 ** 53 - 1 为JS能表示的最大正整数
if (len + argCount > 2 ** 53 - 1) {
throw new TypeError("The number of array is over the max value restricted!")
}
for(let i = 0; i < argCount; i++) {
O[len + i] = items[i];
}
let newLength = len + argCount;
O.length = newLength;
return newLength;
}
复制代码
亲测已经过MDN上全部测试用例。MDN连接
而后来实现 pop 方法:
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;
}
复制代码
亲测已经过MDN上全部测试用例。MDN连接
参考连接:
代码以下:
Array.prototype.filter = function(callbackfn, thisArg) {
// 处理数组类型异常
if (this === null || this === undefined) {
throw new TypeError("Cannot read property 'filter' 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 resLen = 0;
let res = [];
for(let i = 0; i < len; i++) {
if (i in O) {
let element = O[i];
if (callbackfn.call(thisArg, O[i], i, O)) {
res[resLen++] = element;
}
}
}
return res;
}
复制代码
MDN上全部测试用例亲测经过。
参考:
splice 能够说是最受欢迎的数组方法之一,api 灵活,使用方便。如今来梳理一下用法:
被删除元素
组成的数组
。接下来咱们实现这个方法。
参照ecma262草案的规定,详情请点击。
首先咱们梳理一下实现的思路。
Array.prototype.splice = function(startIndex, deleteCount, ...addElements) {
let argumentsLen = arguments.length;
let array = Object(this);
let len = array.length;
let deleteArr = new Array(deleteCount);
// 拷贝删除的元素
sliceDeleteElements(array, startIndex, deleteCount, deleteArr);
// 移动删除元素后面的元素
movePostElements(array, startIndex, len, deleteCount, addElements);
// 插入新元素
for (let i = 0; i < addElements.length; i++) {
array[startIndex + i] = addElements[i];
}
array.length = len - deleteCount + addElements.length;
return deleteArr;
}
复制代码
先拷贝删除的元素,以下所示:
const sliceDeleteElements = (array, startIndex, deleteCount, deleteArr) => {
for (let i = 0; i < deleteCount; i++) {
let index = startIndex + i;
if (index in array) {
let current = array[index];
deleteArr[i] = current;
}
}
};
复制代码
而后对删除元素后面的元素进行挪动, 挪动分为三种状况:
当二者相等时,
const movePostElements = (array, startIndex, len, deleteCount, addElements) => {
if (deleteCount === addElements.length) return;
}
复制代码
当添加的元素个数小于删除的元素时, 如图所示:
const movePostElements = (array, startIndex, len, deleteCount, addElements) => {
//...
// 若是添加的元素和删除的元素个数不相等,则移动后面的元素
if(deleteCount > addElements.length) {
// 删除的元素比新增的元素多,那么后面的元素总体向前挪动
// 一共须要挪动 len - startIndex - deleteCount 个元素
for (let i = startIndex + deleteCount; i < len; i++) {
let fromIndex = i;
// 将要挪动到的目标位置
let toIndex = i - (deleteCount - addElements.length);
if (fromIndex in array) {
array[toIndex] = array[fromIndex];
} else {
delete array[toIndex];
}
}
// 注意注意!这里咱们把后面的元素向前挪,至关于数组长度减少了,须要删除冗余元素
// 目前长度为 len + addElements - deleteCount
for (let i = len - 1; i >= len + addElements.length - deleteCount; i --) {
delete array[i];
}
}
};
复制代码
当添加的元素个数大于删除的元素时, 如图所示:
const movePostElements = (array, startIndex, len, deleteCount, addElements) => {
//...
if(deleteCount < addElements.length) {
// 删除的元素比新增的元素少,那么后面的元素总体向后挪动
// 思考一下: 这里为何要从后往前遍历?从前日后会产生什么问题?
for (let i = len - 1; i >= startIndex + deleteCount; i--) {
let fromIndex = i;
// 将要挪动到的目标位置
let toIndex = i + (addElements.length - deleteCount);
if (fromIndex in array) {
array[toIndex] = array[fromIndex];
} else {
delete array[toIndex];
}
}
}
};
复制代码
当用户传来非法的 startIndex 和 deleteCount 或者负索引的时候,须要咱们作出特殊的处理。
const computeStartIndex = (startIndex, len) => {
// 处理索引负数的状况
if (startIndex < 0) {
return startIndex + len > 0 ? startIndex + len: 0;
}
return startIndex >= len ? len: startIndex;
}
const computeDeleteCount = (startIndex, len, deleteCount, argumentsLen) => {
// 删除数目没有传,默认删除startIndex及后面全部的
if (argumentsLen === 1)
return len - startIndex;
// 删除数目太小
if (deleteCount < 0)
return 0;
// 删除数目过大
if (deleteCount > len - deleteCount)
return len - startIndex;
return deleteCount;
}
Array.prototype.splice = function (startIndex, deleteCount, ...addElements) {
//,...
let deleteArr = new Array(deleteCount);
// 下面参数的清洗工做
startIndex = computeStartIndex(startIndex, len);
deleteCount = computeDeleteCount(startIndex, len, deleteCount, argumentsLen);
// 拷贝删除的元素
sliceDeleteElements(array, startIndex, deleteCount, deleteArr);
//...
}
复制代码
什么是密封对象?
密封对象是不可扩展的对象,并且已有成员的[[Configurable]]属性被设置为false,这意味着不能添加、删除方法和属性。可是属性值是能够修改的。
什么是冻结对象?
冻结对象是最严格的防篡改级别,除了包含密封对象的限制外,还不能修改属性值。
接下来,咱们来把这两种状况一一排除。
// 判断 sealed 对象和 frozen 对象, 即 密封对象 和 冻结对象
if (Object.isSealed(array) && deleteCount !== addElements.length) {
throw new TypeError('the object is a sealed object!')
} else if(Object.isFrozen(array) && (deleteCount > 0 || addElements.length > 0)) {
throw new TypeError('the object is a frozen object!')
}
复制代码
好了,如今就写了一个比较完整的splice,以下:
const sliceDeleteElements = (array, startIndex, deleteCount, deleteArr) => {
for (let i = 0; i < deleteCount; i++) {
let index = startIndex + i;
if (index in array) {
let current = array[index];
deleteArr[i] = current;
}
}
};
const movePostElements = (array, startIndex, len, deleteCount, addElements) => {
// 若是添加的元素和删除的元素个数相等,至关于元素的替换,数组长度不变,被删除元素后面的元素不须要挪动
if (deleteCount === addElements.length) return;
// 若是添加的元素和删除的元素个数不相等,则移动后面的元素
else if(deleteCount > addElements.length) {
// 删除的元素比新增的元素多,那么后面的元素总体向前挪动
// 一共须要挪动 len - startIndex - deleteCount 个元素
for (let i = startIndex + deleteCount; i < len; i++) {
let fromIndex = i;
// 将要挪动到的目标位置
let toIndex = i - (deleteCount - addElements.length);
if (fromIndex in array) {
array[toIndex] = array[fromIndex];
} else {
delete array[toIndex];
}
}
// 注意注意!这里咱们把后面的元素向前挪,至关于数组长度减少了,须要删除冗余元素
// 目前长度为 len + addElements - deleteCount
for (let i = len - 1; i >= len + addElements.length - deleteCount; i --) {
delete array[i];
}
} else if(deleteCount < addElements.length) {
// 删除的元素比新增的元素少,那么后面的元素总体向后挪动
// 思考一下: 这里为何要从后往前遍历?从前日后会产生什么问题?
for (let i = len - 1; i >= startIndex + deleteCount; i--) {
let fromIndex = i;
// 将要挪动到的目标位置
let toIndex = i + (addElements.length - deleteCount);
if (fromIndex in array) {
array[toIndex] = array[fromIndex];
} else {
delete array[toIndex];
}
}
}
};
const computeStartIndex = (startIndex, len) => {
// 处理索引负数的状况
if (startIndex < 0) {
return startIndex + len > 0 ? startIndex + len: 0;
}
return startIndex >= len ? len: startIndex;
}
const computeDeleteCount = (startIndex, len, deleteCount, argumentsLen) => {
// 删除数目没有传,默认删除startIndex及后面全部的
if (argumentsLen === 1)
return len - startIndex;
// 删除数目太小
if (deleteCount < 0)
return 0;
// 删除数目过大
if (deleteCount > len - deleteCount)
return len - startIndex;
return deleteCount;
}
Array.prototype.splice = function(startIndex, deleteCount, ...addElements) {
let argumentsLen = arguments.length;
let array = Object(this);
let len = array.length >>> 0;
let deleteArr = new Array(deleteCount);
startIndex = computeStartIndex(startIndex, len);
deleteCount = computeDeleteCount(startIndex, len, deleteCount, argumentsLen);
// 判断 sealed 对象和 frozen 对象, 即 密封对象 和 冻结对象
if (Object.isSealed(array) && deleteCount !== addElements.length) {
throw new TypeError('the object is a sealed object!')
} else if(Object.isFrozen(array) && (deleteCount > 0 || addElements.length > 0)) {
throw new TypeError('the object is a frozen object!')
}
// 拷贝删除的元素
sliceDeleteElements(array, startIndex, deleteCount, deleteArr);
// 移动删除元素后面的元素
movePostElements(array, startIndex, len, deleteCount, addElements);
// 插入新元素
for (let i = 0; i < addElements.length; i++) {
array[startIndex + i] = addElements[i];
}
array.length = len - deleteCount + addElements.length;
return deleteArr;
}
复制代码
以上代码对照MDN文档中的全部测试用例亲测经过。
相关测试代码请前往: 传送门
最后给你们奉上V8源码,供你们检查: V8数组 splice 源码第 660 行
估计你们对 JS 数组的sort 方法已经不陌生了,以前也对它的用法作了详细的总结。那,它的内部是如何来实现的呢?若是说咱们可以进入它的内部去看一看, 理解背后的设计,会使咱们的思惟和素养获得不错的提高。
sort 方法在 V8 内部相对与其余方法而言是一个比较高深的算法,对于不少边界状况作了反复的优化,可是这里咱们不会直接拿源码来干讲。咱们会来根据源码的思路,实现一个 跟引擎性能同样的排序算法,而且一步步拆解其中的奥秘。
首先大概梳理一下源码中排序的思路:
设要排序的元素个数是n:
插入排序
三路快速排序
在动手以前,我以为咱们有必要为何这么作搞清楚。
第1、为何元素个数少的时候要采用插入排序?
虽然插入排序
理论上说是O(n^2)的算法,快速排序
是一个O(nlogn)级别的算法。可是别忘了,这只是理论上的估算,在实际状况中二者的算法复杂度前面都会有一个系数的, 当 n 足够小的时候,快速排序nlogn
的优点会愈来愈小,假若插入排序O(n^2)前面的系数足够小,那么就会超过快排。而事实上正是如此,插入排序
通过优化之后对于小数据集的排序会有很是优越的性能,不少时候甚至会超过快排。
所以,对于很小的数据量,应用插入排序
是一个很是不错的选择。
第2、为何要花这么大的力气选择哨兵元素?
由于快速排序
的性能瓶颈在于递归的深度,最坏的状况是每次的哨兵都是最小元素或者最大元素,那么进行partition(一边是小于哨兵的元素,另外一边是大于哨兵的元素)时,就会有一边是空的,那么这么排下去,递归的层数就达到了n, 而每一层的复杂度是O(n),所以快排这时候会退化成O(n^2)级别。
这种状况是要尽力避免的!若是来避免?
就是让哨兵元素进可能地处于数组的中间位置,让最大或者最小的状况尽量少。这时候,你就能理解 V8 里面所作的种种优化了。
接下来,咱们来一步步实现的这样的官方排序算法。
最初的插入排序多是这样写的:
const insertSort = (arr, start = 0, end) => {
end = end || arr.length;
for(let i = start; i < end; i++) {
let j;
for(j = i; j > start && arr[j - 1] > arr[j]; j --) {
let temp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = temp;
}
}
return;
}
复制代码
看似能够正确的完成排序,但实际上交换元素会有至关大的性能消耗,咱们彻底能够用变量覆盖的方式来完成,如图所示:
优化后代码以下:
const insertSort = (arr, start = 0, end) => {
end = end || arr.length;
for(let i = start; i < end; i++) {
let e = arr[i];
let j;
for(j = i; j > start && arr[j - 1] > e; j --)
arr[j] = arr[j-1];
arr[j] = e;
}
return;
}
复制代码
接下来正式进入到 sort 方法。
sort的骨架大体以下:
Array.prototype.sort = (comparefn) => {
let array = Object(this);
let length = array.length >>> 0;
return InnerArraySort(array, length, comparefn);
}
const InnerArraySort = (array, length, comparefn) => {
// 比较函数未传入
if (Object.prototype.toString.call(callbackfn) !== "[object Function]") {
comparefn = function (x, y) {
if (x === y) return 0;
x = x.toString();
y = y.toString();
if (x == y) return 0;
else return x < y ? -1 : 1;
};
}
const insertSort = () => {
//...
}
const getThirdIndex = (a, from, to) => {
// 元素个数大于1000时寻找哨兵元素
}
const quickSort = (a, from, to) => {
//哨兵位置
let thirdIndex = 0;
while(true) {
if(to - from <= 10) {
insertSort(a, from, to);
return;
}
if(to - from > 1000) {
thirdIndex = getThirdIndex(a, from , to);
}else {
// 小于1000 直接取中点
thirdIndex = from + ((to - from) >> 2);
}
}
//下面开始快排
}
}
复制代码
咱们先来把求取哨兵位置的代码实现一下:
const getThirdIndex = (a, from, to) => {
let tmpArr = [];
// 递增量,200~215 之间,由于任何正数和15作与操做,不会超过15,固然是大于0的
let increment = 200 + ((to - from) & 15);
let j = 0;
from += 1;
to -= 1;
for (let i = from; i < to; i += increment) {
tmpArr[j] = [i, a[i]];
j++;
}
// 把临时数组排序,取中间的值,确保哨兵的值接近平均位置
tmpArr.sort(function(a, b) {
return comparefn(a[1], b[1]);
});
let thirdIndex = tmpArr[tmpArr.length >> 1][0];
return thirdIndex;
}
复制代码
接下来咱们来完成快排的具体代码:
const _sort = (a, b, c) => {
let arr = [a, b, c];
insertSort(arr, 0, 3);
return arr;
}
const quickSort = (a, from, to) => {
//...
// 上面咱们拿到了thirdIndex
// 如今咱们拥有三个元素,from, thirdIndex, to
// 为了再次确保 thirdIndex 不是最值,把这三个值排序
[a[from], a[thirdIndex], a[to - 1]] = _sort(a[from], a[thirdIndex], a[to - 1]);
// 如今正式把 thirdIndex 做为哨兵
let pivot = a[thirdIndex];
// 正式进入快排
let lowEnd = from + 1;
let highStart = to - 1;
// 如今正式把 thirdIndex 做为哨兵, 而且lowEnd和thirdIndex交换
let pivot = a[thirdIndex];
a[thirdIndex] = a[lowEnd];
a[lowEnd] = pivot;
// [lowEnd, i)的元素是和pivot相等的
// [i, highStart) 的元素是须要处理的
for(let i = lowEnd + 1; i < highStart; i++) {
let element = a[i];
let order = comparefn(element, pivot);
if (order < 0) {
a[i] = a[lowEnd];
a[lowEnd] = element;
lowEnd++;
} else if(order > 0) {
do{
highStart--;
if(highStart === i) break;
order = comparefn(a[highStart], pivot);
}while(order > 0)
// 如今 a[highStart] <= pivot
// a[i] > pivot
// 二者交换
a[i] = a[highStart];
a[highStart] = element;
if(order < 0) {
// a[i] 和 a[lowEnd] 交换
element = a[i];
a[i] = a[lowEnd];
a[lowEnd] = element;
lowEnd++;
}
}
}
// 永远切分大区间
if (lowEnd - from > to - highStart) {
// 继续切分lowEnd ~ from 这个区间
to = lowEnd;
// 单独处理小区间
quickSort(a, highStart, to);
} else if(lowEnd - from <= to - highStart) {
from = highStart;
quickSort(a, from, lowEnd);
}
}
复制代码
测试结果以下:
一万条数据:
十万条数据:
一百万条数据:
一千万条数据:
结果仅供你们参考,由于不一样的node版本对于部分细节的实现可能不同,我如今的版本是v10.15。
从结果能够看到,目前版本的 node 对于有序程度较高的数据是处理的不够好的,而咱们刚刚实现的排序经过反复肯定哨兵的位置就能 有效的规避快排在这一场景下的劣势。
最后给你们完整版的sort代码:
const sort = (arr, comparefn) => {
let array = Object(arr);
let length = array.length >>> 0;
return InnerArraySort(array, length, comparefn);
}
const InnerArraySort = (array, length, comparefn) => {
// 比较函数未传入
if (Object.prototype.toString.call(comparefn) !== "[object Function]") {
comparefn = function (x, y) {
if (x === y) return 0;
x = x.toString();
y = y.toString();
if (x == y) return 0;
else return x < y ? -1 : 1;
};
}
const insertSort = (arr, start = 0, end) => {
end = end || arr.length;
for (let i = start; i < end; i++) {
let e = arr[i];
let j;
for (j = i; j > start && comparefn(arr[j - 1], e) > 0; j--)
arr[j] = arr[j - 1];
arr[j] = e;
}
return;
}
const getThirdIndex = (a, from, to) => {
let tmpArr = [];
// 递增量,200~215 之间,由于任何正数和15作与操做,不会超过15,固然是大于0的
let increment = 200 + ((to - from) & 15);
let j = 0;
from += 1;
to -= 1;
for (let i = from; i < to; i += increment) {
tmpArr[j] = [i, a[i]];
j++;
}
// 把临时数组排序,取中间的值,确保哨兵的值接近平均位置
tmpArr.sort(function (a, b) {
return comparefn(a[1], b[1]);
});
let thirdIndex = tmpArr[tmpArr.length >> 1][0];
return thirdIndex;
};
const _sort = (a, b, c) => {
let arr = [];
arr.push(a, b, c);
insertSort(arr, 0, 3);
return arr;
}
const quickSort = (a, from, to) => {
//哨兵位置
let thirdIndex = 0;
while (true) {
if (to - from <= 10) {
insertSort(a, from, to);
return;
}
if (to - from > 1000) {
thirdIndex = getThirdIndex(a, from, to);
} else {
// 小于1000 直接取中点
thirdIndex = from + ((to - from) >> 2);
}
let tmpArr = _sort(a[from], a[thirdIndex], a[to - 1]);
a[from] = tmpArr[0]; a[thirdIndex] = tmpArr[1]; a[to - 1] = tmpArr[2];
// 如今正式把 thirdIndex 做为哨兵
let pivot = a[thirdIndex];
[a[from], a[thirdIndex]] = [a[thirdIndex], a[from]];
// 正式进入快排
let lowEnd = from + 1;
let highStart = to - 1;
a[thirdIndex] = a[lowEnd];
a[lowEnd] = pivot;
// [lowEnd, i)的元素是和pivot相等的
// [i, highStart) 的元素是须要处理的
for (let i = lowEnd + 1; i < highStart; i++) {
let element = a[i];
let order = comparefn(element, pivot);
if (order < 0) {
a[i] = a[lowEnd];
a[lowEnd] = element;
lowEnd++;
} else if (order > 0) {
do{
highStart--;
if (highStart === i) break;
order = comparefn(a[highStart], pivot);
}while (order > 0) ;
// 如今 a[highStart] <= pivot
// a[i] > pivot
// 二者交换
a[i] = a[highStart];
a[highStart] = element;
if (order < 0) {
// a[i] 和 a[lowEnd] 交换
element = a[i];
a[i] = a[lowEnd];
a[lowEnd] = element;
lowEnd++;
}
}
}
// 永远切分大区间
if (lowEnd - from > to - highStart) {
// 单独处理小区间
quickSort(a, highStart, to);
// 继续切分lowEnd ~ from 这个区间
to = lowEnd;
} else if (lowEnd - from <= to - highStart) {
quickSort(a, from, lowEnd);
from = highStart;
}
}
}
quickSort(array, 0, length);
}
复制代码
参考连接:
new
被调用后作了三件事情:
function newOperator(ctor, ...args) {
if(typeof ctor !== 'function'){
throw 'newOperator function the first param must be a function';
}
let obj = Object.create(ctor.prototype);
let res = ctor.apply(obj, args);
let isObject = typeof res === 'object' && typeof res !== null;
let isFunction = typoof res === 'function';
return isObect || isFunction ? res : obj;
};
复制代码
实现bind以前,咱们首先要知道它作了哪些事情。
对于普通函数,绑定this指向
对于构造函数,要保证原函数的原型对象上的属性不能丢失
Function.prototype.bind = function (context, ...args) {
// 异常处理
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
// 保存this的值,它表明调用 bind 的函数
var self = this;
var fNOP = function () {};
var fbound = function () {
self.apply(this instanceof self ?
this :
context, args.concat(Array.prototype.slice.call(arguments)));
}
fNOP.prototype = this.prototype;
fbound.prototype = new fNOP();
return fbound;
}
复制代码
也能够这么用 Object.create 来处理原型:
Function.prototype.bind = function (context, ...args) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this;
var fbound = function () {
self.apply(this instanceof self ?
this :
context, args.concat(Array.prototype.slice.call(arguments)));
}
fbound.prototype = Object.create(self.prototype);
return fbound;
}
复制代码
引自冴羽
大佬的代码,能够说比较完整了。
Function.prototype.call = function (context) {
var context = context || window;
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context.fn(' + args +')');
delete context.fn
return result;
}
复制代码
不过我认为换成 ES6 的语法会更精炼一些:
Function.prototype.call = function (context, ...args) {
var context = context || window;
context.fn = this;
var result = eval('context.fn(...args)');
delete context.fn
return result;
}
复制代码
相似的,有apply的对应实现:
Function.prototype.apply = function (context, args) {
let context = context || window;
context.fn = this;
let result = eval('context.fn(...args)');
delete context.fn
return result;
}
复制代码
其实JS中的this是一个很是简单的东西,只须要理解它的执行规则就OK。
在这里不想像其余博客同样展现太多的代码例子弄得天花乱坠, 反而不易理解。
call/apply/bind能够显式绑定, 这里就不说了。
主要这些场隐式绑定的场景讨论:
全局上下文默认this指向window, 严格模式下指向undefined。
好比:
let obj = {
a: function() {
console.log(this);
}
}
let func = obj.a;
func();
复制代码
这种状况是直接调用。this至关于全局上下文的状况。
仍是刚刚的例子,我若是这样写:
obj.a();
复制代码
这就是对象.方法
的状况,this指向这个对象
onclick和addEventerListener中 this 默认指向绑定事件的元素。
IE比较奇异,使用attachEvent,里面的this默认指向window。
此时构造函数中的this指向实例对象。
箭头函数没有this, 所以也不能绑定。里面的this会指向当前最近的非箭头函数的this,找不到就是window(严格模式是undefined)。好比:
let obj = {
a: function() {
let do = () => {
console.log(this);
}
do();
}
}
obj.a(); // 找到最近的非箭头函数a,a如今绑定着obj, 所以箭头函数中的this是obj
复制代码
优先级: new > call、apply、bind > 对象.方法 > 直接调用。
首先来直观的感觉一下什么是拷贝。
let arr = [1, 2, 3];
let newArr = arr;
newArr[0] = 100;
console.log(arr);//[100, 2, 3]
复制代码
这是直接赋值的状况,不涉及任何拷贝。当改变newArr的时候,因为是同一个引用,arr指向的值也跟着改变。
如今进行浅拷贝:
let arr = [1, 2, 3];
let newArr = arr.slice();
newArr[0] = 100;
console.log(arr);//[1, 2, 3]
复制代码
当修改newArr的时候,arr的值并不改变。什么缘由?由于这里newArr是arr浅拷贝后的结果,newArr和arr如今引用的已经不是同一块空间啦!
这就是浅拷贝!
可是这又会带来一个潜在的问题:
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr);//[ 1, 2, { val: 1000 } ]
复制代码
咦!不是已经不是同一块空间的引用了吗?为何改变了newArr改变了第二个元素的val值,arr也跟着变了。
这就是浅拷贝的限制所在了。它只能拷贝一层对象。若是有对象的嵌套,那么浅拷贝将无能为力。但幸运的是,深拷贝就是为了解决这个问题而生的,它能 解决无限极的对象嵌套问题,实现完全的拷贝。固然,这是咱们下一篇的重点。 如今先让你们有一个基本的概念。
接下来,咱们来研究一下JS中实现浅拷贝到底有多少种方式?
const shallowClone = (target) => {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? []: {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = target[prop];
}
}
return cloneTarget;
} else {
return target;
}
}
复制代码
可是须要注意的是,Object.assgin() 拷贝的是对象的属性的引用,而不是对象自己。
let obj = { name: 'sy', age: 18 };
const obj2 = Object.assign({}, obj, {name: 'sss'});
console.log(obj2);//{ name: 'sss', age: 18 }
复制代码
let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);//[ 1, 2, 3 ]
复制代码
开头的例子不就说的这个嘛!
let arr = [1, 2, 3];
let newArr = [...arr];//跟arr.slice()是同样的效果
复制代码
上一篇已经解释了什么是深拷贝,如今咱们来一块儿实现一个完整且专业的深拷贝。
JSON.parse(JSON.stringify());
复制代码
估计这个api能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。可是实际上,对于某些严格的场景来讲,这个方法是有巨大的坑的。问题以下:
- 没法解决
循环引用
的问题。举个例子:
const a = {val:2};
a.target = a;
复制代码
拷贝a会出现系统栈溢出,由于出现了无限递归
的状况。
- 没法拷贝一写
特殊的对象
,诸如 RegExp, Date, Set, Map等。
- 没法拷贝
函数
(划重点)。
所以这个api先pass掉,咱们从新写一个深拷贝,简易版以下:
const deepClone = (target) => {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? []: {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop]);
}
}
return cloneTarget;
} else {
return target;
}
}
复制代码
如今,咱们以刚刚发现的三个问题为导向,一步步来完善、优化咱们的深拷贝代码。
如今问题以下:
let obj = {val : 100};
obj.target = obj;
deepClone(obj);//报错: RangeError: Maximum call stack size exceeded
复制代码
这就是循环引用。咱们怎么来解决这个问题呢?
建立一个Map。记录下已经拷贝过的对象,若是说已经拷贝过,那直接返回它行了。
const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;
const deepClone = (target, map = new Map()) => {
if(map.get(target))
return target;
if (isObject(target)) {
map.put(target, true);
const cloneTarget = Array.isArray(target) ? []: {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop]);
}
}
return cloneTarget;
} else {
return target;
}
}
复制代码
如今来试一试:
const a = {val:2};
a.target = a;
let newA = deepClone(a);
console.log(newA)//{ val: 2, target: { val: 2, target: [Circular] } }
复制代码
好像是没有问题了, 拷贝也完成了。但仍是有一个潜在的坑, 就是map 上的 key 和 map 构成了强引用关系
,这是至关危险的。我给你解释一下与之相对的弱引用的概念你就明白了:
在计算机程序设计中,弱引用与强引用相对, 是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并所以可能在任什么时候刻被回收。 --百度百科
说的有一点绕,我用大白话解释一下,被弱引用的对象能够在任什么时候候被回收
,而对于强引用来讲,只要这个强引用还在,那么对象没法被回收
。拿上面的例子说,map 和 a一直是强引用的关系, 在程序结束以前,a 所占的内存空间一直不会被释放
。
怎么解决这个问题?
很简单,让 map 的 key 和 map 构成弱引用
便可。ES6给咱们提供了这样的数据结构,它的名字叫WeakMap
,它是一种特殊的Map, 其中的键是弱引用
的。其键必须是对象,而值能够是任意的。
稍微改造一下便可:
const deepClone = (target, map = new WeakMap()) => {
//...
}
复制代码
对于特殊的对象,咱们使用如下方式来鉴别:
Object.prototype.toString.call(obj);
复制代码
梳理一下对于可遍历对象会有什么结果:
["object Map"]
["object Set"]
["object Array"]
["object Object"]
["object Arguments"]
复制代码
好,以这些不一样的字符串为依据,咱们就能够成功地鉴别这些对象。
const getType = Object.prototype.toString.call(obj);
const canTraverse = {
'[object Map]': true,
'[object Set]': true,
'[object Array]': true,
'[object Object]': true,
'[object Arguments]': true,
};
const deepClone = (target, map = new Map()) => {
if(!isObject(target))
return target;
let type = getType(target);
let cloneTarget;
if(!canTraverse[type]) {
// 处理不能遍历的对象
return;
}else {
// 这波操做至关关键,能够保证对象的原型不丢失!
let ctor = target.prototype;
cloneTarget = new ctor();
}
if(map.get(target))
return target;
map.put(target, true);
if(type === mapTag) {
//处理Map
target.forEach((item, key) => {
cloneTarget.set(deepClone(key), deepClone(item));
})
}
if(type === setTag) {
//处理Set
target.forEach(item => {
target.add(deepClone(item));
})
}
// 处理数组和对象
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop]);
}
}
return cloneTarget;
}
复制代码
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
复制代码
对于不可遍历的对象,不一样的对象有不一样的处理。
const handleRegExp = (target) => {
const { source, flags } = target;
return new target.constructor(source, flags);
}
const handleFunc = (target) => {
// 待会的重点部分
}
const handleNotTraverse = (target, tag) => {
const Ctor = targe.constructor;
switch(tag) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(target);
case regexpTag:
return handleRegExp(target);
case funcTag:
return handleFunc(target);
default:
return new Ctor(target);
}
}
复制代码
虽然函数也是对象,可是它过于特殊,咱们单独把它拿出来拆解。
提到函数,在JS种有两种函数,一种是普通函数,另外一种是箭头函数。每一个普通函数都是 Function的实例,而箭头函数不是任何类的实例,每次调用都是不同的引用。那咱们只须要 处理普通函数的状况,箭头函数直接返回它自己就行了。
那么如何来区分二者呢?
答案是: 利用原型。箭头函数是不存在原型的。
代码以下:
const handleFunc = (func) => {
// 箭头函数直接返回自身
if(!func.prototype) return func;
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
// 分别匹配 函数参数 和 函数体
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if(!body) return null;
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
}
复制代码
到如今,咱们的深拷贝就实现地比较完善了。不过在测试的过程当中,我也发现了一个小小的bug。
以下所示:
const target = new Boolean(false);
const Ctor = target.constructor;
new Ctor(target); // 结果为 Boolean {true} 而不是 false。
复制代码
对于这样一个bug,咱们能够对 Boolean 拷贝作最简单的修改, 调用valueOf: new target.constructor(target.valueOf())。
但实际上,这种写法是不推荐的。由于在ES6后不推荐使用【new 基本类型()】这 样的语法,因此es6中的新类型 Symbol 是不能直接 new 的,只能经过 new Object(SymbelType)。
所以咱们接下来统一一下:
const handleNotTraverse = (target, tag) => {
const Ctor = targe.constructor;
switch(tag) {
case boolTag:
return new Object(Boolean.prototype.valueOf.call(target));
case numberTag:
return new Object(Number.prototype.valueOf.call(target));
case stringTag:
return new Object(String.prototype.valueOf.call(target));
case errorTag:
case dateTag:
return new Ctor(target);
case regexpTag:
return handleRegExp(target);
case funcTag:
return handleFunc(target);
default:
return new Ctor(target);
}
}
复制代码
OK!是时候给你们放出完整版的深拷贝啦:
const getType = obj => Object.prototype.toString.call(obj);
const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;
const canTraverse = {
'[object Map]': true,
'[object Set]': true,
'[object Array]': true,
'[object Object]': true,
'[object Arguments]': true,
};
const mapTag = '[object Map]';
const setTag = '[object Set]';
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
const handleRegExp = (target) => {
const { source, flags } = target;
return new target.constructor(source, flags);
}
const handleFunc = (func) => {
// 箭头函数直接返回自身
if(!func.prototype) return func;
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
// 分别匹配 函数参数 和 函数体
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if(!body) return null;
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
}
const handleNotTraverse = (target, tag) => {
const Ctor = target.constructor;
switch(tag) {
case boolTag:
return new Object(Boolean.prototype.valueOf.call(target));
case numberTag:
return new Object(Number.prototype.valueOf.call(target));
case stringTag:
return new Object(String.prototype.valueOf.call(target));
case symbolTag:
return new Object(Symbol.prototype.valueOf.call(target));
case errorTag:
case dateTag:
return new Ctor(target);
case regexpTag:
return handleRegExp(target);
case funcTag:
return handleFunc(target);
default:
return new Ctor(target);
}
}
const deepClone = (target, map = new WeakMap()) => {
if(!isObject(target))
return target;
let type = getType(target);
let cloneTarget;
if(!canTraverse[type]) {
// 处理不能遍历的对象
return handleNotTraverse(target, type);
}else {
// 这波操做至关关键,能够保证对象的原型不丢失!
let ctor = target.constructor;
cloneTarget = new ctor();
}
if(map.get(target))
return target;
map.set(target, true);
if(type === mapTag) {
//处理Map
target.forEach((item, key) => {
cloneTarget.set(deepClone(key, map), deepClone(item, map));
})
}
if(type === setTag) {
//处理Set
target.forEach(item => {
cloneTarget.add(deepClone(item, map));
})
}
// 处理数组和对象
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop], map);
}
}
return cloneTarget;
}
复制代码
若是你以为这篇内容对你挺有启发,我想邀请你帮我两个小忙:
点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
关注公众号「前端三元同窗」,每日坚持灵魂之问,碰见更好的本身!