前端算法知识大全
一.数组
1.重磅!超详细的 JS 数组方法整理出来了
在本节,我们仅仅围绕数组最基本的操作进行介绍,这远不是数组的全部。关于数组,还有太多太多的故事要讲——实际上,单就其重要的方法的使用:如concat、some、slice、join、sort、pop、push 等等这些,就足以说上个把钟头。
本节暂时不对数组 API 作集中讲解,因为罗列 API 没有意义——脱离场景去记忆 API 实在是一件太痛苦的事情,这会挫伤各位继续走下去的积极性。
关于数组的更多特性和技巧,会被打散到后续的章节中去。各位在真题解读的环节、包括在其它数据结构的讲解中,都会不可避免地再见到数组的身影。彼时数组的每一个方法都会和它对应的应用场景一起出现,相信你会有更深刻的记忆。
事实上,在 JavaScript 数据结构中,数组几乎是“基石”一般的存在。这一点,大家在下一节就会有所感触。
(一)数组的创建、访问、检测
数组是各位要认识的第一个数据结构。
作为最简单、最基础的数据结构,大多数的语言都天然地对数组有着原生的表达,JavaScript 亦然。这意味着我们可以对数组做到“开箱即用”,而不必自行模拟实现,非常方便。
考虑到日常开发过程中,数组的出镜率本身已经很高,相信它也是大多数同学最熟悉的数据结构。 即便如此,这里仍然需要提醒各位:要对数组格外走点心,毕竟后面需要它帮忙的地方会非常多。
1.认识数组并创建数组
性质: 数组是一种线性表数据结构,它用一组连续的内存空间,来存储一组具有相同类型的数据。
区别: 与其他编程语言不同,JavaScript 中的数组长度可以随时改变,数组中的每个槽位可以储存任意类型的数据,并且其数据在内存中也可以不连续。
(1) 使用数组字面量表示法
大家平时用的最多的创建方式想必就是直接方括号+元素内容这种形式:
var arr4 = []; //创建一个空数组
var arr5 = [20]; // 创建一个包含1项数据为20的数组
var arr6 = ["lily","lucy","Tom"]; // 创建一个包含3个字符串的数组
(2)使用 Array 构造函数
不过在算法题中,很多时候我们初始化一个数组时,并不知道它内部元素的情况。这种场景下,要给大家推荐的是构造函数创建数组的方法:
无参构造
var arr1 = new Array(); //创建一个空数组
当我们以构造函数的形式创建数组时,若我们像楼上这样,不传任何参数,得到的就会是一个空数组。等价于:
const arr1 = []
带参构造
①不过咱们使用构造函数,可不是为了创建空数组这么无聊。
我们需要它的时候,往往是因为我们有“创造指定长度的空数组”这样的需求。需要多长的数组,就给它传多大的参数:
const arr = new Array(7)
这样的写法就可以得到一个长度为7的数组:
②在一些场景中,这个需求会稍微变得有点复杂—— “创建一个长度确定、同时每一个元素的值也都确定的数组”。这时我们可以调用 fill 方法,假设需求是每个坑里都填上一个1,只需给它 fill 一个1:
const arr = (new Array(7)).fill(1)
如此便可以得到一个长度为7,且每个元素都初始化为1的数组:
③如果传入一个非数值的参数或者参数个数大于 1,则表示创建一个包含指定元素的数组
var arr3 = new Array("lily","lucy","Tom"); // 创建一个包含3个字符串的数组
var array4 = new Array('23'); // ["23"]
(3)Array.of 方法创建数组(es6 新增)
ES6 为数组新增创建方法的目的之一,是帮助开发者在使用 Array 构造器时避开 js 语言的一个怪异点。
Array.of()方法总会创建一个包含所有传入参数的数组,而不管参数的数量与类型。
let arr = Array.of(1, 2);
console.log(arr.length);//2
let arr1 = Array.of(3);
console.log(arr1.length);//1
console.log(arr1[0]);//3
let arr2 = Array.of('2');
console.log(arr2.length);//1
console.log(arr2[0]);//'2'
(4)Array.from 方法创建数组(es6 新增)
在 js 中将非数组对象转换为真正的数组是非常麻烦的。在 ES6 中,将可迭代对象或者类数组对象作为第一个参数传入,Array.from()就能返回一个数组。
function arga(...args) { //...args剩余参数数组,由传递给函数的实际参数提供
let arg = Array.from(args);
console.log(arg);
}
arga('arr1', 26, 'from'); // ['arr1',26,'from']
映射转换
如果你想实行进一步的数组转换,你可以向 Array.from()方法传递一个映射用的函数作为第二个参数。此函数会将数组对象的每一个值转换为目标形式,并将其存储在目标数组的对应位置上。
function arga(...args) {
return Array.from(args, value => value + 1);
}
let arr = arga('arr', 26, 'pop');
console.log(arr);//['arr1',27,'pop1']
如果映射函数需要在对象上工作,你可以手动传递第三个参数给 Array.from()方法,从而指定映射函数内部的 this 值。
const helper = {
diff: 1,
add(value) {
return value + this.diff;
}
}
function translate() {
//arguments 是一个对应于传递给函数的参数的类数组对象
return Array.from(arguments, helper.add, helper);
}
let arr = translate('liu', 26, 'man');
console.log(arr); // ["liu1", 27, "man1"]
2.数组的访问
① 访问数组中的元素我们可以根据索引下标随机访问(时间复杂度为 O(1)),直接在中括号中指定其索引即可,这个索引通常来说是数字,用来计算元素之间的存储位置的偏移量。
arr[0] // 访问索引下标为0的元素
② 上文提到,这个索引通常是数字,也就是说在 JavaScript 中,通过字符串也可以访问对应的元素:
arr['1'] // 1
其实,JavaScript 中的数组是一种比较特殊的对象,因为在 JavaScript 中,对象的属性名必须是字符串,这些数字索引就被转化成了字符串类型。
3.检测数组的六种方法
let arr = []
// 1. instanceof
arr instanceof Array
// 2. constructor
arr.constructor === Array
// 3. Object.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(arr)
// 4. getPrototypeOf
Object.getPrototypeOf(arr) === Array.prototype
// 5. Object.prototype.toString
Object.prototype.toString.call(arr) === '[object Array]'
// 6. Array.isArray ES6 新增
Array.isArray(arr)
1.Object.prototype.toString.call()
能准确的检测所有的数据类型
每一个继承Object的对象都有一个toString方法,如果toString方法没有被重写,会返回[Object type], type为对象的类型。
const arr = ["a", "b"]
arr.toString() // => "a, b"
Object.prototype.toString.call(arr) // => "[Object Array]"
2.Array.isArray()
用于判断对象是否为数组
Array.isArray()是ES5推出的,不支持IE6~8,使用时应考虑兼容
Array.isArray([1, 2, 3, 4]) // true
Array.isArray({a: 1}) // false
Array.isArray(new Array) // true
Array.isArray("string") // false
if(typeof Array.isArray != 'function') {
Array.isArray = function(obj){
return Object.prototype.toString.call(obj) == '[object Array]'
}
}
3.instanceof
能检测array、function、object类型
instanceof用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上,即 A 是否为 B 的实例:A instanceof B
1 instanceof Number // false
'str' instanceof String // false
true instanceof Boolean // false
[1,2,3] instanceof Array // true
function () {} instanceof Function // true
{} instanceof Object // true
instanceof 的缺点:是否处于原型链上的判断方法不严谨 instanceof 方法判断的是是否处于原型链上,而不是是不是处于原型链最后一位,所以会出现以下情况:
var arr = [1, 2, 3]
arr instanceof Array // true
arr instanceof Object // true
function fn() {}
fn instanceof Function // true
fn instanceof Object // true
所有原型链的尽头都是Object
instanceof, constructor,Object.prototype.isPrototypeOf,getPrototypeOf比较渣,丝毫不负责任,比如我们将 arr 的 __proto__
指向了 Array.prototype 后:
let arr = {
__proto__: Array.prototype
}
// 1. instanceof
arr instanceof Array // true
// 2. constructor
arr.constructor === Array // true
// 3. Object.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(arr) // true
// 4. getPrototypeOf
Object.getPrototypeOf(arr) === Array.prototype // true
4.比较数组
5.清空数组
要清除数组,请将数组的长度设置为 0:
let array = [“A”, “B”, “C”, “D”, “E”, “F”]
array.length = 0
console.log(array) // []
6.数组的分组
[1, 2, 3, 4, 5, 6, 7, 8, 9] => [[1, 2, 3],[4, 5, 6],[7, 8, 9]],把一个一维数组变成三个三个的二维数组。
function convertTo2DArray(arr, chunkSize) {
var result = [];
for (var i = 0; i < arr.length; i += chunkSize) {
result.push(arr.slice(i, i + chunkSize));
}
return result;
}
var inputArray = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var outputArray = convertTo2DArray(inputArray, 3);
console.log(outputArray);
输出结果将是: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
slice 不会修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组,不信的话自己可以编译一下。
这段代码中的convertTo2DArray函数接受两个参数:arr表示输入的一维数组,chunkSize表示每个子数组的大小。它使用slice方法来从输入数组中提取每个子数组,并使用循环来遍历整个数组并构建输出二维数组。最后,它返回生成的二维数组。
(二)二维数组
初学编程的同学基础如果比较薄弱,会对二维数组完全没有概念。这里咱们先简单介绍下:二维数组其实就是数组套数组,也就是每个元素都是数组的数组。
说起来有点绕口,咱们直接上图来看:
const arr = [1,2,3,4,5]
这个数组在逻辑上的分布就是这样式儿的:
像图上这样,数组的元素是数字而非数组。整个数组的结构看上去宛如一条“线”,这就是一维数组。
而“每个元素都是数组的数组”,代码里看是这样:
const arr = [
[1,2,3,4,5],
[1,2,3,4,5],
[1,2,3,4,5],
[1,2,3,4,5],
[1,2,3,4,5]
]
直接把它的逻辑结构画出来看,是这样:
图中的每一行,就代表着一个数组元素。比如第 0 行,就代表着数组中 arr[0] 这个数组元素,其内容是 [1,2,3,4,5]。
每一行中的每一列,则代表一个确切的“坑”。比如第 0 行第 1 列,就代表着数组中 arr[0][1] 这个元素,其值为2,是一个确切的 number。
明白了二维数组的索引规律,现在我们来看一下二维数组的特点:从形状上看,相对于一维数组一条“线”一般的布局,二维数组更像是一个“面”。拿咱们这个例子来说,这里的二维数组逻辑分布图就宛如一个正方形。当然啦,如果我们稍微延长一下其中的一边,它也可以是一个矩形。
在数学中,形如这样长方阵列排列的复数或实数集合,被称为“矩阵”。因此二维数组的别名就叫“矩阵”。
讲到这里,如果有对“矩阵”的定义一脸懵逼的同学,也不用怕——不知道“矩阵”是啥,一点不打紧(所以快停下你复制粘贴到 Google 的手哈哈),但你必须要记住“矩阵”和“二维数组”之间的等价关系。 在算法题目中,见到“矩阵”时,能够立刻反射出它说的是二维数组,不被别名整懵逼,这就够了。
二维数组的初始化
fill 的局限性
有同学用 fill 方法用顺了手,就本能地想用 fill 解决所有的问题,比如初始化一个二维数组:
const arr =(new Array(7)).fill([])
乍一看没啥毛病,7个坑都被乖乖地填上了数组元素:
但是当你想修改某一个坑里的数组的值的时候:
arr[0][0] = 1
你会发现一整列的元素都被设为了 1:
这是什么骚操作???
这就要从 fill 的工作机制讲起了。各位要清楚,当你给 fill 传递一个入参时,如果这个入参的类型是引用类型,那么 fill 在填充坑位时填充的其实就是入参的引用。 也就是说下图中虽然看似我们给7个坑位各初始化了一个数组:
其实这7个数组对应了同一个引用、指向的是同一块内存空间,它们本质上是同一个数组。因此当你修改第0行第0个元素的值时,第1-6行的第0个元素的值也都会跟着发生改变。
初始化一个二维数组
本着安全的原则,这里我推荐大家采纳的二维数组初始化方法非常简单(而且性能也不错)。直接用一个 for 循环来解决:
const len = arr.length
for(let i=0;i<len;i++) {
// 将数组的每一个坑位初始化为数组
arr[i] = []
}
for 循环中,每一次迭代我们都通过“[]”来创建一个新的数组,这样便不会有引用指向问题带来的尴尬。
二维数组的访问
访问二维数组和访问一维数组差别不大,区别在于我们现在需要的是两层循环:
// 缓存外部数组的长度
const outerLen = arr.length
for(let i=0;i<outerLen;i++) {
// 缓存内部数组的长度
const innerLen = arr[i].length
for(let j=0;j<innerLen;j++) {
// 输出数组的值,输出数组的索引
console.log(arr[i][j],i,j)
}
}
一维数组用 for 循环遍历只需一层循环,二维数组是两层,三维数组就是三层。依次类推,N 维数组需要 N 层循环来完成遍历。
(三)数组方法
19个JavaScript数组常用方法总结
数组原型方法主要有以下这些
截至 ES10 规范,数组共包含 35 个标准的 API 方法和一个非标准的 API 方法。
创建数组:Array.of、Array.from
改变自身(9 种):pop、push、shift、unshift、reverse、sort、splice、copyWithin、fill
不改变自身(12 种):concat、join、slice、toString、toLocaleString、valueOf、indexOf、lastIndexOf、未形成标准的 toSource,以及 ES7 新增的方法 includes,以及 ES10 新增的方法 flat、flatMap
不会改变自身的遍历方法一共有(12 种):forEach、every、some、filter、map、reduce、reduceRight,以及 ES6 新增的方法 find、findIndex、keys、values、entries
1.基本操作方法
数组基本操作可以归纳为 增、删、改、查,需要留意的是哪些方法会对原数组产生影响,哪些方法不会。
增
下面前三种是对原数组产生影响的增添方法,第四种则不会对原数组产生影响
- unshift():向数组首位添加新元素
- push() :向数组的末尾添加新元素
- splice():对数组进行增删改
- concat():用于连接两个或多个数组
删
下面三种都会影响原数组,最后一项不影响原数组:
- pop():删除数组的最后一项
- shift():删除数组的第一项
- splice():对数组进行增删改
- slice():按照条件查找出其中的部分元素
改
- splice():对数组进行增删改
查
- indexOf():检测当前值在数组中第一次出现的位置索引
- lastIndexOf(): 检测当前值在数组中最后一次出现的位置索引
- findIndex(): 返回匹配位置的索引
- find(): 返回匹配的值
- includes():判断一个数组是否包含一个指定的值
2.排序方法
数组有两个方法可以用来对元素重新排序:
- reverse():对数组进行倒序
- sort():对数组的元素进行排序
3.转换方法
- join():用指定的分隔符将数组每一项拼接为字符串
- toLocaleString()、toString():将数组转换为字符串
4.迭代方法
常用来迭代数组的方法(都不改变原数组)有如下:
- some():判断数组中是否存在满足条件的项
- every():判断数组中每一项都是否满足条件
- forEach():ES5 及以下循环遍历数组每一项
- filter(): “过滤”功能
- map():ES6 循环遍历数组每一项
- entries() 、keys() 、values(): 遍历数组
5.其他方法
- fill(): 方法能使用特定值填充数组中的一个或多个元素
- copyWithin():用于从数组的指定位置拷贝元素到数组的另一个指定位置中
- flat()、flatMap():扁平化数组
- reduce() 和 reduceRight()累加
各个方法的基本功能详解
1. unshift()和shift()
unshift() 方法可向数组的开头添加一个或更多元素,并返回新的长度。
shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。
var arr = ["Lily","lucy","Tom"];
var count = arr.unshift("Jack","Sean");
console.log(count); // 5
console.log(arr); //["Jack", "Sean", "Lily", "lucy", "Tom"]
var item = arr.shift();
console.log(item); // Jack
console.log(arr); // ["Sean", "Lily", "lucy", "Tom"]
2.push()和 pop()
push() 方法从数组末尾向数组添加元素,可以添加一个或多个元素。
pop() 方法用于删除数组的最后一个元素并返回删除的元素。
var arr = ["Lily","lucy","Tom"];
var count = arr.push("Jack","Sean");
console.log(count); // 5
console.log(arr); // ["Lily", "lucy", "Tom", "Jack", "Sean"]
var item = arr.pop();
console.log(item); // Sean
console.log(arr); // ["Lily", "lucy", "Tom", "Jack"]
3.splice()
splice():很强大的数组方法,它有很多种用法,可以实现删除、插入和替换。
①删除元素,并返回删除的元素
可以删除任意数量的项,只需指定 2 个参数:要删除的第一项的位置和要删除的项数。例如, splice(0,2)会删除数组中的前两项。
var arr = [1,3,5,7,9,11];
var arrRemoved = arr.splice(0,2);
console.log(arr); //[5, 7, 9, 11]
console.log(arrRemoved); //[1, 3]
②向指定索引处添加元素
可以向指定位置插入任意数量的项,只需提供 3 个参数:起始位置、 0(要删除的项数)和要插入的项。例如,splice(2,0,4,6)会从当前数组的位置 2 开始插入 4 和 6。
var array1 = [22, 3, 31, 12];
array1.splice(1, 0, 12, 35); //[]
console.log(array1); // [22, 12, 35, 3, 31, 12]
③替换指定索引位置的元素
可以向指定位置插入任意数量的项,且同时删除任意数量的项,只需指定 3 个参数:起始位置、要删除的项数和要插入的任意数量的项。插入的项数不必与删除的项数相等。例如,splice (2,1,4,6)会删除当前数组位置 2 的项,然后再从位置 2 开始插入 4 和 6。
const array1 = [22, 3, 31, 12];
array1.splice(1, 1, 8); //[3]
console.log(array1); // [22, 8, 31, 12]
4.concat()
concat() 方法用于连接两个或多个数组。
该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。
var arr = [1,3,5,7];
var arrCopy = arr.concat(9,[11,13]);
console.log(arrCopy); //[1, 3, 5, 7, 9, 11, 13]
console.log(arr); // [1, 3, 5, 7](原数组未被修改)
从上面测试结果可以发现:传入的不是数组,则直接把参数添加到数组后面,如果传入的是数组,则将数组中的各个项添加到数组中。但是如果传入的是一个二维数组呢?
var arrCopy2 = arr.concat([9,[11,13]]);
console.log(arrCopy2); //[1, 3, 5, 7, 9, Array[2]]
console.log(arrCopy2[5]); //[11, 13]
5.slice()
slice():返回从原数组中指定开始下标到结束下标之间的项组成的新数组。
slice()方法可以接受一或两个参数,即要返回项的起始和结束位置。
在只有一个参数的情况下, slice()方法返回从该参数指定位置开始到当前数组末尾的所有项。
如果有两个参数,该方法返回起始和结束位置之间的项,但不包括结束位置的项。
当出现负数时,将负数加上数组长度的值来替换该位置的数
var arr = [1,3,5,7,9,11];
var arrCopy = arr.slice(1);
var arrCopy2 = arr.slice(1,4);
var arrCopy3 = arr.slice(1,-2);//相当于arr.slice(1,4)
var arrCopy4 = arr.slice(-4,-1);//相当于arr.slice(2,5)
console.log(arr); //[1, 3, 5, 7, 9, 11](原数组没变)
console.log(arrCopy); //[3, 5, 7, 9, 11]
console.log(arrCopy2); //[3, 5, 7]
console.log(arrCopy3); //[3, 5, 7]
console.log(arrCopy4); //[5, 7, 9]
6.includes() es7 新增
includes() 方法用来判断一个数组是否包含一个指定的值,如果是返回 true,否则 false。
参数有两个,其中第一个是(必填)需要查找的元素值,第二个是(可选)开始查找元素的位置
const array1 = [22, 3, 31, 12, 'arr'];
const includes = array1.includes(31);
console.log(includes); // true
const includes1 = array1.includes(31, 3); // 从索引3开始查找31是否存在
console.log(includes1); // false
需要注意的是:includes使用===运算符来进行值比较,仅有一个例外:NaN 被认为与自身相等。
let values = [1, NaN, 2];
console.log(values.indexOf(NaN));//-1
console.log(values.includes(NaN));//true
7. find()和 findIndex()
find()与 findIndex()方法均接受两个参数:一个回调函数,一个可选值用于指定回调函数内部的 this。
该回调函数可接受三个参数:数组的某个元素,该元素对应的索引位置,以及该数组本身。
该回调函数应当在给定的元素满足你定义的条件时返回 true,而 find()和 findIndex()方法均会在回调函数第一次返回 true 时停止查找。
二者的区别是:find()方法返回匹配的值,而 findIndex()返回匹配位置的索引。
let arr = [1, 2, 3, 'arr', 5, 1, 9];
console.log(arr.find((value, keys, arr) => {
return value > 2;
})); // 3 返回匹配的值
console.log(arr.findIndex((value, keys, arr) => {
return value > 2;
})); // 2 返回匹配位置的索引
8.indexOf()和 lastIndexOf()
接收两个参数:要查找的项和(可选的)表示查找起点位置的索引。
indexOf(): 从数组的开头(位置 0)开始向后查找。
lastIndexOf: 从数组的末尾开始向前查找。
这两个方法都返回要查找的项在数组中的位置,或者在没找到的情况下返回-1。在比较第一个参数与数组中的每一项时,会使用全等操作符。
var arr = [1,3,5,7,7,5,3,1];
console.log(arr.indexOf(5)); //2
console.log(arr.lastIndexOf(5)); //5
console.log(arr.indexOf(5,2)); //2
console.log(arr.lastIndexOf(5,4)); //2
console.log(arr.indexOf("5")); //-1
9.sort()
sort() 方法用于对数组的元素进行排序。
排序顺序可以是字母或数字,并按升序或降序。
默认排序顺序为按字母升序。
var arr1 = ["a", "d", "c", "b"];
console.log(arr1.sort()); // ["a", "b", "c", "d"]
arr2 = [13, 24, 51, 3];
console.log(arr2.sort()); // [13, 24, 3, 51]
console.log(arr2); // [13, 24, 3, 51](元数组被改变)
为了解决上述问题,sort()方法可以接收一个比较函数作为参数,以便我们指定哪个值位于哪个值的前面。
比较函数接收两个参数,如果第一个参数应该位于第二个之前则返回一个负数,如果两个参数相等则返回 0,如果第一个参数应该位于第二个之后则返回一个正数。以下就是一个简单的比较函数:
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
arr2 = [13, 24, 51, 3];
console.log(arr2.sort(compare)); // [3, 13, 24, 51]
如果需要通过比较函数产生降序排序的结果,只要交换比较函数返回的值即可:
function compare(value1, value2) {
if (value1 < value2) {
return 1;
} else if (value1 > value2) {
return -1;
} else {
return 0;
}
}
arr2 = [13, 24, 51, 3];
console.log(arr2.sort(compare)); // [51, 24, 13, 3]
10.reverse()
reverse() 方法用于颠倒数组中元素的顺序。
var arr = [13, 24, 51, 3];
console.log(arr.reverse()); //[3, 51, 24, 13]
console.log(arr); //[3, 51, 24, 13](原数组改变)
11.join()
join()方法用于把数组中的所有元素转换一个字符串。
元素是通过指定的分隔符进行分隔的。默认使用逗号作为分隔符
var arr = [1,2,3];
console.log(arr.join()); // 1,2,3
console.log(arr.join("-")); // 1-2-3
console.log(arr); // [1, 2, 3](原数组不变)
通过join()方法可以实现重复字符串,只需传入字符串以及重复的次数,就能返回重复后的字符串,函数如下:
function repeatString(str, n) {
//一个长度为n+1的空数组用string去拼接成字符串,就成了n个string的重复
return new Array(n + 1).join(str);
}
console.log(repeatString("abc", 3)); // abcabcabc
console.log(repeatString("Hi", 5)); // HiHiHiHiHi
12.toLocaleString() 和 toString()
将数组转换为字符串
const array1 = [22, 3, 31, 12];
const str = array1.toLocaleString();
const str1 = array1.toString();
console.log(str); // 22,3,31,12
console.log(str1); // 22,3,31,12
13.some()
some():判断数组中是否存在满足条件的项,只要有一项满足条件,就会返回 true。
var arr = [1, 2, 3, 4, 5];
var arr2 = arr.some(function(x) {
return x < 3;
});
console.log(arr2); //true
var arr3 = arr.some(function(x) {
return x < 1;
});
console.log(arr3); // false
14.every()
every():判断数组中每一项都是否满足条件,只有所有项都满足条件,才会返回 true。
var arr = [1, 2, 3, 4, 5];
var arr2 = arr.every(function(x) {
return x < 10;
});
console.log(arr2); //true
var arr3 = arr.every(function(x) {
return x < 3;
});
console.log(arr3); // false
15.forEach()
forEach():对数组进行遍历循环,对数组中的每一项运行给定函数。这个方法没有返回值。参数都是 function 类型,默认有传。
参数分别为:遍历的数组内容;第对应的数组索引,数组本身
var arr = [11, 22, 33, 44, 55];
arr.forEach(function(x, index, a){
console.log(x + '|' + index + '|' + (a === arr));
});
输出为:
11|0|true
22|1|true
33|2|true
44|3|true
55|4|true
16.map()
map() 方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。
map() 方法按照原始数组元素顺序依次处理元素。该方法不会改变原数组
var arr = [1, 2, 3, 4, 5];
var arr2 = arr.map(function(item){
return item*item;
});
console.log(arr2); //[1, 4, 9, 16, 25]
17.filter()
filter():“过滤”功能,数组中的每一项运行给定函数,返回满足过滤条件组成的数组。
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var arr2 = arr.filter(function(x, index) {
return index % 3 === 0 || x >= 8;
});
console.log(arr2); //[1, 4, 7, 8, 9, 10]
18. entries(),keys() 和 values() 【ES6】
entries(),keys()和values() —— 用于遍历数组。它们都返回一个遍历器对象,可以用for…of循环进行遍历
区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
如果不使用for…of循环,可以手动调用遍历器对象的next方法,进行遍历。
let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']
19.fill() es6 新增
fill()方法能使用特定值填充数组中的一个或多个元素。当只是用一个参数时,该方法会用该参数的值填充整个数组。
let arr = [1, 2, 3, 'cc', 5];
arr.fill(1);
console.log(arr);//[1,1,1,1,1];
如果不想改变数组中的所有元素,而只是想改变其中一部分,那么可以使用可选的起始位置参数与结束位置参数(不包括结束位置的那个元素)
3 个参数: 填充数值,起始位置参数,结束位置参数(不包括结束位置的那个元素)
let arr = [1, 2, 3, 'arr', 5];
arr.fill(1, 2);
console.log(arr);//[1,2,1,1,1]
arr.fill(0, 1, 3);
console.log(arr);//[1,0,0,1,1];
20.reduce()和 reduceRight()
①以前我没得选,现在我只想用 Array.prototype.reduce
②reduce方法高级使用
③13个JavaScript数组reduce的实例方法
这两个方法都会实现迭代数组的所有项(即累加器),然后构建一个最终返回的值。
reduce()方法从数组的第一项开始,逐个遍历到最后。
reduceRight()则从数组的最后一项开始,向前遍历到第一项。
4 个参数:前一个值、当前值、项的索引和数组对象
var values = [1,2,3,4,5];
var sum = values.reduceRight(function(prev, cur, index, array){
return prev + cur;
},10); //数组一开始加了一个初始值10,可以不设默认0
console.log(sum); //25
21.copyWithin() [es6 新增]
copyWithin() 方法用于从数组的指定位置拷贝元素到数组的另一个指定位置中。
该方法会改变现有数组
//将数组的前两个元素复制到数组的最后两个位置
let arr = [1, 2, 3, 'arr', 5];
arr.copyWithin(3, 0);
console.log(arr);//[1,2,3,1,2]
默认情况下,copyWithin()方法总是会一直复制到数组末尾,不过你还可以提供一个可选参数来限制到底有多少元素会被覆盖。这第三个参数指定了复制停止的位置(不包含该位置本身)。
let arr = [1, 2, 3, 'arr', 5, 9, 17];
//从索引3的位置开始粘贴
//从索引0的位置开始复制
//遇到索引3时停止复制
arr.copyWithin(3, 0, 3);
console.log(arr);//[1,2,3,1,2,3,17]
22.flat() 和 flatMap() es6 新增
flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
该方法返回一个新数组,对原数据没有影响。
参数: 指定要提取嵌套数组的结构深度,默认值为 1。
const arr1 = [0, 1, 2, [3, 4]];
console.log(arr1.flat());
// expected output: [0, 1, 2, 3, 4]
const arr2 = [0, 1, 2, [[[3, 4]]]];
console.log(arr2.flat(2));
// expected output: [0, 1, 2, [3, 4]]
//使用 Infinity,可展开任意深度的嵌套数组
var arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
arr4.flat(Infinity);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// 扁平化数组空项,如果原数组有空位,flat()方法会跳过空位
var arr4 = [1, 2, , 4, 5];
arr4.flat();
// [1, 2, 4, 5]
flatMap()方法对原数组的每个成员执行一个函数,相当于执行Array.prototype.map(),然后对返回值组成的数组执行flat()方法。
该方法返回一个新数组,不改变原数组。
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
(四)数组去重
参考阅读:盘点JS中数组去重写法
4.1创建一个新数组(不重复的放进去)
缺点:性能不太好,因为要开辟一个新的堆内存空间
1. 使用indexOf方法
另一种常见的数组去重方法是遍历原数组,将每个元素与新数组中的元素进行比较,如果新数组中不存在该元素,则将其添加到新数组中。
let arr = [1, 2, 3, 4, 3, 2, 1];
let uniqueArr = [];
for (let i = 0; i < arr.length; i++) {
if (uniqueArr.indexOf(arr[i]) === -1) {
uniqueArr.push(arr[i]);
}
}
console.log(uniqueArr); // [1, 2, 3, 4]
以上代码我们使用indexOf方法判断新数组uniqueArr中是否已经存在当前元素,如果不存在则将其添加到新数组中。
2.使用 findIndex 方法
与indexOf类似,使用findIndex函数可以在数组中查找元素的索引,如果找到重复的元素,其索引值会大于首次出现的索引值。因此,可以遍历数组,当findIndex返回的索引值大于当前索引时,说明该元素是重复的,可以过滤掉。
let array = [1, 2, 2, 3, 4, 4, 5];
let uniqueArray = [];
for (let i = 0; i < array.length; i++) {
if (array.findIndex(value => value === array[i]) === i) {
uniqueArray.push(array[i]);
}
}
console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5]
3. 使用includes方法
ES7中新增了includes方法,该方法可以更方便地判断数组中是否包含某个元素。我们可以利用这个特性来实现数组去重。
let arr = [1, 2, 3, 4, 3, 2, 1];
let uniqueArr = [];
for (let i = 0; i < arr.length; i++) {
if (!uniqueArr.includes(arr[i])) {
uniqueArr.push(arr[i]);
}
}
console.log(uniqueArr); // [1, 2, 3, 4]
上述代码中,我们使用includes方法判断新数组uniqueArr中是否已经存在当前元素,如果不存在则将其添加到新数组中。
4. 利用 for of 循环和 includes 方法
这种方法与上一个方法类似,但使用了 for…of 循环,可读性更强。
let arr = [1, 2, 2, 3, 4, 4, 5];
let uniqueArr = [];
for (let value of arr) {
if (!uniqueArr.includes(value)) {
uniqueArr.push(value);
}
}
5.使用递归去重
removeDuplicates 函数接受一个数组作为参数。它使用一个辅助函数 recursiveCheck,该函数使用递归的方式遍历原始数组并将非重复元素添加到结果数组中。result.includes(arr[index]) 这行代码用于检查结果数组中是否已经包含当前元素,如果不包含则将其添加到结果数组中(这里也可以换做其他方法查询是否包含当前元素)。
function removeDuplicates(arr) {
// 创建一个新数组来存储去重后的结果
var result = [];
// 递归函数,用于遍历原始数组并添加非重复元素到结果数组中
function recursiveCheck(index) {
if (index >= arr.length) {
return; // 递归结束条件:遍历完原始数组
}
if (!result.includes(arr[index])) {
result.push(arr[index]); // 将非重复元素添加到结果数组
}
recursiveCheck(index + 1); // 递归调用,处理下一个元素
}
recursiveCheck(0); // 从索引 0 开始递归检查
return result;
}
var array = [1, 2, 3, 2, 4, 3, 5];
var uniqueArray = removeDuplicates(array);
console.log(uniqueArray); // 输出 [1, 2, 3, 4, 5]
该例子中的去重算法时间复杂度为O(n^2),在处理大量数据时可能不够高效。如果对性能要求较高,可以考虑其他算法。
6.使用 hasOwnProperty 方法去重
使用 hasOwnProperty 方法结合一个空对象来判断数组元素是否重复,可以实现数组去重。
我们创建一个空对象 result,用于存储去重后的结果。然后,我们遍历原始数组 arr,对于每个元素,我们使用 hasOwnProperty 检查它是否已经存在于 result 对象中,如果不存在,则将其添加到 result 数组中。
function removeDuplicates(arr) {
var result = [];
for (var i = 0; i < arr.length; i++) {
if (!result.hasOwnProperty(arr[i])) {
result.push(arr[i]);
}
}
return result;
}
var array = [1, 2, 3, 2, 4, 3, 5];
var uniqueArray = removeDuplicates(array);
console.log(uniqueArray); // 输出 [1, 2, 3, 4, 5]
4.2 不创建新数组法
1.使用Set数据结构
Set数据结构是ES6中新增的一种集合类型,它只会存储不重复的值。我们可以通过将数组转换为Set来实现数组去重。Set这个结构默认就可以把数组进行去重,返回来的结果是Set这个类的实例。那把再它变成数组即可,可以用…展开运算符的方式,也可用Array.from()转换成数组。
/* SET */
let ary = [12, 23, 12, 15, 25, 15, 25, 14, 16];
//new Set(ary)是Set类的实例
let arr = [...new Set(ary)];
//let arr = Array.from(new Set(ary))
console.log(arr)
// [12, 23, 15, 25, 14, 16]
2.利用 Map 数据结构
Map 数据结构也可用于去重,它允许使用任何类型作为键(键可以是重复的),并自动删除所有重复的键。在此例中我们可以将数组元素作为 Map 的键,这样重复的键会被自动删除。然后我们再通过 Array.from() 方法将 Map 对象转回数组。这种方法与第一种方法类似,但无需使用额外的 Set 数据结构。
let arr = [1, 2, 2, 3, 4, 4, 5];
let uniqueArr = Array.from(new Map(arr.map(x => [x, true]))).map(x => x[0]);
3. 使用filter方法
filter方法是数组的一个高阶函数,可以根据指定的条件筛选出符合条件的元素并返回一个新数组。我们可以利用这个方法来实现数组去重。
let arr = [1, 2, 3, 4, 3, 2, 1];
let uniqueArr = arr.filter((value, index, self) => {
return self.indexOf(value) === index;
});
console.log(uniqueArr); // [1, 2, 3, 4]
通过filter方法遍历数组arr,并使用indexOf方法判断当前元素在数组中的第一个索引位置,如果与当前索引相等,则保留该元素。
4.splice
拿出当前项和后面的内容进行比较
①原来数组改变,这样如果i继续++,则会产生数组塌陷
②性能不好:当前项一旦删除,后面项索引都要变
let ary = [12, 23, 12, 15, 25, 15, 25, 14, 16];
for(let i=0;i<ary.length-1;i++){ //ary.length-1的原因:拿出当前项和后面的内容进行比较,最后一项没有后一项。
let item=ary[i],
args=ary.slice(i+1);
if(args.indexOf(item)>-1){ //indexOf不兼容ie678,(如果有就获取当前项再当前数组第一次出现的索引,如果没有就-1)。includes就更不兼容了
//包含:我们可以把当前项干掉
// splice删除
// 1. 原来数组改变,这样如果i继续++,则会产生数组塌陷
// 2. 性能不好:当前项一旦删除,后面项索引都要变
ary.splice(i,1);
i--;
}
}
console.log(ary);
5.赋值为null,后续filter一次
let ary = [12, 23, 12, 15, 25, 15, 25, 14, 16];
for(let i=0;i<ary.length-1;i++){ //ary.length-1的原因:拿出当前项和后面的内容进行比较,最后一项没有后一项。
let item=ary[i],
args=ary.slice(i+1);
if(args.indexOf(item)>-1){ //indexOf不兼容ie678,(如果有就获取当前项再当前数组第一次出现的索引,如果没有就-1)。includes就更不兼容了
ary[i] = null;
}
}
ary=ary.filter(item=>item!==null);
console.log(ary);
//[23, 12, 15, 25, 14, 16]
6.用最后一项替换
拿最后一项替换当前项,新数组还没有比过。还要把新替换的再的值再比一次,所以还要i–,
let ary = [12, 23, 12, 15, 25, 15, 25, 14, 16];
for(let i=0;i<ary.length-1;i++){ //ary.length-1的原因:拿出当前项和后面的内容进行比较,最后一项没有后一项。
let item=ary[i],
args=ary.slice(i+1);
if(args.indexOf(item)>-1){ //indexOf不兼容ie678,(如果有就获取当前项再当前数组第一次出现的索引,如果没有就-1)。includes就更不兼容了
ary[i]=ary[ary.length-1];//拿最后一项替换当前项
ary.length--;//去掉后一项
i--
}
}
console.log(ary);
7.拿数组中的每项向新容器中存储,如果已经存储过了,把当前项干掉
把当前作为键值对往里面存的话,如果之前已经有相同的属性名,就把它干掉。用对象和数组都可以,创建一个空容器,拿出一项放空容器里边,但是每拿一项放的时候都看一看,之前有没有放过,说明当前项就是重复的,把当前项干掉。
let ary = [12, 23, 12, 15, 25, 15, 25, 14, 16];
let obj={};
for(let i=0;i<ary.length;i++){
let item=ary[i];
//判断obj里有没有item,用in,object.keys
//如果当前对象没有这个属性就是undefined,如果不是underfined,说明一定有这个属性,说明就重了
if(typeof obj[item]!=='undefined'){
//用最后一项来替换
ary[i]=ary[ary.length-1];
ary.length--;
i--;
continue;//不存了
}
obj[item]=item;
}
obj=null;//把当前用的堆销毁掉
console.log(ary);
8.相邻项的处理方案
let ary = [12, 23, 12, 15, 25, 15, 25, 14, 16];
ary.sort((a,b)=>a-b);
ary = ary.join('@')+'@';
let reg=/(\d+@)\1*/g,
arr=[];
ary.replace(reg,(val,group1)=>{
arr.push(Number(group1.slice(0,group1.length-1)));
// arr.push(parseFloat(group1));
});
console.log(arr);
(四)数组扁平化
let arr = [
[1, 2, 2],
[3, 4, 5, 5],
[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10
];
1.ES6方法直接实现
let arr = [
[1, 2, 2],
[3, 4, 5, 5],
[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10
];
arr = arr.flat(Infinity);
console.log(arr)
//[1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]
2.转化为字符串
(1)toString()
let arr = [
[1, 2, 2],
[3, 4, 5, 5],
[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10
];
//arr=arr.toString()=>1,2,2,3,4,5,5,6,7,8,9,11,12,12,13,14,10
//arr=arr.toString().split(',') =>split() 方法用于把一个字符串分割成字符串数组。join() 方法用于把数组中的所有元素放入一个字符串。元素是通过指定的分隔符进行分隔的。
// ["1", "2", "2", "3", "4", "5", "5", "6", "7", "8", "9", "11", "12", "12", "13", "14", "10"]
arr=arr.toString().split(',').map(item=>parseFloat(item));
console.log(arr)
//[1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]
(2)JSON.sringify()
let arr = [
[1, 2, 2],
[3, 4, 5, 5],
[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10
];
//arr=JSON.stringify(arr) =>[[1,2,2],[3,4,5,5],[6,7,8,9,[11,12,[12,13,[14]]]],10]转换为JSON格式的字符串中括号还在
//arr=JSON.stringify(arr).replace(/(\[|\])/g,'') =>"1,2,2,3,4,5,5,6,7,8,9,11,12,12,13,14,10"
//arr=JSON.stringify(arr).replace(/(\[|\])/g,'').split(',')=> ["1", "2", "2", "3", "4", "5", "5", "6", "7", "8", "9", "11", "12", "12", "13", "14", "10"]
arr=JSON.stringify(arr).replace(/(\[|\])/g,'').split(',').map(item=>parseFloat(item))
console.log(arr)
//[1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]
3.循环验证是否为数组
(1)基于while和some
let arr = [
[1, 2, 2],
[3, 4, 5, 5],
[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10
];
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
console.log(arr)
(2).利用 reduce 函数迭代
function flatten(arr) {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur)
}, [])
}
let arr = [1, 2, [3, 4], [5, [6, 7]]]
console.log(flatten(arr))
(3)递归处理
(function () {
function myFlat() {
let result = [],
_this = this;
//=>循环ARR中的每一项,把不是数组的存储到新数组中
let fn = (arr) => {
for (let i = 0; i < arr.length; i++) {
let item = arr[i];
if (Array.isArray(item)) {
fn(item);
continue;
}
result.push(item);
}
};
fn(_this);
return result;
}
Array.prototype.myFlat = myFlat;
})();
arr = arr.myFlat();
(五)数组的交差并
1、includes 方法+ filter 方法实现交差并
let a = [1, 2, 3]
let b = [2, 4, 5]
// 并集
let union = a.concat(b.filter((v) => !a.includes(v)))
// [1,2,3,4,5]
// 交集
let intersection = a.filter((v) => b.includes(v))
// [2]
// 差集
let difference = a.concat(b).filter((v) => !a.includes(v) || !b.includes(v))
// [1,3,4,5]
2.ES6 的 Set 数据结构实现交叉并
let a = new Set([1, 2, 3])
let b = new Set([2, 4, 5])
// 并集
let union = new Set([...a, ...b])
// Set {1, 2, 3, 4,5}
// 交集
let intersect = new Set([...a].filter((x) => b.has(x)))
// set {2}
// a 相对于 b 的差集
let difference = new Set([...a].filter((x) => !b.has(x)))
// Set {1, 3}
3.数组合并
普通数组
const arr1 =[1, 2, 3, 4].concat([5, 6]) //[1,2,3,4,5,6]
const arr2 =[...[1, 2, 3, 4],...[4, 5]] //[1,2,3,4,5,6]
const arrA = [1, 2], arrB = [3, 4]
const arr3 =[].concat.apply(arrA, arrB)//arrA值为[1,2,3,4]
数组对象
const arr4 = [{ age: 1 }].concat([{ age: 2 }])
const arr5 = [...[{ age: 1 }],...[{ age: 2 }]]
console.log(arr4) //[ { age: 1 }, { age: 2 } ]
console.log(arr5) // [ { age: 1 }, { age: 2 } ]
(六)类数组的转化
1.Array 的 slice 方法
- Array.prototype.slice.call(arguments) || Array.prototype.slice.apply(arguments)
//从索引开始出提取数组
var test = function() {
return Array.prototype.slice.call(arguments) ; //es5
}
let a = test(1,2,3,4)
console.log(a) // [1, 2, 3, 4]
//例二:
var test = function() {
return Array.prototype.slice.apply(arguments) ; //es5
}
let a = test(1,2,3,4)
console.log(a) // [1, 2, 3, 4]
[].slice.call(arguments)
- [].slice.call(arguments)
var test = function() {
return [].slice.call(arguments) ; //es5
}
let a = test(1,2,3,4)
console.log(a) // [1, 2, 3, 4]
2.ES6 的 Array.from()
var test = function(){
return Array.from(arguments)
}
var a = test(1,2,3,4) ;
console.log(a) //[1,2,3,4]
document.querySelectAll("div") // NodeList[div, div, div, div]
Array.from(document.querySelectorAll('div')) // 转换为真数组
3.扩展运算符…
let arr = [...arguments]
伪数组不能调用真数组对象上的方法,所以得将伪数组转换为真数组,获取js元素是伪数组。
document.querySelectAll("div") // NodeList[div, div, div, div]
[...document.querySelectorAll('div')] // 转换为真数组
4.循环类数组
let utils = (function(){
function toArray(){
let args = [];
for (let i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
return args
}
return {
toArray
};
})();
let ary = utils.toArray(10,20,30);
console.log(ary)//=>[10,20,30]
let ary1 = utils.toArray('A',10,20,30);
console.log(ary1) //=>["A",10,20,30]
(七)排序
对于前端来说,排序算法在应用方面似乎始终不是什么瓶颈——JS 天生地提供了对排序能力的支持,很多时候,我们实现排序只需要这样寥寥数行的代码:
arr.sort((a,b) => {
return a - b
})
以某一个排序算法为“引子”,顺藤摸瓜式地盘问,可以问出非常多的东西,这也是排序算法始终热门的一个重要原因——面试官可以通过这种方式在较短的时间里试探出候选人算法能力的扎实程度和知识链路的完整性。因此排序算法在面试中的权重不容小觑。
1.冒泡、快排、插入、选择四大排序
1.冒泡排序
/*
*冒泡排序的思想:
* 重复的让数组中当前项和后一项进行比较,如果当前项比后一项大,则两项交换位置(让大的靠后)即可
*/
/*
*bubble: 实现冒泡排序
* @params
* ary [ARRAY] 需要排序的数组
* @return
* 排序
* by donglize on 2019/07/26
*/
function Bubble(ary){
// 外层循环I控制比较的轮数
for(i=0;i<ary.length-1;i++){
// 里层循环控制每一轮比较的次数J
for(j=0;j < ary.length-1-i;j++){
// 当前项大于后一项
if(ary[j]>ary[j+1]){
[ary[j],ary[j+1]] = [ary[j+1],ary[j]]
// var temp = ary[j];
// ary[j] = ary[j+1];
// ary[j+1] = temp;
}
}
}
return ary;
}
let ary = [12,8,24,16,1];
ary=Bubble(ary);
console.log(ary)
面向“最好情况”的进一步改进
很多同学反映说,在不少教材里都看到了“冒泡排序时间复杂度在最好情况下是 O(n)”这种说法,但是横看竖看,包括楼上示例在内的各种冒泡排序主流模板似乎都无法帮助我们推导出 O(n) 这个结果。
实际上,你的想法是对的,冒泡排序最常见的写法(也就是楼上的编码示例)在最好情况下对应的时间复杂度确实不是O(n),而是O(n^2)。
那么是O(n)这个说法错了吗?其实也不错,因为冒泡排序通过进一步的改进,确实是可以做到最好情况下 O(n)复杂度的,这里我先把代码给大家写出来(注意解析在注释里):
function betterBubbleSort(ary){
for(i=0;i<ary.length-1;i++){
//区别在这里,我们加了一个标志位
let flag = false
for(j=0;j < ary.length-1-i;j++){
if(ary[j]>ary[j+1]){
[ary[j],ary[j+1]] = [ary[j+1],ary[j]]
// var temp = ary[j];
// ary[j] = ary[j+1];
// ary[j+1] = temp;
flag = true
}
}
if(flag == false) return ary;
}
return ary;
}
let ary = [12,8,24,16,1];
ary = betterBubbleSort(ary);
console.log(ary)
标志位可以帮助我们在第一次冒泡的时候就定位到数组是否完全有序,进而节省掉不必要的判断逻辑,将最好情况下的时间复杂度定向优化为 O(n)。
2.快速排序
/*
*quick: 实现快速排序
* @params
* ary [ARRAY] 需要排序的数组
* @return
* 排序
* by donglize on 2019/07/26
*/
function quick(ary){
// 4.结束递归 (当ARY中小于等于一项,则不用处理)
if(ary.length<=1){
return ary;
}
// 1. 找到数组中间项,在原有的数组中把它移除
let middleIndex = Math.floor(ary.length/2);
let middleValue = ary.splice(middleIndex,1)[0];
// 2.准备左右两个数组,循环剩下数组中的每一项,比当前小的放到左边数组中,反之放到右边数组中
let aryLeft=[],
aryRight=[];
for(let i=0;i<ary.length;i++){
let item=ary[i];
item<middleValue?aryLeft.push(item):aryRight.push(item);
}
//3.递归方式让左右两边的数组持续这样处理,一直到左右两边都排好序为止(最后让左边+中间+右边拼接成为最后的结果)
return quick(aryLeft).concat(middleValue,quick(aryRight));
}
let ary = [12,8,15,16,1,24];
ary=quick(ary);
console.log(ary)
3.插入排序
/*
*insert: 实现插入排序
* @params
* ary [ARRAY] 需要排序的数组
* @return
* 排序
* by donglize on 2019/07/26
*/
function insert(ary){
// 1.准备一个新数组,用来存储抓到手里的牌,开始先抓一张牌进来
let handle=[];
handle.push(ary[0]);
// 2.从第二项开始抓牌,一直把台面上的牌抓光
for(let i=1;i<ary.length;i++){
// A是新抓的牌
let A=ary[i];
// 和handle手里的牌依次比较(从后向前比)
for(let j=handle.length-1;j>=0;j--){
// 每一次要比较手里的牌
let B=handle[j];
// 如果当前新牌A要比要比较的牌大了,把A放到B的后面
if(A>B){
handle.splice(j+1,0,A);
break;
}
// 已经比到第一项,我们把新牌放到手中最前面即可
if(j===0){
handle.unshift(A);
}
}
}
return handle;
}
let ary = [12,8,24,16,1];
ary = insert(ary);
console.log(ary)
4.选择排序
思路分析
选择排序的关键字是“最小值”:循环遍历数组,每次都找出当前范围内的最小值,把它放在当前范围的头部;然后缩小排序范围,继续重复以上操作,直至数组完全有序为止。
真实排序过程演示
下面我们尝试基于选择排序的思路,对如下数组进行排序:
[5, 3, 2, 4, 1]
首先,索引范围为 [0, n-1] 也即 [0,4] 之间的元素进行的遍历(两个箭头分别对应当前范围的起点和终点):
[5, 3, 2, 4, 1]
↑ ↑
得出整个数组的最小值为 1。因此把1锁定在当前范围的头部,也就是和 5 进行交换:
[1, 3, 2, 4, 5]
交换后,数组的第一个元素值就明确了。接下来需要排序的是 [1, 4] 这个索引区间:
[1, 3, 2, 4, 5]
↑ ↑
遍历这个区间,找出区间内最小值为 2。因此区间头部的元素锁定为 2,也就是把 2 和 3 交换。相应地,将需要排序的区间范围的起点再次后移一位,此时区间为 [2, 4]:
[1, 2, 3, 4, 5]
↑ ↑
遍历 [2,4] 区间,得到最小值为 3。3 本来就在当前区间的头部,因此不需要做额外的交换。
以此类推,4会被定位为索引区间 [3,4] 上的最小值,仍然是不需要额外交换的。
基于这个思路,我们来写代码:
编码示范
function selectSort(arr) {
// 缓存数组长度
const len = arr.length
// 定义 minIndex,缓存当前区间最小值的索引,注意是索引
let minIndex
// i 是当前排序区间的起点
for(let i = 0; i < len - 1; i++) {
// 初始化 minIndex 为当前区间第一个元素
minIndex = i
// i、j分别定义当前区间的上下界,i是左边界,j是右边界
for(let j = i; j < len; j++) {
// 若 j 处的数据项比当前最小值还要小,则更新最小值索引为 j
if(arr[j] < arr[minIndex]) {
minIndex = j
}
}
// 如果 minIndex 对应元素不是目前的头部元素,则交换两者
if(minIndex !== i) {
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
}
}
return arr
}
编码复盘——选择排序的时间复杂度
在时间复杂度这方面,选择排序没有那么多弯弯绕绕:最好情况也好,最坏情况也罢,两者之间的区别仅仅在于元素交换的次数不同,但都是要走内层循环作比较的。因此选择排序的三个时间复杂度都对应两层循环消耗的时间量级: O(n^2)。
2.数组对象的排序
1.单个属性排序
function compare(property) {
return function (a, b) {
let value1 = a[property]
let value2 = b[property]
return value1 - value2
}
}
let arr = [
{ name: 'zopp', age: 10 },
{ name: 'gpp', age: 18 },
{ name: 'yjj', age: 8 },
]
console.log(arr.sort(compare('age')))
2.多个属性排序
function by(name, minor) {
return function(o, p) {
let a, b
if (o && p && typeof o === 'object' && typeof p === 'object') {
a = o[name]
b = p[name]
if (a === b) {
return typeof minor === 'function' ? minor(o, p) : 0
}
if (typeof a === typeof b) {
return a < b ? -1 : 1
}
return typeof a < typeof b ? -1 : 1
} else {
thro('error')
}
}
},
(八)斐波那契数列
- 思路1
function fibonacci(n){
if(n<=1) return 1;
let arr=[1,1];
//=>即将要创建多少个
let i=n+1-2;
while(i>0){
let a=arr[arr.length-2],
b=arr[arr.length-1];
arr.push(a+b);
i--;
}
return arr[arr.length-1];
console.log(fibonacci(0))
console.log(fibonacci(1))
console.log(fibonacci(2))
console.log(fibonacci(3))
console.log(fibonacci(4))
思路2:递归
①递归1
用递归的方式把当前的next作为下一次递归的count,把当前项和next相加的和作为下一次fn调用的下一项,count一直减到0为止,最终会返回一个值。递归的性能也不好,与上一种思路相比,思路1是从0创建到1000,思路2是从1000减到0。
function fibonacci(count) {
function fn(count, curr = 1, next = 1) {
if (count == 0) {
return curr;
} else {
return fn(count - 1, next, curr + next);
}
};
return fn(count);
}
②递归2
function fb(n){
if(n-2>=0){
return fb(n-2) + fb(n-1)
}else{
return 1
}
}
(九)数组的遍历
几种遍历方法中for执行最快,它没有任何额外的函数调用栈和上下文。但在实际开发中我们要结合语义话、可读性和程序性能,去选择究竟使用哪种方案。下面来看for , foreach , map , for…in , for…of五种方法现场battle。
使用建议:对于纯对象的遍历,选择for…in枚举更方便;对于数组遍历,如果不需要知道索引for…of迭代更合适,因为还可以中断;如果需要知道索引,则forEach()更合适;对于其他字符串,类数组,类型数组的迭代,for…of更占上风更胜一筹。但是注意低版本浏览器的是配性。
1.for 循环
这个是最最基础的操作。我们可以通过循环数组的下标,来依次访问每个值。这里给个小建议:个人推荐如果没有特殊的需要,那么统一使用 for 循环来实现遍历。因为从性能上看,for 循环遍历起来是最快的。我是最早出现的一方遍历语句,在座的各位需称我一声爷爷。我能满足开发人员的绝大多数的需求。
// 遍历数组
let arr = [1,2,3];
for(let i = 0;i < arr.length;i++){
console.log(i) // 索引,数组下标
console.log(arr[i]) // 数组下标所对应的元素
}
// 遍历对象
let profile = {name:"April",nickname:"二十七刻",country:"China"};
for(let i = 0, keys=Object.keys(profile); i < keys.length;i++){
console.log(keys[i]) // 对象的键值
console.log(profile[keys[i]]) // 对象的键对应的值
}
// 遍历字符串
let str = "abcdef";
for(let i = 0;i < str.length ;i++){
console.log(i) // 索引 字符串的下标
console.log(str[i]) // 字符串下标所对应的元素
}
// 遍历DOM 节点
let articleParagraphs = document.querySelectorAll('.article > p');
for(let i = 0;i<articleParagraphs.length;i++){
articleParagraphs[i].classList.add("paragraph");
// 给class名为“article”节点下的 p 标签添加一个名为“paragraph” class属性。
}
2.forEach 方法
自我介绍:我是ES5版本发布的。按升序为数组中含有效值的每一项执行一次 callback 函数,那些已删除或者未初始化的项将被跳过(例如在稀疏数组上)。我是 for 循环的加强版。
通过取 forEach 方法中传入函数的第一个入参和第二个入参,我们也可以取到数组每个元素的值及其对应索引。
如果再写for循环,我就锤自己
(1)局限及原理 :
但是forEach也有一些局限,不能continue跳过或者break/return终止循环。
查看forEach实现原理,就会理解这个问题。
Array.prototype.forEach(callbackfn [,thisArg]{
}
传入的function是这里的回调函数。在回调函数里面使用break肯定是非法的,因为break只能用于跳出循环,回调函数不是循环体。
在回调函数中使用return,只是将结果返回到上级函数,也就是这个for循环中,并没有结束for循环,所以return也是无效的。
//语法
array.forEach(function(currentValue, index, arr), thisValue)
//遍历数组
arr.forEach((item, index)=> {
// 输出数组的元素值,输出当前索引
console.log(item, index)
})
//遍历对象
let profile = {name:"April",nickname:"二十七刻",country:"China"};
let keys = Object.keys(profile);
keys.forEach(i => {
console.log(i) // 对象的键值
console.log(profile[i]) // 对象的键对应的值
})
[].forEach(function(value,index,array){
//do something
});
等价于:
$.each([],function(index,value,array){
//do something
})
(2)解决方案:Javascript数组的forEach方法大家都用过,也应该都知道forEach是无法退出的,及时在方法里写了return也没用。但是很多人都知道可以通过
①抛出异常②把数组长度设置为0。
let arr = [1,2,3];
arr.forEach(item => {
console.log(item);
if (item > 1 ) {
return;
}
})
//最终输出的还是:1,2,3
①有一种退出forEach的方法相信很多人都知道,就是抛出一个异常。
let arr = [1,2,3];
arr.forEach(item => {
console.log(item);
if (item > 1 ) {
throw Error;
}
})
//最终输出的还是:1,2。说明forEach被结束了。
②奇妙的退出方法,办法就是把数组的长度设为0。
let arr = [1,2,3];
arr.forEach(item => {
console.log(item);
if (item > 1 ) {
arr.length = 0;
}
}
3.map 方法
我也是ES5版本发布的, map 方法在调用形式上与 forEach 无异,区别在于 map 方法会根据你传入的函数逻辑对数组中每个元素进行处理、进而返回一个全新的数组。
(1)所以其实 map 做的事情不仅仅是遍历,而是在遍历的基础上“再加工”。当我们需要对数组内容做批量修改、同时修改的逻辑又高度一致时,就可以调用 map 来达到我们的目的:
const newArr = arr.map((item, index)=> {
// 输出数组的元素值,输出当前索引
console.log(item, index)
// 在当前元素值的基础上加1
return item+1
})
这段代码就通过 map 来返回了一个全新的数组,数组中每个元素的值都是在其现有元素值的基础上+1后的结果。
(2)map() 方法是可以链式调用的,这意味着它可以方便的结合其它方法一起使用。例如:reduce(), sort(), filter() 等。但是其它方法并不能做到这一点。forEach()的返回值是undefined,所以无法链式调用。
// 将元素乘以本身,再进行求和。
let arr = [1, 2, 3, 4, 5];
let res1 = arr.map(item => item * item).reduce((total, value) => total + value);
console.log(res1) // logs 55 undefined"
(3)缺点: forEach 与map 是不支持跳出循环体的,其它三种方法均支持
4.for…in枚举
我是ES5版本发布的。以任意顺序遍历一个对象的除Symbol以外的可枚举属性。
for(var item in arr|obj){} 可以用于遍历数组和对象
遍历数组时,item表示索引值, arr表示当前索引值对应的元素 arr[item]
遍历对象时,item表示key值,arr表示key值对应的value值 obj[item]
for in一般循环遍历的都是对象的属性,遍历对象本身的所有可枚举属性,以及对象从其构造函数原型中继承的属性
// 遍历对象
let profile = {name:"April",nickname:"二十七刻",country:"China"};
for(let i in profile){
let item = profile[i];
console.log(item) // 对象的键值
console.log(i) // 对象的键对应的值
}
// 遍历数组
let arr = ['a','b','c'];
for(let i in arr){
let item = arr[i];
console.log(item) // 数组下标所对应的元素
console.log(i) // 索引,数组下标
}
// 遍历字符串
let str = "abcd"
for(let i in str){
let item = str[i];
console.log(item) // 字符串下标所对应的元素
console.log(i) // 索引 字符串的下标
}
//遍历原型上的属性
Object.prototype.objCustom = function() {};
Array.prototype.arrCustom = function() {};
var arr = ['a', 'b', 'c'];
arr.foo = 'hello
for (var i in arr) {
console.log(i);
}
// logs
// 0
// 1
// 2
// foo
// arrCustom
// objCustom
/*然而在实际的开发中,我们并不需要原型对象上的属性。这种情况下我们可以使用hasOwnProperty() 方法,它会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。如下:*/
Object.prototype.objCustom = function() {};
Array.prototype.arrCustom = function() {};
var arr = ['a', 'b', 'c'];
arr.foo = 'hello
for (var i in arr) {
if (arr.hasOwnProperty(i)) {
console.log(i);
}
}
// logs
// 0
// 1
// 2
// foo
// 可见数组本身的属性还是无法摆脱。此时建议使用 forEach
5.for of
ES6中新增加的语法 for of 语句创建一个循环来迭代可迭代的对象。在 ES6 中引入的 for of 循环,以替代 for in 和 forEach() ,并支持新的迭代协议。for of 允许你遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等。
循环一个数组:
let arr = ['A', 'B', 'C']
for (let val of arr) {
console.log(val)
}
// A B C
循环一个字符串:
let iterable = "abc";
for (let value of iterable) {
console.log(value);
}
// "a"
// "b"
// "c"
循环一个Map:
let iterable = new Map([["a", 1], ["b", 2], ["c", 3]]);
for (let [key, value] of iterable) {
console.log(value);
}
// 1
// 2
// 3
for (let entry of iterable) {
console.log(entry);
}
// [a, 1]
// [b, 2]
// [c, 3]
循环一个 Set:
let iterable = new Set([1, 1, 2, 2, 3, 3]);
for (let value of iterable) {
console.log(value);
}
// 1
// 2
// 3
循环一个拥有enumerable属性的对象
for of循环并不能直接使用在普通的对象上,但如果我们按对象所拥有的属性进行循环,可使用内置的Object.keys()方法:
for (var key of Object.keys(someObject)) {
console.log(key + ": " + someObject[key]);
}
循环一个生成器(generators):
function* fibonacci() { // a generator function
let [prev, curr] = [0, 1];
while (true) {
[prev, curr] = [curr, prev + curr];
yield curr;
}
}
for (let n of fibonacci()) {
console.log(n);
// truncate the sequence at 1000
if (n >= 1000) {
break;
}
}
// 迭代 DOM 节点
let articleParagraphs = document.querySelectorAll('.article > p');
for (let paragraph of articleParagraphs) {
paragraph.classList.add("paragraph");
// 给class名为“article”节点下的 p 标签添加一个名为“paragraph” class属性。
}
// 迭代arguments类数组对象
(function() {
for (let argument of arguments) {
console.log(argument);
}
})(1, 2, 3);
// logs:
// 1
// 2
// 3
性能
有兴趣的读者可以找一组数据自行测试,文章就直接给出结果了,并做相应的解释。
for > for-of > forEach > map > for-in
- for 循环当然是最简单的,因为它没有任何额外的函数调用栈和上下文;
- for…of只要具有Iterator接口的数据结构,都可以使用它迭代成员。它直接读取的是键值。
- forEach,因为它其实比我们想象得要复杂一些,它实际上是array.forEach(function(currentValue, index, arr), thisValue)它不是普通的 for 循环的语法糖,还有诸多参数和上下文需要在执行的时候考虑进来,这里可能拖慢性能;
- map() 最慢,因为它的返回值是一个等长的全新的数组,数组创建和赋值产生的性能开销很大。
- for…in需要穷举对象的所有属性,包括自定义的添加的属性也能遍历到。且for…in的key是String类型,有转换过程,开销比较大。
6.jQuery里面的$.each
$.each(arr|obj, function(k, v))
可以用来遍历数组和对象,其中k表示索引值或者key值,v表示value值
var arr = ['a','b','c']
$.each(arr, function(key, val) {
console.log(key, val);
})
//0 a
//1 b
//2 c
7.jQuery里面的$().each()
$().each()
在dom处理上面用的较多,主要是用来遍历DOMList。如果页面有多个input标签类型为checkbox,对于这时用$().each()来处理多个checkbox,例如:
$(“input[name=’checkbox’]”).each(function(i){
if($(this).attr(‘checked’)==true){
//操作代码
}
推荐在循环对象属性的时候使用for in,在遍历数组的时候的时候使用for of;
for in循环出的是key,for of循环出的是value;
for of是ES6新引入的特性。修复了ES5的for in的不足;
for of不能循环普通的对象,需要通过和Object.keys()搭配使用。
跳出循环的方式有如下几种:
- return 函数执行被终止;
- break 循环被终止;
- continue 循环被跳过。
8.map()、.reduce()、.filter()方法比较
这个三个方法的基本思路和for循环是一样的,只是在参数功能细节等方面,各自有擅长的地方。使用场景是:
(1) 如果需要一个数组,请使用.map()方法;
(2) 如果需要一个结果,请使用reduce()方法;
(3) 如果需要过滤一个结果,请使用.filter()方法。
map()方法
创建一个数组对象,这个数组对象里包含三个对象,这三个对象里包含着年龄和姓名。map方法里是接受一个匿名函数作为参数,匿名函数它又有三个参数,第一个参数是当前数组当中的第几个成员,第二个参数是当前数组在数组当中的索引,第三个参数是调用map方法的被操作的数组本身。
var _arrObj = [{
age: 11, name: 'aaa'
}, {
age: 22, name: 'bbb'
}, {
age: 33, name: 'ccc'
}];
var _age = _arrObj.map(function (_d, inx, arr) {
console.log(_d, inx, arr);
return _d.age;
})
console.log(_age)
es6重写
箭头函数只需要一行代码,代码更加简洁。也不用调用push方法拼接数组了。
var _arrObj = [{
age: 11, name: 'aaa'
},
age: 22, name: 'bbb'
}, {
age: 33, name: 'ccc'
}];
var _ageEs6 = _arrObj.map( _d=>_d.age);
console.log(_ageEs6)
reduce()
reduce也是一个循环,但它循环的是回调的方法,数组当中的每一个成员都进入回调,区别在于每次回调方法返回的值都会传入到下次回调中,就是累加。它有两个参数,第一是回调方法,第二个是累计初始的值。下面例子中,_n是承接计算的结果,初始值是0。
var _arrObj = [{
age: 11, name: 'aaa'
},
age: 22, name: 'bbb'
}, {
age: 33, name: 'ccc'
}];
var _ageYear = _arrObj.reduce(function (_n, _y) {
console.log('------:' + _n + ':' + _y.age)
return _n + _y.age
}, 0)
console.log(_ageYear)
ES6重写
var _arrObj = [{
age: 11, name: 'aaa'
},
age: 22, name: 'bbb'
}, {
age: 33, name: 'ccc'
}];
var _ageYearEs6 = _arrObj.reduce((_n, _y) => _n + _y.age, 0);
console.log(_ageYearEs6)
filter()
filter()用于过滤,filter()方法返回的也是一个数组。filter()方法接收的是一个匿名函数,匿名函数只有一个参数。参数就是数组对象中的每一个成员,进行比较。如下面代码中name为ccc的就被过滤出来,也就是说,只要filter方法的回调函数中的比较运算符的结果为true,那么符合条件的对象就会包在一个数组中返回,比较结果为false的就被过滤掉了。
var _arrObj = [{
age: 11, name: 'aaa'
},
age: 22, name: 'bbb'
}, {
age: 33, name: 'ccc'
}];
var _arrName=_arrObj.filter(function(_d){
return _d.name === 'ccc'
});
console.log(_arrName)
ES6重写filter()
var _arrObj = [{
age: 11, name: 'aaa'
},
age: 22, name: 'bbb'
}, {
age: 33, name: 'ccc'
}];
var _arrNameEs6=_arrObj.filter(_d => _d.name === 'ccc');
console.log(_arrNameEs6)
(十)数组是否包含值
普通数组
console.log([1, 2, 3].includes(4)) //false
console.log([1, 2, 3].indexOf(4)) //-1 如果存在换回索引
console.log([1, 2, 3].find((item) => item === 3)) //3 如果数组中无值返回undefined
console.log([1, 2, 3].findIndex((item) => item === 3)) //2 如果数组中无值返回-1
数组对象
const flag = [{age:1},{age:2}].some(v=>JSON.stringify(v)===JSON.stringify({age:2}))
console.log(flag)
(十一)javascript深浅拷贝
1.浅拷贝与深拷贝
- 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
- 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
var a1 = {b: {c: {}};
var a2 = shallowClone(a1); // 浅拷贝方法
a2.b.c === a1.b.c // true 新旧对象还是共享同一块内存
var a3 = deepClone(a3); // 深拷贝方法
a3.b.c === a1.b.c // false 新对象跟原对象不共享内存
借助ConardLi大佬以下两张图片,帮我们更好的理解两者的含义:
总而言之,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
2.赋值和深/浅拷贝的区别
这三者的区别如下,不过比较的前提都是针对引用类型:
-
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
-
浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
-
深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。
我们先来看下面的例子,对比赋值与深/浅拷贝得到的对象修改后对原始对象的影响:
// 对象赋值
let obj1 = {
name : '浪里行舟',
arr : [1,[2,3],4],
};
let obj2 = obj1;
obj2.name = "阿浪";
obj2.arr[1] =[5,6,7] ;
console.log('obj1',obj1)
// obj1 { name: '阿浪', arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj2',obj2)
// obj2 { name: '阿浪', arr: [ 1, [ 5, 6, 7 ], 4 ] }
// 浅拷贝
let obj1 = {
name : '浪里行舟',
arr : [1,[2,3],4],
};
let obj3=shallowClone(obj1)
obj3.name = "阿浪";
obj3.arr[1] = [5,6,7] ; // 新旧对象还是共享同一块内存
// 这是个浅拷贝的方法
function shallowClone(source) {
var target = {};
for(var i in source) {
if (source.hasOwnProperty(i)) {
target[i] = source[i];
}
}
return target;
}
console.log('obj1',obj1)
// obj1 { name: '浪里行舟', arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj3',obj3)
// obj3 { name: '阿浪', arr: [ 1, [ 5, 6, 7 ], 4 ] }
// 深拷贝
let obj1 = {
name : '浪里行舟',
arr : [1,[2,3],4],
};
let obj4=deepClone(obj1)
obj4.name = "阿浪";
obj4.arr[1] = [5,6,7] ; // 新对象跟原对象不共享内存
// 这是个深拷贝的方法
function deepClone(obj) {
if (obj === null) return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (typeof obj !== "object") return obj;
let cloneObj = new obj.constructor();
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key]);
}
}
return cloneObj;
}
console.log('obj1',obj1)
// obj1 { name: '浪里行舟', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4',obj4)
// obj4 { name: '阿浪', arr: [ 1, [ 5, 6, 7 ], 4 ] }
上面例子中,obj1是原始对象,obj2是赋值操作得到的对象,obj3浅拷贝得到的对象,obj4深拷贝得到的对象,通过下面的表格,我们可以很清晰看到他们对原始数据的影响:
3.浅拷贝的实现方式
1.Object.assign()
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1);
// { person: { name: 'wade', age: 41 }, sports: 'basketball' }
2.函数库lodash的_.clone方法
该函数库也有提供_.clone用来做 Shallow Copy,后面我们会再介绍利用这个库实现深拷贝。
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);
// true
3.展开运算符…
展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。
let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2)
// obj2 { name: 'Kobe', address: { x: 200, y: 100 } }
4.Array.prototype.concat()
let arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.concat();
arr2[2].username = 'wade';
console.log(arr);
//[ 1, 3, { username: 'wade' } ]
5.Array.prototype.slice()
let arr = [1, 3, {
username: ' kobe'
}];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr);
// [ 1, 3, { username: 'wade' } ]
4.深拷贝的实现方式
1.JSON.parse(JSON.stringify())
let arr = [1, 3, {
username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan';
console.log(arr, arr4)
这也是利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
**缺点:**这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,因为这两者基于JSON.stringify和JSON.parse处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了。
比如下面的例子:
let arr = [1, 3, {
username: ' kobe'
},function(){}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan';
console.log(arr, arr4)
2.函数库lodash的_.cloneDeep方法
该函数库也有提供_.cloneDeep用来做 Deep Copy
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);
// false
3.jQuery.extend()方法
jquery 有提供一個$.extend可以用来做 Deep Copy
$.extend(deepCopy, target, object1, [objectN])//第一个参数为true,就是深拷贝
var $ = require('jquery');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f);
// false
4.手写递归方法
递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
有种特殊情况需注意就是对象存在循环引用的情况,即对象的属性直接的引用了自身的情况,解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。关于这块如有疑惑,请仔细阅读ConardLi大佬如何写出一个惊艳面试官的深拷贝?这篇文章。
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj;
// 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
let obj = { name: 1, address: { x: 100 } };
obj.o = obj; // 对象存在循环引用的情况
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);
(十二)关于数组操作处理的N种场景
1.如何把一个数组打散
使用原生实现,Math.rondom()
- 0.5 有时大于0,有时小于0会达成这样的效果
[1, 2, 3, 4].sort((x, y) => Math.random() - 0.5)
借用 lodash 可更方便
_.shuffle([1, 2, 3, 4])
//-> [3, 2, 4, 1]
2.快速将字符串转换为数组
不再使用字符串split方法,使用扩展运算符可以快速转换为数组。
let str = "abcdefg"
console.log([...str]) // ["a", "b", "c", "d", "e", "f", "g"]
3.数组转换为对象
let person = ["蛙人", 24, "male"]
let obj = {}
person.forEach(item => (obj[item] = item))
4.去除当前数组里的false值
把数组里面的假值过滤掉。
let list = ["", false, 1, null, undefined, "蛙人", 24]
let res = item => item.filter(Boolean)
console.log(res(list))
5.使用Map使数组直接返回结果
有时我们处理数组时,想直接返回处理完的结果,而不是在重新组合一个数组,这时Map就登场了。
let person = [10, 20, 30]
function fn(item) {
return item + 1
}
let res = person.map(fn)
console.log(res) // [11, 21, 31]
6.数组求和使用reduce
之前我们都使用for循环进行遍历求和,也可以使用reduce方法进行求和,简洁代码。
let nums = [1,22,31,4,56]
let sum = nums.reduce((prev, cur) => prev + cur, 0)
参考链接
1.JS数组奇巧淫技
2.浅谈6种JS数组遍历方法的区别
3.数组的深浅拷贝
4.数组的深浅拷贝
5.数组拷贝
6.数组拷贝
7.深浅拷贝
8.解锁各种js数组骚操作,总有你想要的!
9.JS开发必须知道的41个技巧【持续更新】
10.45道JS能力测评经典题总结
11.20个你可能不知道的javascript奇巧淫技(内附参考链接)
12.数组回炉重造+6道前端算法面试高频题解
(十三)数组的应用–真题归纳与解读
1.Map的妙用一一两数求和问题
真题描述:给定一个整数数组nums和一个目标值target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
示例:给定nums=[2,7,11,15],target=9
因为nums[0]+nums[1]=2+7=9所以返回[0,1]
思路分析:
(1)第一个最基本的思路:暴力解法
两层循环来遍历同一个数组;第一层循环遍历的值记为a,第二层循环时遍历的值记为b;若a+b=目标值,那么a和b对应的数组下标就是我们想要的答案。但是大家以后做算法题的时候,要有这样的一种本能:当发现自己的代码里有两层循环时,先反思一下,能不能用空间换时间,把它优化成一层循环。因为两层循环很多情况下都意味着O(n^2)的复杂度,这个复杂度非常容易导致你的算法超时。即便没有超时,在明明有一层遍历解法的情况下,你写了两层遍历,面试官对你的印象分会大打折扣。
const twoSum = function(nums, target) {
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j < nums.length; j++) {
if (nums[i] === target - nums[j]) {
return [i, j]
}
}
}
}
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
(2)空间换时间,Map来帮忙
拿我们这道题来说,其实二层遍历是完全不必要的。大家记住一个结论:几乎所有的求和问题,都可以转化为求差问题。这道题就是一个典型的例子,通过把求和问题转化为求差问题,事情会变得更加简单。
我们可以在遍历数组的过程中,增加一个Map来记录已经遍历过的数字及其对应的索引值。然后每遍历到一个新数字的时候,都回到Map里去查询targetNum与该数的差值是否已经在前面的数字中出现过了。若出现过,那么答案已然显现,我们就不必再往下走了。
我们以nums=[2,7,11,15]这个数组为例,来模拟一下这个思路:
第一次遍历到2,此时Map为空:
以2为key,索引0为value作存储,继续往下走;遇到了7:
计算targetNum和7的差值为2,去Map中检索2这个key,发现是之前出现过的值:
那么2和7的索引组合就是这道题的答案啦。
键值对存储我们可以用ES6里的Map来做,如果图省事,直接用对象字面量来定义也没什么问题。
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
const twoSum = function(nums, target) {
// 这里我用对象来模拟 map 的能力
const diffs = {}
// 缓存数组长度
const len = nums.length
// 遍历数组
for(let i=0;i<len;i++) {
// 判断当前值对应的 target 差值是否存在(是否已遍历过)
if(diffs[target-nums[i]]!==undefined) {
// 若有对应差值,那么答案get!
return [diffs[target - nums[i]], i]
}
// 若没有对应差值,则记录当前值
diffs[nums[i]]=i
}
};
tips:这道题也可以用ES6中的Map来做,你试试呢?
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
let len = nums.length;
// 创建 MAP
const MAP = new Map();
// 由于第一个元素在它之前一定没有元素与之匹配,所以先存入哈希表
MAP.set(nums[0], 0);
for (let i = 1; i < len; i++) {
// 提取共用
let other = target - nums[i];
// 判断是否符合条件,返回对应的下标
if (MAP.get(other) !== undefined) return [MAP.get(other), i];
// 不符合的存入hash表
MAP.set(nums[i], i)
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(n)
2.强大的双指针法一一合并两个有序数组
真题描述:给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
说明: 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
示例:
输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
输出: [1,2,2,3,5,6]
思路分析
标准解法:这道题没有太多的弯弯绕绕,标准解法就是双指针法。首先我们定义两个指针,各指向两个数组生效部分的尾部:
每次只对指针所指的元素进行比较。取其中较大的元素,把它从 nums1 的末尾往前面填补:
这里有一点需要解释一下:
为什么是从后往前填补?因为是要把所有的值合并到 nums1 里,所以说我们这里可以把 nums1 看做是一个“容器”。但是这个容器,它不是空的,而是前面几个坑有内容的。如果我们从前往后填补,就没法直接往对应的坑位赋值了(会产生值覆盖)。
从后往前填补,我们填的都是没有内容的坑,这样会省掉很多麻烦。
由于 nums1 的有效部分和 nums2 并不一定是一样长的。我们还需要考虑其中一个提前到头的这种情况:
①如果提前遍历完的是 nums1 的有效部分,剩下的是 nums2。那么这时意味着 nums1 的头部空出来了,直接把 nums2 整个补到 nums1 前面去即可。
②如果提前遍历完的是 nums2,剩下的是 nums1。由于容器本身就是 nums1,所以此时不必做任何额外的操作。
编码实现:
/**
* @param {number[]} nums1
* @param {number} m
* @param {number[]} nums2
* @param {number} n
* @return {void} Do not return anything, modify nums1 in-place instead.
*/
const merge = function(nums1, m, nums2, n) {
// 初始化两个指针的指向,初始化 nums1 尾部索引k
let i = m - 1, j = n - 1, k = m + n - 1
// 当两个数组都没遍历完时,指针同步移动
while(i >= 0 && j >= 0) {
// 取较大的值,从末尾往前填补
if(nums1[i] >= nums2[j]) {
nums1[k] = nums1[i]
i--
k--
} else {
nums1[k] = nums2[j]
j--
k--
}
}
// nums2 留下的情况,特殊处理一下
while(j>=0) {
nums1[k] = nums2[j]
k--
j--
}
};
找点乐子:
上面我们给出的,是面试官最喜欢看到的一种解法,这种解法适用于各种语言。
但是就 JS 而言,我们还可以“另辟蹊径”,仔细想想,你有什么妙招?
3.三数求和问题
双指针法能处理的问题多到你想不到。不信来瞅瞅两数求和它儿子——三数求和问题!
俗话说,青出于蓝而胜于蓝,三数求和虽然和两数求和只差了一个字,但是思路却完全不同。
真题描述:给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例: 给定数组 nums = [-1, 0, 1, 2, -1, -4], 满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]
思路分析
三数之和延续两数之和的思路,我们可以把求和问题变成求差问题——固定其中一个数,在剩下的数中寻找是否有两个数和这个固定数相加是等于0的。
虽然乍一看似乎还是需要三层循环才能解决的样子,不过现在我们有了双指针法,定位效率将会被大大提升,从此告别过度循环~
(这里大家相信已经能察觉出来双指针法的使用场景了,一方面,它可以做到空间换时间;另一方面,它也可以帮我们降低问题的复杂度。)
双指针法用在涉及求和、比大小类的数组题目里时,大前提往往是:该数组必须有序。否则双指针根本无法帮助我们缩小定位的范围,压根没有意义。因此这道题的第一步是将数组排序:
nums = nums.sort((a,b)=>{
return a-b
})
然后,对数组进行遍历,每次遍历到哪个数字,就固定哪个数字。然后把左指针指向该数字后面一个坑里的数字,把右指针指向数组末尾,让左右指针从起点开始,向中间前进:
每次指针移动一次位置,就计算一下两个指针指向数字之和加上固定的那个数之后,是否等于0。如果是,那么我们就得到了一个目标组合;否则,分两种情况来看:
- 相加之和大于0,说明右侧的数偏大了,右指针左移
- 相加之和小于0,说明左侧的数偏小了,左指针右移
tips:这个数组在题目中要求了“不重复的三元组”,因此我们还需要做一个重复元素的跳过处理。这一点在编码实现环节大家会注意到。
编码实现
/**
* @param {number[]} nums
* @return {number[][]}
*/
const threeSum = function(nums) {
// 用于存放结果数组
let res = []
// 给 nums 排序
nums = nums.sort((a,b)=>{
return a-b
})
// 缓存数组长度
const len = nums.length
// 注意我们遍历到倒数第三个数就足够了,因为左右指针会遍历后面两个数
for(let i=0;i<len-2;i++) {
// 左指针 j
let j=i+1
// 右指针k
let k=len-1
// 如果遇到重复的数字,则跳过
if(i>0&&nums[i]===nums[i-1]) {
continue
} while(j<k) {
// 三数之和小于0,左指针前进
if(nums[i]+nums[j]+nums[k]<0){
j++
// 处理左指针元素重复的情况
while(j<k&&nums[j]===nums[j-1]) {
j++
}
} else if(nums[i]+nums[j]+nums[k]>0){
// 三数之和大于0,右指针后退 k--
// 处理右指针元素重复的情况
while(j<k&&nums[k]===nums[k+1]) {
k-- }
} else {
// 得到目标数字组合,推入结果数组
res.push([nums[i],nums[j],nums[k]])
// 左右指针一起前进
j++
k--
// 若左指针元素重复,跳过
while(j<k&&nums[j]===nums[j-1]) {
j++
}
// 若右指针元素重复,跳过
while(j<k&&nums[k]===nums[k+1]) {
k--
}
}
}
}
// 返回结果数组
return res
};
4.结局:N 数之和
请用算法实现,从给定的无需、不重复的数组A中,取出N个数,使其相加和为M。并给出算法的时间、空间复杂度,如:
var arr = [1, 4, 7, 11, 9, 8, 10, 6];
var N = 3;
var M = 27;
Result:
[7, 11, 9], [11, 10, 6], [9, 8, 10]
【解题思路】:利用二进制
根据数组长度构建二进制数据,再选择其中满足条件的数据。
我们用 1 和 0 来表示数组中某位元素是否被选中。因此,可以用 0110 来表示数组中第 1 位和第 2 位被选中了。
所以,本题可以解读为:
- 数组中被选中的个数是 N 。
- 被选中的和是 M 。
我们的算法思路逐渐清晰起来:遍历所有二进制,判断选中个数是否为 N ,然后再求对应的元素之和,看其是否为 M 。
1.从数组中取出 N 个数
例如:
var arr = [1, 2, 3, 4];
var N = 3;
var M = 6;
如何判断 N=3 是,对应的选取二进制中有几个 1 喃?
最简单的方式就是:
const n = num => num.toString(2).replace(/0/g, '').length
这里我们尝试使用一下位运算来解决本题,因为位运算是不需要编译的(位运算直接用二进制进行表示,省去了中间过程的各种复杂转换,提高了速度),肯定速度最快。
我们知道 1&0=0 、 1&1=1 ,1111&1110=1110 ,即 15&14=14 ,所以我们每次 & 比自身小 1 的数都会消除一个 1 ,所以这里建立一个迭代,通过统计消除的次数,就能确定最终有几个 1 了:
const n = num => {
let count = 0
while(num) {
num &= (num - 1)
count++
}
return count
}
2.和为 M
现在最后一层判断就是选取的这些数字和必须等于 M ,即根据 N 生成的对应二进制所在元素上的和 是否为 M
比如 1110 ,我们应该判断 arr[0] + arr[1] + arr[2] 是否为 M。
那么问题也就转化成了如何判断数组下标是否在 1110 中呢?如何在则求和
其实也很简单,比如下标 1 在,而下标 3 不在。我们把 1 转化成 0100 , 1110 & 0100 为 1100, 大于 0 ,因此下标 1 在。而 1110 & 0001 为 0 ,因此 下标 3 不在。
所以求和我们可以如下实现:
let arr = [1,2,3,4]
// i 为满足条件的二进制
let i = 0b1110
let s = 0, temp = []
let len = arr.length
for (let j = 0; j < len; j++) {
if ( i & 1 << (len - 1 - j)) {
s += arr[j]
temp.push(arr[j])
}
}
console.log(temp)
// => [1, 2, 3]
最终实现
// 参数依次为目标数组、选取元素数目、目标和
const search = (arr, count, sum) => {
// 计算某选择情况下有几个 1,也就是选择元素的个数
const getCount = num => {
let count = 0
while(num) {
num &= (num - 1)
count++
}
return count
}
let len = arr.length, bit = 1 << len, res = []
// 遍历所有的选择情况
for(let i = 1; i < bit; i++){
// 满足选择的元素个数 === count
if(getCount(i) === count){
let s = 0, temp = []
// 每一种满足个数为 N 的选择情况下,继续判断是否满足 和为 M
for(let j = 0; j < len; j++){
// 建立映射,找出选择位上的元素
if(i & 1 << (len - 1 -j)) {
s += arr[j]
temp.push(arr[j])
}
}
// 如果这种选择情况满足和为 M
if(s === sum) {
res.push(temp)
}
}
}
return res
}
4.盛水最多的容器
给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器。
示例1
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例2
输入:height = [1,1]
输出:1
示例3
输入:height = [4,3,2,1,4]
输出:16
示例4
输入:height = [1,2,1]
输出:2
提示:
- n = height.length
- 2 <= n <= 3 * 104
- 0 <= height[i] <= 3 * 104
虽然是中等难度,但这道题解起来还是比较简单的,老规矩,我们看下符合第一直觉
暴力法:
幼儿园数学题:矩形面积 = 长 * 宽
放到我们这道题中,矩形的长和宽就分别对应:
- 长:两条垂直线的距离
- 宽:两条垂直线中较短的一条的长度
双重 for 循环遍历所有可能,记录最大值。
const maxArea = function(height) {
let max = 0 // 最大容纳水量
for (let i = 0; i < height.length; i++) {
for (let j = i + 1; j < height.length; j++) {
// 当前容纳水量
let cur = (j - i) * Math.min(height[i], height[j])
if (cur > max) {
max = cur
}
}
}
return max
}
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
暴力法时间复杂度 O(n^2) 太高了,我们还是要想办法进行优化。
双指针:
我们可以借用双指针来减少搜索空间,转换为双指针的视角后,回顾矩形的面积对应关系如下:
(矩形面积)容纳的水量 = (两条垂直线的距离)指针之间的距离 * (两个指针指向的数字中的较小值)两条垂直线中较短的一条的长度
设置两个指针,分别指向头和尾(i指向头,j指向尾),不断向中间逼近,在逼近的过程中为了找到更长的垂直线:
- 如果左边低,将i右移
- 如果右边低,将j左移
有点贪心思想那味儿了,因为更长的垂直线能组成更大的面积,所以我们放弃了较短的那一条的可能性。
但是这么做,我们有没有更能漏掉一个更大的面积的可能性呢?
先告诉你答案是不会漏掉。
关于该算法的正确性证明已经有很多同学们给出了答案,感兴趣的请戳下面链接。
const maxArea = function(height) {
let max = 0 // 最大容纳水量
let left = 0 // 左指针
let right = height.length - 1 // 右指针
while (left < right) {
// 当前容纳水量
let cur = (right - left) * Math.min(height[left], height[right]);
max = Math.max(cur, max)
height[left] < height[right] ? left ++ : right --
}
return max
};
- 时间复杂度:O(n)
- 空间复杂度:O(1)
4.双指针法中的“对撞指针”法
在上面这道题中,左右指针一起从两边往中间位置相互迫近,这样的特殊双指针形态,被称为“对撞指针”。
什么时候你需要联想到对撞指针?
这里我给大家两个关键字——“有序”和“数组”。
没错,见到这两个关键字,立刻把双指针法调度进你的大脑内存。普通双指针走不通,立刻想对撞指针!
即便数组题目中并没有直接给出“有序”这个关键条件,我们在发觉普通思路走不下去的时候,也应该及时地尝试手动对其进行排序试试看有没有新的切入点——没有条件,创造条件也要上。
对撞指针可以帮助我们缩小问题的范围,这一点在“三数求和”问题中体现得淋漓尽致:因为数组有序,所以我们可以用两个指针“画地为牢”圈出一个范围,这个范围以外的值不是太大就是太小、直接被排除在我们的判断逻辑之外,这样我们就可以把时间花在真正有意义的计算和对比上。如此一来,不仅节省了计算的时间,更降低了问题本身的复杂度,我们做题的速度也会大大加快。
二.字符串
(一).Javscript字符串的常用方法有哪些?
(1)操作方法
我们也可将字符串常用的操作方法归纳为增、删、改、查
增
这里增的意思并不是说直接增添内容,而是创建字符串的一个副本,再进行操作
除了常用+以及${}进行字符串拼接之外,还可通过concat
concat
用于将一个或多个字符串拼接成一个新字符串
let stringValue = "hello ";
let result = stringValue.concat("world");
console.log(result); // "hello world"
console.log(stringValue); // "hello
删
这里的删的意思并不是说删除原字符串的内容,而是创建字符串的一个副本,再进行操作
常见的有:
- slice()
- substr()
- substring()
这三个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。
let stringValue = "hello world";
console.log(stringValue.slice(3)); // "lo world"
console.log(stringValue.substring(3)); // "lo world"
console.log(stringValue.substr(3)); // "lo world"
console.log(stringValue.slice(3, 7)); // "lo w"
console.log(stringValue.substring(3,7)); // "lo w"
console.log(stringValue.substr(3, 7)); // "lo worl"
改
这里改的意思也不是改变原字符串,而是创建字符串的一个副本,再进行操作
常见的有:
- trim()、trimLeft()、trimRight()
- repeat()
- padStart()、padEnd()
- toLowerCase()、 toUpperCase()
trim()、trimLeft()、trimRight()
删除前、后或前后所有空格符,再返回新的字符串
let stringValue = " hello world ";
let trimmedStringValue = stringValue.trim();
console.log(stringValue); // " hello world "
console.log(trimmedStringValue); // "hello world"
repeat()
接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果
let stringValue = "na ";
let copyResult = stringValue.repeat(2) // na na
padStart()和padEnd()
某些字符串我们需要对其进行前后的填充,来实现某种格式化效果,ES8中增加了 padStart 和 padEnd 方法,分别是对字符串的首尾进行填充的。
const message = "Hello World"
console.log(message.padStart(15, "a")) // aaaaHello World
console.log(message.padEnd(15, "b")) // Hello Worldbbbb
//我们简单具一个应用场景:比如需要对身份证、银行卡的前面位数进行隐藏:
const cardNumber = "3242523524256245223879"
const lastFourNumber = cardNumber.slice(-4)
const finalCardNumber = lastFourNumber.padStart(cardNumber.length, "*")
console.log(finalCardNumber) // ******************3879
toLowerCase()、 toUpperCase()
大小写转化
let stringValue = "hello world";
console.log(stringValue.toUpperCase()); // "HELLO WORLD"
console.log(stringValue.toLowerCase()); // "hello world"
// 首字母大写操作,使用toUpperCase和slice方法
let city = 'paris';
city = city[0].toUpperCase() + city.slice(1);
console.log(city); // "Paris"
查
除了通过索引的方式获取字符串的值,还可通过:
- chatAt()
- charCodeAt()
- fromCharCode()
- indexOf()
- startWith()
- endWith()
- includes()
charAt()
返回给定索引位置的字符,由传给方法的整数参数指定
let message = "abcde";
console.log(message.charAt(2)); // "c"
charCodeAt()
charCodeAt() 方法返回字符串中指定索引处字符的Unicode。
fromCharCode()
fromCharCode() 方法将 Unicode 值转换为字符。这是String对象的静态方法,语法始终是String.fromCharCode()。
indexOf()和includes()
从字符串开头去搜索传入的字符串,并返回位置(如果没找到,则返回 -1 )
let stringValue = "hello world";
console.log(stringValue.indexOf("a")); //-1
//从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值。
let message = "foobarbaz";
console.log(message.includes("bar")); // true
console.log(message.includes("qux")); // false
startWith()、endWith()
EndWith() 方法确定字符串是否以指定字符串的字符结尾。如果字符串以字符结尾,则此方法返回 true,否则返回 false,而startWith()恰好相反。
let message = "foobarbaz";
console.log(message.startsWith("foo")); // true
console.log(message.startsWith("bar")); // false
console.log(message.endsWith("baz")); // true
console.log(message.endsWith("bar")); // false
(2)转换方法
split()
把字符串按照指定的分割符,拆分成数组中的每一项
let str = "12+23+34"
let arr = str.split("+") // [12,23,34]
(3)模板匹配方法
针对正则表达式,字符串设计了几个方法:
- match()匹配
接收一个参数,可以是一个正则表达式字符串,也可以是一个对象,返回数组RegExp。
let text = "cat, bat, sat, fat";
let pattern = /.at/;
let matches = text.match(pattern);
console.log(matches[0]); // "cat"
- search()搜索
接收一个参数,可以是一个正则表达式字符串,也可以是一个对象,找到则返回匹配索引,否则返回 -1RegExp
let text = "cat, bat, sat, fat";
let pos = text.search(/at/);
console.log(pos); // 1
- replace()替换
接收两个参数,第一个参数为匹配的内容,第二个参数为替换的元素(可用函数)
let text = "cat, bat, sat, fat";
let result = text.replace("at", "ond");
console.log(result); // "cond, bat, sat, fat"
(二).反转字符串
在 JS 中,反转字符串我们直接调相关 API 即可,相信不少同学都能手到擒来:
// 定义被反转的字符串
const str = 'juejin'
// 定义反转后的字符串
const res = str.split('').reverse().join('')
console.log(res) // nijeuj
(这段代码需要你非常熟悉,一些公司一面为了试水,有时会单独考这个操作)。
1.判断一个字符串是否是回文字符串
回文字符串,就是正着读和倒着读都一模一样的字符串,比如这样的:
'yessey'
结合这个定义,我们不难写出一个判定回文字符串的方法:
function isPalindrome(str) {
// 先反转字符串
const reversedStr = str.split('').reverse().join('')
// 判断反转前后是否相等
return reversedStr === str
}
同时,回文字符串还有另一个特性:如果从中间位置“劈开”,那么两边的两个子串在内容上是完全对称的。因此我们也可以结合对称性来做判断:
function isPalindrome(str) {
// 缓存字符串的长度
const len = str.length
// 遍历前半部分,判断和后半部分是否对称
for(let i=0;i<len/2;i++) {
if(str[i]!==str[len-i-1]) {
return false
}
}
return true
}
(谨记这个对称的特性,非常容易用到)
2.[高频真题解读]:回文字符串的衍生问题
真题描述:给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。
示例 1: 输入: "aba"
输出: True
示例 2:
输入: "abca"
输出: True
解释: 你可以删除c字符。
注意: 字符串只包含从 a-z 的小写字母。字符串的最大长度是50000。
思路分析1
这道题很多同学第一眼看过去,可能本能地会想到这样一种解法:若字符串本身不回文,则直接遍历整个字符串。遍历到哪个字符就删除哪个字符、同时对删除该字符后的字符串进行是否回文的判断,看看存不存在删掉某个字符后、字符串能够满足回文的这种情况。
这个思路真的实现起来的话,在判题系统眼里其实也是没啥毛病的。但是在面试官看来,就有点问题了——这不是一个高效的解法。
如何判断自己解决回文类问题的解法是否“高效”?其中一个很重要的标准,就是看你对回文字符串的对称特性利用得是否彻底。
思路分析2
字符串题干中若有“回文”关键字,那么做题时脑海中一定要冒出两个关键字——对称性 和 双指针。这两个工具一起上,足以解决大部分的回文字符串衍生问题。
回到这道题上来,我们首先是初始化两个指针,一个指向字符串头部,另一个指向尾部:
如果两个指针所指的字符恰好相等,那么这两个字符就符合了回文字符串对对称性的要求,跳过它们往下走即可。如果两个指针所指的字符串不等,比如这样:
那么就意味着不对称发生了,意味着这是一个可以“删掉试试看”的操作点。我们可以分别对左指针字符和右指针字符尝试进行“跳过”,看看区间在 [left+1, right] 或 [left, right-1] 的字符串是否回文。如果是的话,那么就意味着如果删掉被“跳过”那个字符,整个字符串都将回文:
比如说这里我们跳过了 b,[left+1, right] 的区间就是 [2, 2],它对应 c 这个字符,单个字符一定回文。这样一来,删掉 b 之后,左右指针所指的内部区间是回文的,外部区间也是回文的,可以认为整个字符串就是一个回文字符串了。
编码实现
const validPalindrome = function(s) {
// 缓存字符串的长度
const len = s.length
// i、j分别为左右指针
let i=0, j=len-1
// 当左右指针均满足对称时,一起向中间前进
while(i<j&&s[i]===s[j]) { i++ j-- }
// 尝试判断跳过左指针元素后字符串是否回文
if(isPalindrome(i+1,j)) {
return true
}
// 尝试判断跳过右指针元素后字符串是否回文
if(isPalindrome(i,j-1)) {
return true
}
// 工具方法,用于判断字符串是否回文
function isPalindrome(st, ed) {
while(st<ed) {
if(s[st] !== s[ed]) {
return false
}
st++
ed--
}
return true
}
// 默认返回 false
return false
};
三.对象
两种类型
Native:在ECMAScript标准中定义和描述,包括JavaScript内置对象(数组,日期对象等)和用户自定义对象;
Host:在主机环境(如浏览器)中实现并提供给开发者使用,比如Windows对象和所有的DOM对象;
(一).对象的创建
面试官:有几种创建对象的方式,字面量相对于 new 创建对象有哪些优势?
推荐阅读:4种用JavaScript创建对象的方法
最常用的创建对象的两种方式是new和字面量:
-
字面量(常用)
-
new 构造函数(常用)
-
Object.create()
// 第一种方式:字面量(从结果上看这两种算是一类,写法不一样)
var o1={name:'o1'};//字面量对象--默认这个对象原型链指向Object
// 第二种方式:通过new
var o2=new Object({name:'o2'}); //通过new Object声明的一个对象
var M=function(name){ this.name = name;};
var o3=new M('o3');
// 第三种方式:Object.create方法创建
var p={name:'p'};
var o4=Object.create(p);
字面量创建对象的优势所在:
- 代码量更少,更易读
- 对象字面量运行速度更快,它们可以在解析的时候被优化。他不会像 new一个对象一样,解析器需要顺着作用域链从当前作用域开始查找,如果在当前作用域找到了名为 Object()的函数就执行,如果没找到,就继续顺着作用域链往上照,直到找到全局 Object() 构造函数为止。
- Object()构造函数可以接收参数,通过这个参数可以把对象实例的创建过程委托给另一个内置构造函数,并返回另外一个对象实例,而这往往不是你想要的。
对于 Object.create()方式创建对象:
Object.create(proto, [propertiesObject]);
- proto:新创建对象的原型对象。
- propertiesObject:(可选)可为创建的新对象设置属性和值。
一般用于继承:
var People = function (name){
this.name = name;
};
People.prototype.sayName = function (){
console.log(this.name);
}
function Person(name, age){
this.age = age;
People.call(this, name); // 使用call,实现了People属性的继承
};
// 使用Object.create()方法,实现People原型方法的继承,并且修改了constructor指向
Person.prototype = Object.create(People.prototype, {
constructor: {
configurable: true,
enumerable: true,
value: Person,
writable: true
}
});
Person.prototype.sayAge = function (){
console.log(this.age);
}
var p1 = new Person('person1', 25);
p1.sayName(); //'person1'
p1.sayAge(); //25
面试官:new/字面量 与 Object.create(null) 创建对象的区别?
- new 和 字面量创建的对象的原型指向 Object.prototype,会继承 Object 的属性和方法。
- 而通过 Object.create(null) 创建的对象,其原型指向 null,null作为原型链的顶端,没有也不会继承任何属性和方法。
(二)访问对象属性
const obj = {
info: 'wakaka',
inner: {
a: 10,
b: 20
},
arr: [1, 2],
sayHi: (name) => {
console.log(`hi,${name}`)
}
}
// 用 dot(点 .) 的方式访问
console.log(obj.info) // wakaka
console.log(obj.inner) // {"a":10,"b":20}
console.log(obj.arr) // [1,2]
obj.sayHi('dengke') // hi,dengke
// 用 [] 的方式访问
console.log(obj['info']) // wakaka
console.log(obj['inner']) // {"a":10,"b":20}
console.log(obj['arr']) // [1,2]
obj['sayHi']('dengke') // hi,dengke
- 如果要访问的对象不存在,可以使用 逻辑运算符 || 指定默认值
只要“||”前面为false,不管“||”后面是true还是false,都返回“||”后面的值。
只要“||”前面为true,不管“||”后面是true还是false,都返回“||”前面的值。
console.log(obj.age || 18) // 18
- 很多时候,我们想根据这个值是否为空来做接下来的操作,可以使用空值运算符 (??) (es11)
有一个冷门运算符??可以判断undefined和null,这样是比较符合普遍需求的。
const age = 0
const a = age ?? 123
console.log(a) // 0
- 可选链式操作符(?.)(es11)
这是当对象上没有这个键的时候,不会报错,而是赋值undefined
const foo = { name: "zengbo" }
let a = foo.name?.toUpperCase() // "ZENGBO"
let b = foo.name?.firstName?.toUpperCase() // "undefined"
(二)对象属性的遍历\枚举\删除\合并\检测
1.遍历
2.枚举对象的属性
在JS里面枚举对象属性一共有三种方法:
- for in: 会遍历对象中所有的可枚举属性(包括自有属性和继承属性)
const obj = {
itemA: 'itemA',
itemB: 'itemB'
}
// 使用Object.create创建一个原型为obj的对象 (模拟继承来的属性)
var newObj = Object.create(obj)
newObj.newItemA = 'newItemA'
newObj.newItemB = 'newItemB'
for(i in newObj){
console.log(i)
}
// newItemA
// newItemB
// itemA
// itemB
// 现在我们将其中的一个属性变为不可枚举属性
Object.defineProperty(newObj, 'newItemA', {
enumerable: false
})
for(i in newObj){
console.log(i)
}
// newItemB
// itemA
// itemB
如果不想让for…in枚举继承来的属性可以借助Object.prototype.hasOwnProperty()
// 接上例
for(i in newObj){
if( newObj.hasOwnProperty(i) ) console.log(i)
}
// newItemB
Object.prototype.hasOwnProperty()该方法在下文有更具体的介绍
- Object.keys(): 会返回一个包括所有的可枚举的自有属性的名称组成的数组
// 接上例
const result = Object.keys(newObj)
console.log(result) // ["newItemB"]
复制代码
- Object.getOwnPropertyNames(): 会返回自有属性的名称 (不管是不是可枚举的)
// 接上例
const result = Object.keys(newObj)
console.log(result) // ['newItemA','newItemB']
复制代码
Object.getOwnPropertyNames()该方法在下文有更具体的介绍
3.删除
const o = {
p: 10,
m: 20
}
delete o.p
console.log(o) // { m: 20 }
// 删除对象的属性后,在访问返回 undefined
console.log(o.p) // undefined
4.对象的合并
5.检测
- typeof 常用 多用于原始数据类型的判断
const fn = function(n){
console.log(n)
}
const str = 'string'
const arr = [1,2,3]
const obj = {
a:123,
b:456
}
const num = 1
const b = true
const n = null
const u = undefined
console.log(typeof str) // string
console.log(typeof arr) // object
console.log(typeof obj) // object
console.log(typeof num) // number
console.log(typeof b) // boolean
console.log(typeof n) // object null是一个空的对象
console.log(typeof u) // undefined
console.log(typeof fn) // function
复制代码
通过上面的检测我们发现typeof检测的Array和Object的返回类型都是Object,因此用typeof是无法检测出来数组和对象的。
- tostring 常用 最实用的检测各种类型
我们经常会把这个封装成一个函数,使用起来更加方便
/**
* @description: 数据类型的检测
* @param {any} data 要检测数据类型的变量
* @return {string} type 返回具体的类型名称【小写】
*/
const isTypeOf = (data) => {
return Object.prototype.toString.call(data).replace(/\[object (\w+)\]/, '$1').toLowerCase()
}
console.log(isTypeOf({})) // object
console.log(isTypeOf([])) // array
console.log(isTypeOf("ss")) // string
console.log(isTypeOf(1)) // number
console.log(isTypeOf(false)) // boolean
console.log(isTypeOf(/w+/)) // regexp
console.log(isTypeOf(null)) // null
console.log(isTypeOf(undefined)) // undefined
console.log(isTypeOf(Symbol("id"))) // symbol
console.log(isTypeOf(() => { })) // function
复制代码
(四)9种日常JavaScript编程中经常使用的对象创建模式
(五)JavaScript 对象所有API解析【2021版】
1.Object.defineProperty
在ES3中,除了一些内置属性(如:Math.PI),对象的所有的属性在任何时候都可以被修改、插入、删除。在ES5中,我们可以设置属性是否可以被改变或是被删除——在这之前,它是内置属性的特权。ES5中引入了属性描述符的概念,我们可以通过它对所定义的属性有更大的控制权。这些属性描述符(特性)包括:
value——当试图获取属性时所返回的值。writable——该属性是否可写。enumerable——该属性在for in循环中是否会被枚举configurable——该属性是否可被删除。set()——该属性的更新操作所调用的函数。get()——获取属性值时所调用的函数。另外,数据描述符(其中属性为:enumerable,configurable,value,writable)与存取描述符(其中属性为enumerable,configurable,set(),get())之间是有互斥关系的。在定义了set()和get()之后,描述符会认为存取操作已被 定义了,其中再定义value和writable会引起错误。以下是ES3风格的属性定义方式:
var person = {};
person.legs = 2;
以下是等价的 ES5 通过数据描述符定义属性的方式:
var person = {};
Object.defineProperty(person, 'legs', {
value: 2,
writable: true,
configurable: true,
enumerable: true
});
其中, 除了 value 的默认值为undefined以外,其他的默认值都为false。这就意味着,如果想要通过这一方式定义一个可写的属性,必须显示将它们设为true。或者,我们也可以通过ES5的存储描述符来定义:
var person = {};
Object.defineProperty(person, 'legs', {
set:function(v) {
returnthis.value = v;
},
get: function(v) {
returnthis.value;
},
configurable: true,
enumerable: true
});
person.legs = 2;
这样一来,多了许多可以用来描述属性的代码,如果想要防止别人篡改我们的属性,就必须要用到它们。此外,也不要忘了浏览器向后兼容ES3方面所做的考虑。例如,跟添加Array.prototype属性不一样,我们不能在旧版的浏览器中使用shim这一特性。另外,我们还可以(通过定义nonmalleable属性),在具体行为中运用这些描述符:
var person = {};
Object.defineProperty(person, 'heads', {value: 1});
person.heads = 0; // 0
person.heads; // 1 (改不了)
delete person.heads; // false
person.heads // 1 (删不掉)
Vue和Vue-router源码中的应用
(1)Vue中响应式原理
(2)vue-router 源码里就是类似这样写的,this.$router,this.$route
无法修改。
// vue-router 源码
Object.defineProperty(Vue.prototype, '$router', {
get () { returnthis._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { returnthis._routerRoot._route }
})
日常开发中的使用
近日发现有挺多人对对象基础API不熟悉,举个开发中常见的需求,经常会有类似的封装http到原型Vue.prototype,一般人是这样封装的,但容易被篡改。
function Vue(){
console.log('test vue');
}
function http(){
console.log('我是调用接口的http');
}
Vue.prototype.$http = http;
var vm = new Vue();
vm.$http()
vm.$http = 1; // 一旦被修改,虽然一般正常情况下不会被修改
vm.$http(); // 再次调用报错
熟悉Object.defineProperty或者说熟悉对象API的人,一般是如下代码写的,则不会出现被修改的问题。
function Vue(){
console.log('test vue');
};
function http(){
console.log('我是调用接口的http');
};
Object.defineProperty(Vue.prototype, '$http', {
get(){
return http;
}
});
var vm = new Vue();
vm.$http();
vm.$http = 1; // 这里无法修改
vm.$http(); // 调用正常
2.Object.assign()
语法
//参数:target 目标参数,sources源对象
//返回值:目标对象
Object.assign(target, ...sources)
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。常用来合并对象。
const obj1 = { a: 1, b: 2 }
const obj2 = { b: 4, c: 5 }
const obj3 = Object.assign(obj1, obj2)
const obj4 = Object.assign({}, obj1) // 克隆了obj1对象
console.log(obj1) // { a: 1, b: 4, c: 5 } 对同名属性b进行了替换 obj1发生改变是因为obj2赋给了obj1
console.log(obj2) // { b: 4, c: 5 }
console.log(obj3) // { a: 1, b: 4, c: 5 }
console.log(obj4) // { a: 1, b: 4, c: 5 }
【注意】
1.如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。
2.Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。
3.assign其实是浅拷贝而不是深拷贝
也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。同名属性会替换
const obj5 = {
name: 'dengke',
a: 10,
fn: {
sum: 10
}
}
const obj6 = Object.assign(obj1, obj5)
console.log(obj6) // { a: 10, b: 2, name: 'dengke', fn: {…}}
console.log(obj1) // {a: 10, b: 2, name: 'dengke', fn: {…}} 对同名属性a进行了替换
- Object.assign 不会在那些source对象值为null或undefined的时候抛出错误。
参考链接
JavaScript日常开发中常用的Object操作方法总结
四.日常开发中的算法
1. tree数据处理方案
五.LeetCode刷题笔记
主要有以下几类高频考题:
- 最长回文子串 【中等】【双指针】【面试真题】
- 最长公共前缀 【简单】【双指针】
- 无重复字符的最长子串【中等】【双指针】
- 最小覆盖子串 【困难】【滑动窗口】【面试真题】
【面试真题】最长回文子串【双指针】
1. 最长回文子串(中等)
【LeetCode 直通车】:5 最长回文子串(中等)
/**
* @param {string} s
* @return {string}
*/
var longestPalindrome = function(s) {
if (s.length === 1) return s;
let maxRes = 0, maxStr = '';
for (let i = 0; i < s.length; i++) {
let str1 = palindrome(s, i, i);
let str2 = palindrome(s, i, i + 1);
if (str1.length > maxRes) {
maxStr = str1;
maxRes = str1.length;
}
if (str2.length > maxRes) {
maxStr = str2;
maxRes = str2.length;
}
}
return maxStr;
};
function palindrome(s, l, r) {
while (l >= 0 && r < s.length && s[l] === s[r]) {
l--;
r++;
}
return s.slice(l + 1, r);
}
2.最长公共前缀【双指针】
【LeetCode 直通车】:14 最长公共前缀(简单)
/**
* @param {string[]} strs
* @return {string}
*/
var longestCommonPrefix = function(strs) {
if (strs.length === 0) return "";
let first = strs[0];
if (first === "") return "";
let minLen = Number.MAX_SAFE_INTEGER;
for (let i = 1; i < strs.length; i++) {
const len = twoStrLongestCommonPrefix(first, strs[i]);
minLen = Math.min(len, minLen);
}
return first.slice(0, minLen);
};
function twoStrLongestCommonPrefix (s, t) {
let i = 0, j = 0;
let cnt = 0;
while (i < s.length && j < t.length) {
console.log(s[i], t[j], cnt)
if (s[i] === t[j]) {
cnt++;
} else {
return cnt;
}
i++;
j++;
}
return cnt;
}
3.无重复字符的最长子串【双指针】
【LeetCode 直通车】:3 无重复字符的最长子串(中等)
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
let window = {};
let left = 0, right = 0;
let maxLen = 0, maxStr = '';
while (right < s.length) {
let c = s[right];
right++;
if (window[c]) window[c]++;
else window[c] = 1
while (window[c] > 1) {
let d = s[left];
left++;
window[d]--;
}
if (maxLen < right - left) {
maxLen = right - left;
}
}
return maxLen;
};