初次发布文章,有什么错误的地方请大佬们多多指教javascript
注意,ES6 内部使用严格相等运算符(===
),判断一个位置是否有值。因此,只有当一个数组成员严格等于undefined
,默认值才会生效。java
let [x=1]=[undefined] //x=1 let [y=1]=[null] //y=null
上面代码中,若是一个数组成员是null
,默认值就不会生效,由于null
不严格等于undefined
。node
若是默认值是一个表达式
,那么这个表达式是惰性求值
的,即只有在用到的时候,才会求值。栗子:git
fn=()=> { return 'aaa' } let [y=fn()]=[] //y='aaa' let [x=fn()]=[1] //x=1
上面代码中,由于x
能取到值,因此函数fn
根本不会执行。上面的代码其实等价于下面的代码。github
let x; if([1][0]===undefined){ x=fn() }else{ x=[1][0] }
默认值能够引用解构赋值的其余变量,但该变量必须已经声明。算法
let [x = 1, y = x] = []; // x=1; y=1 let [x = 1, y = x] = [2]; // x=2; y=2 let [x = 1, y = x] = [1, 2]; // x=1; y=2 let [x = y, y = 1] = []; // ReferenceError: y is not defined
上面最后一个表达式之因此会报错,是由于x
用y
作默认值时,y
尚未声明。编程
对象的解构与数组有一个重要的不一样。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。栗子:数组
let { bar, foo } = { foo: 'aaa', bar: 'bbb' }; foo // "aaa" bar // "bbb" let { baz } = { foo: 'aaa', bar: 'bbb' }; baz // undefined
若是变量名与属性名不一致,必须写成下面这样。浏览器
let { foo: baz } = { foo: 'aaa', bar: 'bbb' }; baz // "aaa"
与数组同样,解构也能够用于嵌套结构的对象。注意,下面栗子第二三
个loc
是模式,不是变量,所以不会被赋值,第一
个才是赋值数据结构
const node = { loc: { start: { line: 1, column: 5 } } }; let { loc, loc: { start }, loc: { start: { line }} } = node; line // 1 loc // Object {start: Object} start // Object {line: 1, column: 5}
对象
解构赋值----注意点(1)若是要将一个已经声明的变量用于解构赋值,必须很是当心。
// 错误的写法 let x; {x} = {x: 1}; // SyntaxError: syntax error
上面代码的写法会报错,由于 JavaScript 引擎会将{x}
理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题,即用()
。
// 正确的写法 let x; ({x} = {x: 1});
(2)因为数组本质是特殊的对象,所以能够对数组进行对象属性的解构。
let arr = [1, 2, 3]; let {0 : first, [arr.length - 1] : last} = arr; first // 1 last // 3
字符串也能够解构赋值。这是由于此时,字符串被转换成了一个相似数组的对象
。
const [a, b, c, d, e] = 'hello'; a // "h" b // "e" c // "l" d // "l" e // "o"
相似数组的对象都有一个length
属性,所以还能够对这个属性解构赋值。
let {length : len} = 'hello'; len // 5
undefined
就会触发函数参数的默认值。
[1, undefined, 3].map((x = 'yes') => x); // [ 1, 'yes', 3 ]
(1)交换变量的值
let x = 1; let y = 2; [x, y] = [y, x];
上面代码交换变量x
和y
的值,这样的写法不只简洁,并且易读,语义很是清晰。
ES6 为字符串添加了遍历器接口(详见《Iterator》一章),使得字符串能够被for...of
循环遍历。
for (let codePoint of 'foo') { console.log(codePoint) } // "f" // "o" // "o"
模板字符串(template string)是加强版的字符串,用反引号(`
)标识。它能够看成普通字符串使用,也能够用来定义多行字符串,或者在字符串中嵌入变量。模板字符串中嵌入变量,须要将变量名写在${}
之中。
// 字符串中嵌入变量 let name = "Bob", time = "today"; `Hello ${name}, how are you ${time}?` // 多行字符串 `In JavaScript this is not legal.`
模板字符串之中还能调用函数。
function fn() { return "Hello World"; } `foo ${fn()} bar` // foo Hello World bar
“标签模板”的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。
注:includes()
也能够用于数组
传统上,JavaScript 只有indexOf
方法,能够用来肯定一个字符串是否包含在另外一个字符串中。ES6 又提供了三种新方法。
其中tager表示要查找的字符串,index表示开始搜索的位置(仅用于includes和startsWith),可省略(默认从0开始)。endsWith
的行为与其余两个方法有所不一样。它针对前n
个字符
let s = 'Hello world!'; s.startsWith('world') // true s.startsWith('world', 6) // true s.endsWith('Hello', 5) // true s.includes('Hello', 6) // false
repeat
方法返回一个新字符串,表示将原字符串重复n
次。
'x'.repeat(3) // "xxx" 'hello'.repeat(2) // "hellohello" 'na'.repeat(0) // ""
ES2017 引入了字符串补全长度的功能。若是某个字符串不够指定长度,会在头部或尾部补全。padStart()
用于头部补全,padEnd()
用于尾部补全。
'x'.padStart(4, 'ab') // 'abax' 'x'.padEnd(5, 'ab') // 'xabab'
若是原字符串的长度,等于或大于最大长度
,则字符串补全不生效
,返回原字符串。
'xxx'.padStart(2, 'ab') // 'xxx'
若是用来补全的字符串与原字符串,二者的长度之和超过了最大长度,则会截去超出位数的补全字符串
。
'abc'.padStart(10, '0123456789') // '0123456abc'
若是省略第二个参数
,默认使用空格
补全长度。
'x'.padStart(4) // ' x' 'x'.padEnd(4) // 'x '
ES2019 对字符串实例新增了trimStart()
和trimEnd()
这两个方法。它们的行为与trim()
一致,trimStart()
消除字符串头部的空格,trimEnd()
消除尾部的空格。它们返回的都是新字符串
,不会修改原始字符串
。
const s = ' abc '; s.trim() // "abc" s.trimStart() // "abc " s.trimEnd() // " abc"
ES6 在Number
对象上,新提供了Number.isFinite()
和Number.isNaN()
两个方法。Number.isFinite()
用来检查一个数值是否为有限的(finite),即不是Infinity
。
Number.isFinite(15); // true Number.isFinite(0.8); // true Number.isFinite(NaN); // false //Number.isFinite()和isFinite()的区别是isFinite()会将值转成number类型再执行isFinite(),而Number.isFinite()只针对数值 isFinite(15); // true isFinite('15'); // true Number('15')---15 isFinite(true); // true Number(true)---1 isFinite(false); // true Number(false)---0 isFinite(null); // true Number(null)-----0 isFinite(NaN); // false Number(NaN)-----NaN isFinite(undefined); // false Number(NaN)-----NaN isFinite('str'); // false Number('str')-----NaN
注意,若是参数类型不是数值
,Number.isFinite
一概返回false
。
Number.isNaN()
用来检查一个值是否为NaN
。
Number.isNaN(NaN) // true Number.isNaN(15) // false Number.isNaN('15') // false Number.isNaN(true) // false Number.isNaN(9/NaN) // true Number.isNaN('true' / 0) // true Number.isNaN('true' / 'true') // true
若是参数类型不是NaN
,Number.isNaN
一概返回false
。
它们与传统的全局方法isFinite()
和isNaN()
的区别在于,传统方法先调用Number()
将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效
ES6 将全局方法parseInt()
和parseFloat()
,移植到Number
对象上面,行为彻底保持不变。
// ES5的写法 parseInt('12.34') // 12 parseFloat('123.45#') // 123.45 // ES6的写法 Number.parseInt('12.34') // 12 Number.parseFloat('123.45#') // 123.45
这样作的目的,是逐步减小全局性方法,使得语言逐步模块化。
参数变量是默认声明
的,因此不能用let
或const
再次声明。
function foo(x = 5) { let x = 1; // error const x = 2; // error }
使用参数默认值
时,函数不能有同名参数。
// 不报错 function foo(x, x, y) { // ... } // 报错--有默认值 function foo(x, x, y = 1) { // ... }
另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都从新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。
let x = 99; function foo(p = x + 1) { console.log(p); } foo() // 100 x = 100; foo() // 101
上面代码中,参数p
的默认值是x + 1
。这时,每次调用函数foo
,都会从新计算x + 1
,而不是默认p
等于 100。
function foo({x, y = 5} = {}) { console.log(x, y); } foo() // undefined 5
一般状况下,定义了默认值的参数,应该是函数的尾参数
。由于这样比较容易看出来,到底省略了哪些参数。若是非尾部的参数设置默认值,实际上这个参数是无法省略的。
function f(x = 1, y) { return [x, y]; } f(, 1) // 报错 f(undefined, 1) // [1, 1] // 例二 function f(x, y = 5, z) { return [x, y, z]; } f(1, ,2) // 报错 f(1, undefined, null) // [1, 5, null]
注:若是传入undefined
,将触发该参数等于默认值,null
则没有这个效果,至关于赋值为null
一旦设置了参数的默认值
,函数进行声明初始化时,参数会造成一个单独的做用域(context)
。等到初始化结束,这个做用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的
。
let x = 1; function f(y = x) { let x = 2; console.log(y); } f() // 1
上面代码中,函数f
调用时,参数y = x
造成一个单独的做用域。这个做用域里面,变量x
自己没有定义,因此指向外层的全局变量x
。函数调用时,函数体内部的局部变量x
影响不到默认值变量x
。
若是此时,全局变量x
不存在(没有let x=1),就会报错。
下面这样写,也会报错。
var x = 1; function foo(x = x) { // ... } foo() // ReferenceError: x is not defined
上面代码中,参数x = x
造成一个单独做用域。实际执行的是let x = x
,因为暂时性死区的缘由,这行代码会报错”x 未定义“。
下面是一个更复杂的例子。
var x = 1; function foo(x, y = function() { x = 2; }) { var x = 3; y(); console.log(x); } foo() // 3 x // 1
上面代码中,函数foo
的参数造成一个单独做用域。这个做用域里面,首先声明了变量x
,而后声明了变量y
,y
的默认值是一个匿名函数。这个匿名函数内部的变量x指向同一个做用域的第一个参数x
,。函数foo
内部又声明了一个内部变量x
,该变量与第一个参数x
因为不是同一个做用域,因此不是同一个变量
,所以执行y
后,内部变量x
和外部全局变量x
的值都没变。
若是将var x = 3
的var
去除,函数foo
的内部变量x就指向第一个参数x
,与匿名函数内部的x
是一致的,因此最后输出的就是2
,而外层的全局变量x
依然不受影响。
var x = 1; function foo(x, y = function() { x = 2; }) { x = 3; y(); console.log(x); } foo() // 2 x // 1
ES6 容许使用“箭头”(=>
)定义函数。
var f = () => 5; // 等同于 var f = function () { return 5 };
因为大括号被解释为代码块,因此若是箭头函数直接返回一个对象
,必须在对象外面加上括号
,不然会报错。
// 报错 let getTempItem = id => { id: id, name: "Temp" }; // 不报错 let getTempItem = id => ({ id: id, name: "Temp" });
使用注意点
箭头函数有几个使用注意点。
(1)函数体内的this
对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不能够看成构造函数,也就是说,不可使用new
命令,不然会抛出一个错误。
(3)不可使用arguments
对象,该对象在函数体内不存在。若是要用,能够用 rest 参数代替。
(4)不可使用yield
命令,所以箭头函数不能用做 Generator 函数。
解释:什么不能用做构造函数?
new操做的主要步骤:
所以不能使用call或者apply将this指向新建立的对象
)尾调用(Tail Call)是函数式编程的一个重要概念,自己很是简单,一句话就能说清楚,就是指某个函数的最后一步是调用另外一个函数。
咱们知道,函数调用会在内存造成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息,尾调用因为是函数的最后一步操做,因此不须要保留外层函数的调用帧。
尾调用必定是 return +方法名()
function f() { let m = 1; let n = 2; return g(m + n); } f(); // 等同于 function f() { return g(3); } f(); // 等同于 g(3);
注意,只有再也不用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,不然就没法进行“尾调用优化”。
function addOne(a){ var one = 1; function inner(b){ return b + one; } return inner(a); }
上面的函数不会进行尾调用优化,由于内层函数inner
用到了外层函数addOne
的内部变量one
。
注意,目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持。
函数调用自身,称为递归。若是尾调用自身,就称为尾递归。
递归很是耗费内存,由于须要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来讲,因为只存在一个调用帧,因此永远不会发生“栈溢出”错误。
function factorial(n) { if (n === 1) return 1; return n * factorial(n - 1); } factorial(5) // 120
上面代码是一个阶乘函数,计算n
的阶乘,最多须要保存n
个调用记录,复杂度 O(n) 。
若是改写成尾递归,只保留一个调用记录,复杂度 O(1) 。
function factorial(n, total) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5, 1) // 120
JavaScript 语言的try...catch
结构,之前明确要求catch
命令后面必须跟参数,接受try
代码块抛出的错误对象。
try { // ... } catch (err) { // 处理错误 }
上面代码中,catch
命令后面带有参数err
。
不少时候,catch
代码块可能用不到这个参数。可是,为了保证语法正确,仍是必须写。ES2019 作出了改变,容许catch
语句省略参数。
try { // ... } catch { // ... }
扩展运算符(spread)是三个点(...
)。它比如 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
console.log(...[1, 2, 3]) // 1 2 3
扩展运算符后面还能够放置表达式。
const arr = [ ...(x > 0 ? ['a'] : []), 'b', ];
复制或者合并数组
这些方法都是浅拷贝,使用的时候须要注意。
//ES5 const a1 = [1, 2]; const a2 = a1.concat(); const a3 = a1.splice(); //ES6 const a1 = [1, 2]; const a2 = [...a1];
与解构赋值结合
// ES5 let list=[1,2,3,4,5,6] a = list[0], rest = list.slice(1) // ES6 [a, ...rest] = list //a=1 //rest=[]
字符串/对象/数组
扩展运算符还能够将字符串转为真正的数组。
[...'hello'] // [ "h", "e", "l", "l", "o" ] let obj={a: 1, b: 2} {...obj} //{a: 1, b: 2} let arr=[1,2,3,4] [...arr] //[1,2,3,4]
实现了 Iterator 接口的对象(Map、Set、Generator函数)
任何定义了遍历器(Iterator)接口的对象(参阅 Iterator 一章),均可以用扩展运算符转为真正的数组。
let nodeList = document.querySelectorAll('div'); let array = [...nodeList];
上面代码中,querySelectorAll
方法返回的是一个NodeList
对象。它不是数组,而是一个相似数组的对象(相似数组对象必须有length
属性)。
Array.from的语法Array.from(arr).map(fn(),this)
Array.from(arr,()=>{})
第一个参数arr要转换的值(必须
),第二个参数是map方法,若是第二个参数不是箭头函数
用.的方式写,可接受第三个参数,修改map方法里面的this指向,若是第二个参数是箭头函数
用,方式写第二个参数,箭头函数没有本身的this,因此不须要第三个参数
Array.from
方法用于将两类对象转为真正的数组:相似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
下面是一个相似数组的对象,Array.from
将它转为真正的数组。
常见的相似数组的对象是 DOM 操做返回的 NodeList 集合,以及函数内部的arguments
对象
let arrayLike = { '0': 'a', '1': 'b', '2': 'c', length: 3 }; // ES5的写法 var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c'] // ES6的写法 let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
只要是部署了 Iterator 接口的数据结构,Array.from
都能将其转为数组。
Array.from('hello') // ['h', 'e', 'l', 'l', 'o'] let namesSet = new Set(['a', 'b']) Array.from(namesSet) // ['a', 'b']
值得提醒的是,扩展运算符(...
)也能够将某些数据结构转为数组。
// arguments对象 function foo() { const args = [...arguments]; } // NodeList对象 [...document.querySelectorAll('div')]
扩展运算符背后调用的是遍历器接口(Symbol.iterator
),若是一个对象没有部署这个接口,就没法转换。Array.from
方法还支持相似数组的对象。所谓相似数组的对象,本质特征只有一点,即必须有length
属性。所以,任何有length
属性的对象,均可以经过Array.from
方法转为数组,而此时扩展运算符就没法转换。
Array.from
还能够接受第二个参数,做用相似于数组的map
方法,用来对每一个元素进行处理,将处理后的值放入返回的数组。
Array.from(arrayLike, x => x * x); // 等同于 Array.from(arrayLike).map(x => x * x); Array.from([1, 2, 3], (x) => x * x) // [1, 4, 9]
Array.of
方法用于将一组值,转换为数组。这个方法的主要目的,是弥补数组构造函数Array()
的不足。由于参数个数的不一样,会致使Array()
的行为有差别。Array()在只有一个参数的时候表示的是长度。
Array.of() //[] Array.of(3, 11, 8) // [3,11,8] Array.of(3) // [3] Array(3) // [, , ,] Array.of(3).length // 1
数组实例的copyWithin()
方法,在当前数组内部,将指定位置的成员复制到其余位置(会覆盖原有成员),而后返回当前数组。也就是说,使用这个方法,会修改当前数组
。
Array.prototype.copyWithin(target, start = 0, end = this.length)
它接受三个参数。
若是为负值,表示倒数
。不包括
),默认等于数组长度。若是为负值,表示从末尾开始计算。这三个参数都应该是数值,若是不是,会自动转为数值。
let arr=[1, 2, 3, 4, 5] arr.copyWithin(0, 2,4) //从下标为2的开始复制即从3开始复制,到下标为4的结束即到5结束(不包括5) //[3, 4, 3, 4, 5]
find()语法
语法一:target.find(fn,this)
语法二:target.find((val,index,arr)=>{})
,fn为箭头函数
数组实例的find
方法,用于找出第一个符合条件的数组成员。直到找出第一个返回值为true
的成员,而后返回该成员
。若是没有符合条件的成员,则返回undefined
。
[1, 4, -5, 10].find((n) => n < 0) // -5
findIndex()、
数组实例的findIndex
方法的用法与find
方法很是相似,返回第一个符合条件的数组成员的位置
,若是全部成员都不符合条件,则返回-1
[1, 5, 10, 15].findIndex(function(value, index, arr) { return value > 9; }) // 2
fill(target,start,end)
fill
方法使用给定值,填充一个数组。
注意,若是填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。
['a', 'b', 'c'].fill(7, 1, 2) // ['a', 7, 'c'] let arr = new Array(3).fill({name: "Mike"}); arr[0].name = "Ben"; arr // [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
能够用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"
语法:includes(target,start),第二个参数表示开始搜索的位置includes()也能够用于字符串
Array.prototype.includes
方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes
方法相似。ES2016 引入了该方法。
[1, 2, 3].includes(2) // true [1, 2, 3].includes(4) // false
flat()语法
Infinity
关键字做为参数数组的成员有时仍是数组,Array.prototype.flat()
用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组
,对原数据没有影响。
若是原数组有空位,flat()
方法会跳过空位。
[1, 2, , [4, 5]].flat() // [1, 2, 4, 5]
flatMap()语法
flatMap()
方法对原数组的每一个成员执行一个函数,而后对返回值组成的数组执行flat()
方法。该方法返回一个新数组
,不改变原数组。flatMap()
只能展开一层数组。
// 至关于 [[2, 4], [3, 6], [4, 8]].flat() [2, 3, 4].flatMap((x) => [x, x * 2]) // [2, 4, 3, 6, 4, 8]
ES6 容许在大括号里面,直接写入变量和函数,做为对象的属性和方法。
const foo = 'bar'; const baz = {foo}; baz // {foo: "bar"} // 等同于 const baz = {foo: foo};
变量foo
直接写在大括号里面。这时,属性名就是变量名, 属性值就是变量值
。
除了属性简写,方法也能够简写(对象的方法
)。
const o = { method() { return "Hello!"; } }; // 等同于 const o = { method: function() { return "Hello!"; } };
CommonJS 模块输出一组变量,就很是合适使用简洁写法。
function a(){} function b(){} module.exports = { a, b };
注意,简写的对象方法不能用做构造函数,会报错。
const obj = { f() { this.foo = 'bar'; } }; new obj.f() // 报错
只有function这种写法的能够做为构造函数,非function写法的属于简写,没有构造器的方法
JavaScript 定义对象的属性,有两种方法。
// 方法一 obj.foo = true; // 方法二 obj['a' + 'bc'] = 123;
上面代码的方法一是直接用标识符做为属性名,方法二是用表达式做为属性名,这时要将表达式放在方括号以内。
属性的可枚举性和遍历
ES6 一共有 5 种方法能够遍历对象的属性。
(1)for...in
for...in
循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
(2)Object.keys(obj)
Object.keys
返回一个数组,包括对象自身的(不含继承的)全部可枚举属性(不含 Symbol 属性)的键名。
(3)Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames
返回一个数组,包含对象自身的全部属性(不含 Symbol 属性,可是包括不可枚举属性)的键名。
(4)Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols
返回一个数组,包含对象自身的全部 Symbol 属性的键名。
(5)Reflect.ownKeys(obj)
Reflect.ownKeys
返回一个数组,包含对象自身的(不含继承的)全部键名,无论键名是 Symbol 或字符串,也不论是否可枚举。
咱们知道,this
关键字老是指向函数所在的当前对象,ES6 又新增了另外一个相似的关键字super
,指向当前对象的原型对象(不改变this的指向
)。
const proto = { foo: 'hello' }; const obj = { foo: 'world', find() { return super.foo; } }; Object.setPrototypeOf(obj, proto); obj.find() // "hello"
上面代码中,对象obj.find()
方法之中,经过super.foo
引用了原型对象proto
的foo
属性。
注意,super
关键字表示原型对象时,只能用在对象的方法之中,用在其余地方都会报错。
// 报错 const obj = { foo: super.foo } // 报错 const obj = { foo: () => super.foo } // 报错 const obj = { foo: function () { return super.foo } }
上面三种super
的用法都会报错,由于对于 JavaScript 引擎来讲,这里的super
都没有用在对象的方法之中。第一种写法是super
用在属性里面,第二种和第三种写法是super
用在一个函数里面,而后赋值给foo
属性。目前,只有对象方法的简写法可让 JavaScript 引擎确认,定义的是对象的方法。
const proto = { x: 'hello', foo() { console.log(this.x); }, }; const obj = { x: 'world', foo() { super.foo(); } } Object.setPrototypeOf(obj, proto); obj.foo() // "world"
上面代码中,super.foo
指向原型对象proto
的foo
方法,可是绑定的this
却仍是当前对象obj
,所以输出的就是world
。
解构赋值
注意,解构赋值的拷贝是浅拷贝,即若是一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。
let obj = { a: { b: 1 } }; let { ...x } = obj; obj.a.b = 2; x.a.b // 2
另外,扩展运算符的解构赋值,不能复制继承自原型对象的属性
。
let o1 = { a: 1 }; let o2 = { b: 2 }; o2.__proto__ = o1; let { ...o3 } = o2; o3 // { b: 2 } o3.a // undefined
上面代码中,对象o3
复制了o2
,可是只复制了o2
自身的属性,没有复制它的原型对象o1
的属性。
对象的扩展运算符(...
)用于取出参数对象的全部可遍历属性,拷贝到当前对象之中。
let z = { a: 3, b: 4 }; let n = { ...z }; n // { a: 3, b: 4 }
因为数组是特殊的对象,因此对象的扩展运算符也能够用于数组。
let foo = { ...['a', 'b', 'c'] }; foo // {0: "a", 1: "b", 2: "c"}
对象的扩展运算符等同于使用Object.assign()
方法。
let a=['a','b','c'] let aClone = { ...a }; // 等同于 let aClone = Object.assign({}, a); //{0: "a", 1: "b", 2: "c"}
上面的例子只是拷贝了对象实例的属性,若是想完整克隆一个对象,还拷贝对象原型的属性,能够采用下面的写法。
// 写法一 const clone1 = { __proto__: Object.getPrototypeOf(obj), ...obj }; // 写法二 const clone2 = Object.assign( Object.create(Object.getPrototypeOf(obj)), obj ); // 写法三 const clone3 = Object.create( Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj) )
上面代码中,写法一的__proto__
属性在非浏览器的环境不必定部署,所以推荐使用写法二和写法三。
编程实务中,若是读取对象内部的某个属性,每每须要判断一下该对象是否存在。或者使用三元运算符?:
,判断一个对象是否存在。
const firstName = (message && message.body && message.body.user && message.body.user.firstName) || 'default'; //链式 const firstName = message?.body?.user?.firstName || 'default'; //三目运算 const fooInput = myForm.querySelector('input[name=foo]') const fooValue = fooInput ? fooInput.value : undefined //链式 const fooValue = myForm.querySelector('input[name=foo]')?.value
这样的层层判断很是麻烦,所以 ES2020 引入了“链判断运算符”(optional chaining operator)?.
,简化上面的写法。
上面代码使用了?.
运算符,直接在链式调用的时候判断,左侧的对象是否为null
或undefined
。若是是的,就再也不往下运算,而是返回undefined
。
链判断运算符有三种用法。
obj?.prop
// 对象属性obj?.[expr]
// 同上func?.(...args)
// 函数或对象方法的调用下面是这个运算符常见的使用形式,以及不使用该运算符时的等价形式。
a?.b // 等同于 a == null ? undefined : a.b a?.[x] // 等同于 a == null ? undefined : a[x] a?.b() // 等同于 a == null ? undefined : a.b() a?.() // 等同于 a == null ? undefined : a()
上面代码中,特别注意后两种形式,若是a?.b()
里面的a.b
不是函数,不可调用,那么a?.b()
是会报错的。a?.()
也是如此,若是a
不是null
或undefined
,但也不是函数,那么a?.()
会报错。
读取对象属性的时候,若是某个属性的值是null
或undefined
,有时候须要为它们指定默认值。常见作法是经过||
运算符指定默认值。
const headerText = headerText || 'Hello, world!';
上面的代码都经过||
运算符指定默认值,可是这样写是错的。开发者的原意是,只要属性的值为null
或undefined
,默认值就会生效,可是属性的值若是为空字符串或false
或0
,默认值也会生效。
为了不这种状况,ES2020 引入了一个新的 Null 判断运算符??
。它的行为相似||
,可是只有运算符左侧的值为null
或undefined
时,才会返回右侧的值。
const headerText = headerText ?? 'Hello, world!';
这个运算符的一个目的,就是跟链判断运算符?.
配合使用,为null
或undefined
的值设置默认值。
const animationDuration = response.settings?.animationDuration ?? 300;
??
有一个运算优先级问题,它与&&
和||
的优先级孰高孰低。如今的规则是,若是多个逻辑运算符一块儿使用,必须用括号代表优先级,不然会报错。
(lhs && middle) ?? rhs;
ES5 比较两个值(字符串)是否相等,只有两个运算符:相等运算符(==
)和严格相等运算符(===
)。它们都有缺点,前者会自动转换数据类型,后者的NaN
不等于自身,以及+0
等于-0
。JavaScript 缺少一种运算,在全部环境中,只要两个值是同样的,它们就应该相等。
ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。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
基本用法
Object.assign
方法用于对象的合并,将源对象(source)的全部可枚举属性,复制到目标对象(target)。
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}
Object.assign
方法的第一个参数是目标对象,后面的参数都是源对象。
注意,若是目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
其余类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。可是,除了字符串会以数组形式,拷贝入目标对象,其余值都不会产生效果。
const v1 = 'abc'; const v2 = true; const v3 = 10; const obj = Object.assign({}, v1, v2, v3); console.log(obj); // { "0": "a", "1": "b", "2": "c" }
数组的处理Object.assign
能够用来处理数组,可是会把数组视为对象。
Object.assign([1, 2, 3], [4, 5]) // [4, 5, 3]
上面代码中,Object.assign
把数组视为属性名为 0、一、2 的对象,所以源数组的 0 号属性4
覆盖了目标数组的 0 号属性1
。
Object.keys()
ES5 引入了Object.keys
方法,返回一个数组,成员是参数对象自身的(不含继承的)全部可遍历(enumerable)属性的键名。
var obj = { foo: 'bar', baz: 42 }; Object.keys(obj) // ["foo", "baz"]
Object.values()Object.values
方法返回一个数组,成员是参数对象自身的(不含继承的)全部可遍历(enumerable)属性的键值。
const obj = { foo: 'bar', baz: 42 }; Object.values(obj) // ["bar", 42]
Object.entries()Object.entries()
方法返回一个数组,成员是参数对象自身的(不含继承的)全部可遍历(enumerable)属性的键值对数组。
const obj = { foo: 'bar', baz: 42 }; Object.entries(obj) // [ ["foo", "bar"], ["baz", 42] ]
Object.fromEntries()Object.fromEntries()
方法是Object.entries()
的逆操做,用于将一个键值对数组转为对象。
Object.fromEntries([ ['foo', 'bar'], ['baz', 42] ]) // { foo: "bar", baz: 42 }
该方法的主要目的,是将键值对的数据结构还原为对象,所以特别适合将 Map 结构转为对象。
// 例一 const entries = new Map([ ['foo', 'bar'], ['baz', 42] ]); Object.fromEntries(entries) // { foo: "bar", baz: 42 } // 例二 const map = new Map().set('foo', true).set('bar', false); Object.fromEntries(map) // { foo: true, bar: false }
ES2017 引入了跟Object.keys
配套的Object.values
和Object.entries
,做为遍历一个对象的补充手段,供for...of
循环使用。
let {keys, values, entries} = Object; let obj = { a: 1, b: 2, c: 3 }; for (let key of keys(obj)) { console.log(key); // 'a', 'b', 'c' } for (let value of values(obj)) { console.log(value); // 1, 2, 3 } for (let [key, value] of entries(obj)) { console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3] }