6、数组的方法介绍
6.1、forEach()
forEach方法用于调用数组的每个元素,并将元素传递给回调函数。数组中的每个值都会调用回调函数。其语法如下:
array.forEach(function(currentValue, index, arr){}, thisValue)
该方法的第一个参数为回调函数,是必传的。它有三个参数:
* currentValue;必须,当前元素
* index:可选,当前元素的索引值
* arr: 可选,当前元素所属的数组对象
let arr=[1, 2, 3, 4, 5] arr.forEach((item, index, arr) => { console.log(index + ':' + item) })
该方法的第二个参数,用来绑定回调函数内部this变量(前提是回调函数不能是箭头函数,因为箭头函数没有this),是可选的;
let arr=[1, 2, 3, 4, 5] let arr1=[9, 8, 7, 6, 5] arr.forEach(function(item, index, arr) { console.log(this[index]) // 9 8 7 6 5 }, arr1) // this 就是 arr1数组
小结:
* forEach方法不会改变原数组,也没有返回值
* forEach无法使用break, continue跳出循环,使用return 时,效果和在for循环中使用 continue 一致;
* forEach方法无法遍历对象,仅适用于数组的遍历
6.2、map()
map()方法会返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。该方法按照原始数组元素顺序依次处理元素。其语法如下:
array.map(function(currentValue, index, arr){}, thisValue) 1
该方法的第一个参数为回调函数,是必传的。它有三个参数:
* currentValue:必须,当前元素的值
* index: 可选,当前元素的索引值
* arr: 可选,当前元素所属的数组对象
let arr = [1, 2, 3] arr.map(function(item) { return item + 1 }) // 输出结果:[2, 3, 4]
同样,它也有第二个参数,用来绑定参数函数内部的this变量。是可选的;
let arr = ['a', 'b', 'c']; [1, 2].map(function (e) { return this[e]; }, arr) // 输出结果: ['b', 'c'] this就是arr
该方法可以链式调用:
let arr = [1, 2, 3] arr.map(item => item + 1).map(item => item +1) // 第一个map的输出结果:[2, 3, 4] // 最终输出结果: [3, 4, 5]
小结:
* map方法不会对空数据进行检测
* map方法遍历数组是会返回一个新数组,不会改变原数组
* map方法无法遍历对象,仅适用于数组的遍历
6.3、filter()
filter()方法用于过滤数组,满足条件的元素会被返回。它的第一个参数是回调函数,所有数组元素依次执行该函数,返回结果为true的元素会被返回,没有符合条件的元素,则返回空数组。其语法如下:
array.filter(function(currentValue,index,arr), thisValue)
该方法的第一个参数是回调函数,是必传的。它有三个参数:
* currentValue:必须,当前元素的值
* index:可选,当前元素的索引值
* arr:可选,当前元素所属的数组对象
const arr = [1, 2, 3, 4, 5] arr.filter(item => item > 2) // 输出结果:[3, 4, 5]
同样,它也有第二个参数,用来绑定参数函数内部的this变量。是可选的。
新知识:
可以使用filter()方法来移除数组中的 undefined、null、NAN等值
let arr = [1, undefined, 2, null, 3, false, '', 4, 0] arr.filter(Boolean) 等价于: array.filter((item) => {return Boolean(item)}) // 输出结果:[1, 2, 3, 4]
小结:
* filter方法会返回一个新数组,不会改变原数组
* filter方法不会对空数组进行检测
* filter方法仅适用于检测数组
6.4、some()、every()
some() 方法会对数组中的每一项进行遍历,只要有一个元素符合条件,就返回true,且剩余的元素不会再进行检测,否则就返回false。
every() 方法会对数组中的每一项进行遍历,只有所有元素都符合条件时,才返回true,如果数组中检测到有一个元素不满足,则整个表达式返回 false ,且剩余的元素不会再进行检测。其语法如下:
array.some(function(currentValue,index,arr),thisValue) array.every(function(currentValue,index,arr), thisValue)
两个方法的第一个参数为回调函数,是必传的,它有三个参数:
* currentValue:必须,当前元素的值
* index: 可选,当前元素的索引值
* arr: 可选,当前元素所属的数组对象
let arr = [1, 2, 3, 4, 5] arr.some(item => item > 4) // 输出结果: true let arr = [1, 2, 3, 4, 5] arr.every(item => item > 0) // 输出结果: true
小结:
* 两个方法会返回一个布尔值,且都不会改变原数组
* 两个方法都不会对空数组进行检测
* 两个方法都适用于检测数组
6.5、find(),findIndex(),findLast(),findLastIndex()
数组实例的find()方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
[1, 4, -5, 10].find((n) => n < 0) // -5
上面代码找出数组中第一个小于 0 的成员。
[1, 5, 10, 15].find(function(value, index, arr) { return value > 9; }) // 10
上面代码中,find()方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。
数组实例的findIndex()方法的用法与find()方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
[1, 5, 10, 15].findIndex(function(value, index, arr) { return value > 9; }) // 2
这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。
function f(v){ return v > this.age; } let person = {name: 'John', age: 20}; [10, 12, 26, 15].find(f, person); // 26
上面的代码中,find()函数接收了第二个参数person对象,回调函数中的this对象指向person对象。
另外,这两个方法都可以发现NaN,弥补了数组的indexOf()方法的不足。
[NaN].indexOf(NaN) // -1 [NaN].findIndex(y => Object.is(NaN, y)) // 0
上面代码中,indexOf()方法无法识别数组的NaN成员,但是findIndex()方法可以借助Object.is()方法做到。
find()和findIndex()都是从数组的0号位,依次向后检查。ES2022 新增了两个方法findLast()和findLastIndex(),从数组的最后一个成员开始,依次向前检查,其他都保持不变。
const array = [ { value: 1 }, { value: 2 }, { value: 3 }, { value: 4 } ]; array.findLast(n => n.value % 2 === 1); // { value: 3 } array.findLastIndex(n => n.value % 2 === 1); // 2
上面示例中,findLast()和findLastIndex()从数组结尾开始,寻找第一个value属性为奇数的成员。结果,该成员是{ value: 3 },位置是2号位。
6.6、includes()
Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。ES2016 引入了该方法。
[1, 2, 3].includes(2) // true [1, 2, 3].includes(4) // false [1, 2, NaN].includes(NaN) // true
该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。
[1, 2, 3].includes(3, 3); // false [1, 2, 3].includes(3, -1); // true
没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。
if (arr.indexOf(el) !== -1) { // ... }
indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。
[NaN].indexOf(NaN) // -1
includes使用的是不一样的判断算法,就没有这个问题。
[NaN].includes(NaN) // true
7、函数的扩展
7.1、函数参数的默认值
7.1.1、基本用法
ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
function log(x, y) { y = y || 'World'; console.log(x, y); } log('Hello') // Hello World log('Hello', 'China') // Hello China log('Hello', '') // Hello World
上面代码检查函数log()的参数y有没有赋值,如果没有,则指定默认值为World。这种写法的缺点在于,如果参数y赋值了,但是对应的布尔值为false,则该赋值不起作用。就像上面代码的最后一行,参数y等于空字符,结果被改为默认值。
为了避免这个问题,通常需要先判断一下参数y是否被赋值,如果没有,再等于默认值。
if (typeof y === 'undefined') { y = 'World'; }
ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
function log(x, y = 'World') { console.log(x, y); } log('Hello') // Hello World log('Hello', 'China') // Hello China log('Hello', '') // Hello
使用参数默认值时,函数不能有同名参数。
// 不报错 function foo(x, x, y) { // ... } // 报错 function foo(x, x, y = 1) { // ... } // SyntaxError: Duplicate parameter name not allowed in this context
7.1.2、参数默认值的位置
通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
// 例一 function f(x = 1, y) { return [x, y]; } f() // [1, undefined] f(2) // [2, undefined] f(, 1) // 报错 f(undefined, 1) // [1, 1]
上面代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined。
如果传入undefined,将触发该参数等于默认值,null则没有这个效果。
function foo(x = 5, y = 6) { console.log(x, y); } foo(undefined, null) // 5 null
上面代码中,x参数对应undefined,结果触发了默认值,y参数等于null,就没有触发默认值。
7.2、箭头函数
7.2.1、基本用法
ES6 允许使用“箭头”(=>)定义函数。 var f = v => v; // 等同于 var f = v=> v;
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
var f = () => 5; // 等同于 var f = function () { return 5 }; var sum = (num1, num2) => num1 + num2; // 等同于 var sum = function(num1, num2) { return num1 + num2; };
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
var sum = (num1, num2) => { return num1 + num2; }
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
// 报错 let getTempItem = id => { id: id, name: "Temp" }; // 不报错 let getTempItem = id => ({ id: id, name: "Temp" });
7.2.2、使用注意点
箭头函数有几个使用注意点。
(1)箭头函数没有自己的this对象。
(2)不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
上面三点中,最重要的是第一点。对于普通函数来说,内部的this指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的this对象,内部的this就是定义时上层作用域中的this(就是定义该函数时所在的作用域指向的对象)。也就是说,箭头函数内部的this指向是固定的,相比之下,普通函数的this指向是可变的。
下面例子是回调函数分别为箭头函数和普通函数,对比它们内部的this指向。
function Timer() { this.s1 = 0; this.s2 = 0; // 箭头函数 setInterval(() => this.s1++, 1000); // 普通函数 setInterval(function () { this.s2++; }, 1000); } var timer = new Timer(); setTimeout(() => console.log('s1: ', timer.s1), 3100); setTimeout(() => console.log('s2: ', timer.s2), 3100); // s1: 3 // s2: 0
上面代码中,Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数),后者的this指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1被更新了 3 次,而timer.s2一次都没更新。
箭头函数实际上可以让this指向固定化,绑定this使得它不再可变,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。
var handler = { id: '123456', init: function() { document.addEventListener('click', event => this.doSomething(event.type), false); }, doSomething: function(type) { console.log('Handling ' + type + ' for ' + this.id); } };
上面代码的init()方法中,使用了箭头函数,这导致这个箭头函数里面的this,总是指向handler对象。如果回调函数是普通函数,那么运行this.doSomething()这一行会报错,因为此时this指向document对象。
7.2.3、不适用场合
由于箭头函数使得this从“动态”变成“静态”,下面两个场合不应该使用箭头函数。
第一个场合是定义对象的方法,且该方法内部包括this。
const cat = { lives: 9, jumps: () => { this.lives--; } }
上面代码中,cat.jumps()方法是一个箭头函数,这是错误的。调用cat.jumps()时,如果是普通函数,该方法内部的this指向cat;如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域。
第二个场合是需要动态this的时候,也不应使用箭头函数。
var button = document.getElementById('press'); button.addEventListener('click', () => { this.classList.toggle('on'); });
上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。
8、对象的扩展
8.1、属性的简洁表示法
ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。
const foo = 'bar'; const baz = {foo}; baz // {foo: "bar"} // 等同于 const baz = {foo: foo};
上面代码中,变量foo直接写在大括号里面。这时,属性名就是变量名, 属性值就是变量值。
除了属性简写,方法也可以简写。
const o = { method() { return "Hello!"; } }; // 等同于 const o = { method() { return "Hello!"; } };
8.2、对象的新增方法
8.2.1、Object.is()
Object.is是用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
Object.is('foo', 'foo') // true Object.is({}, {}) // false
不同之处只有两个:一是+0不等于-0,二是NaN等于自身。
+0 === -0 //true NaN === NaN // false Object.is(+0, -0) // false Object.is(NaN, NaN) // true
8.2.2、Object.assign()
Object.assign()方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
const target = { a: 1 }; const source1 = { b: 2 }; const source2 = { c: 3 }; Object.assign(target, source1, source2); target // {a:1, b:2, c:3}
Object.assign()方法的第一个参数是目标对象,后面的参数都是源对象。
注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
const target = { a: 1, b: 1 }; const source1 = { b: 2, c: 2 }; const source2 = { c: 3 }; Object.assign(target, source1, source2); target // {a:1, b:2, c:3}