模板字符串
基本使用
在ES6之前,如果我们想要将字符串和一些动态的变量(标识符)拼接到一起,需要使用一堆的 + 号,非常不方便。
ES6允许我们使用字符串模板来嵌入JS的变量或者表达式来进行拼接:
- 使用``` 符号来包裹的字符串,称之为模板字符串;
- 在模板字符串中,可以通过
${expression}
来嵌入动态的内容 ```javascript // ES6之前拼接字符串和其他标识符 const name = “why” const age = 18 const height = 1.88
// console.log(“my name is “ + name + “, age is “ + age + “, height is “ + height)
// ES6提供模板字符串 `
const message =
my name is ${name}, age is ${age}, height is ${height}` // 简单取值
console.log(message)
const info = age double is ${age * 2}
// 取值并计算
console.log(info)
function doubleAge() { return age * 2 }
const info2 = double age is ${doubleAge()}
// 调用函数
console.log(info2)
<a name="ZS1tl"></a>
## 标签模板字符串使用
模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals)。<br />````可以调用函数,并且可以把模板字符串以**数组**的形式当参数传入函数中,如果模板字符串中有`${}`表达式,则会将字符串**分割成几份**。<br />被分割的字符串会将形成的数组传给函数的第一个参数,`${}`表达式的值会传给函数第二个参数,多个表达式就依次往后面的参数传。
```javascript
function foo1(m) {
console.log(m);
}
foo1`123456` // [ '123456' ]
function foo2(m, n) {
console.log(m, n);
}
const name = 'zs'
foo2`123${name}456` // [ '123', '456' ] zs
function foo3(m, n, t) {
console.log(m, n, t);
}
const name1 = 'zs'
const name2 = 'ls'
foo3`123${name}456${name2}789` // [ '123', '456', '789' ] zs ls
我们一般不会主动编写这样的代码,但是有些框架内部实现就会用到标签目标字符串,比如 react。
函数的默认参数
之前设置函数参数的默认值我们会使用异或写法,但是这种写法有两个缺点,繁琐和逻辑考虑不全产生bug。
function foo(m, n) {
m = m || "aaa"
n = n || "bbb"
console.log(m, n)
}
// 0 和 null 布尔运算都是 false,导致无法做参数
foo(0, null) // aaa bbb
ES6 可以让默认值直接赋值写在参数上,并且还可以和解构连用,并且有两种写法。
// 1.ES6可以给函数参数提供默认值
function foo(m = "aaa", n = "bbb") {
console.log(m, n)
}
foo(0, "") // 0
// 2.对象参数和默认值以及解构,默认要解构的对象
function printInfo({name, age} = {name: "why", age: 18}) {
console.log(name, age)
}
printInfo({name: "kobe", age: 40})
// 另外一种写法:不默认对象,直接默认解构后的参数值
function printInfo1({name = "why", age = 18} = {}) {
console.log(name, age)
}
printInfo1()
有多个参数情况下,规范要求把有默认值的参数写到最后面,这样当想要使用参数默认值的时候就可以省略写最后一个参数,调用更方便。
另外,foo.length
属性表示函数参数的个数。但是参数定义默认值后,该参数及其之后的参数将不会被统计到 length 属性中。
// 3.有默认值的形参最好放到最后
function bar(x, y, z = 30) {
console.log(x, y, z)
}
bar(10, 20) // 使用参数默认值,调用时可省略写最后的参数
bar(undefined, 10, 20) // 假设 z 定义在第一个位置,则不能省略
// 4.有参数默认值的函数 length 属性
function baz(x, y, z, m = 20, n) {
console.log(x, y, z, m, n)
}
console.log(baz.length) // 3,后面两个不统计
参数作用域
一般来讲函数只有一个作用域,参数和函数内部都属于同一个作用域。但是如果参数设置了默认值,则函数作用域会被更加细分参数作用域和函数内部作用域。
var x = 0
// 当函数的参数有默认值时, 会形成一个新的作用域, 这个作用域用于保存参数的值
// 这个x=3是赋值给了同为参数的x
function foo(x, y = function() { x = 3; console.log(x) }) {
console.log(x) // 参数 x 未赋值,为 undefined
var x = 2 // 在函数内部作用域中声明了变量 x = 2
y() // 执行参数函数,并打印了同为参数的 x = 3
console.log(x)
}
foo()
console.log(x) // 全局的 x 并未更改,依然为 0
// undefined
// 3
// 2
// 0
函数的剩余参数
ES6中引用了rest parameter,可以将不定数量的参数放入到一个数组中:如果最后一个参数是...
为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组;
那么剩余参数和 arguments 有什么区别呢?
- 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参;
- arguments 对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作;
arguments 是早期的 ECMAScript 中为了方便去获取所有的参数提供的一个数据结构,而 rest 参数是ES6中提供并且希望以此来替代 arguments 的;
注意:剩余参数必须放到最后一个位置,否则会报错。
function foo(m, n, ...args) {
console.log(m, n) // 20 30
console.log(args) // [ 40, 50, 60 ]
console.log(arguments) // [Arguments] { '0': 20, '1': 30, '2': 40, '3': 50, '4': 60 }
}
foo(20, 30, 40, 50, 60)
function fn(...args, n) {} // Rest parameter must be last formal parameter
箭头函数补充
前面讲了箭头函数没有 this 和 argument,它也没有显式原型。所以不能当构造函数来 new,因为没办法完成 new 关键的第二步,将函数显式原型赋值给空对象的隐式原型。
var bar = () => { }
console.log(bar.prototype) // undefined
// bar is not a constructor
const b = new bar()
展开语法
展开语法也是...
和剩余参数需要注意区分,剩余参数是在定义参数时来存值的,展示语法是来取值的。
展开语法(Spread syntax):
- 可以在函数调用/数组构造时,将 数组表达式 或者 string 在语法层面展开,然后取值。
- 数组是按索引顺序展开取值
- 字符串是按字符一个一个展开
- 还可以在构造字面量对象时, 将对象表达式按 key-value 的方式展开;
展开语法的场景:
- 在函数调用时使用;
- 在数组构造时使用;
- 在构建对象字面量时,也可以使用展开运算符,这个是在ES2018(ES9)中添加的新特性; ```javascript const arr = [“abc”, “cba”, “nba”] const str = “hi”
const info = {name: “zs”, age: 18}
// 函数调用时使用 function foo(…args) { console.log(args); }
// 1. 函数调用时,展开取值放入剩余参数数组中 foo(…arr) // [ ‘abc’, ‘cba’, ‘nba’ ] foo(…str) // [ ‘h’, ‘i’ ]
// 2. 构造数组时,展开取值初始化数组 const newarr = […arr, …str] console.log(newarr) // [ ‘abc’, ‘cba’, ‘nba’, ‘h’, ‘i’]
// 3. 展开取值放入字面量对象 const obj1 = {…info} const obj2 = {…info, …arr} const obj3 = {…info, …arr, …str} console.log(obj1); // 对象按 key-value 展开 console.log(obj2); // 数组按索引当属性初始化到对象中 console.log(obj3); // 字符串也是按索引,因为数组也是按索引,所以会互相覆盖属性值
// { name: ‘zs’, age: 18 } // { ‘0’: ‘abc’, ‘1’: ‘cba’, ‘2’: ‘nba’, name: ‘zs’, age: 18 } // { ‘0’: ‘h’, ‘1’: ‘i’, ‘2’: ‘nba’, name: ‘zs’, age: 18 }
注意:**展开运算符其实是一种浅拷贝**。如果要展开的数组或对象中有引用类型时,修改引用类型中的值要注意。
浅拷贝就是复制的时候只会复制引用类型的内存地址,而不是真的在堆内存中再新建一个一模一样的复杂类型。所以改了复杂类型中的值,改动将会影响所有对该复杂类型的引用。
```javascript
const info = {
name: "why",
friend: { name: "kobe" }
}
const obj = { ...info, name: "coderwhy" }
console.log(obj) // { name: 'coderwhy', friend: { name: 'kobe' } }
// 修改引用类型 friend 中 name 的值
obj.friend.name = "james"
// info 对象的值也被改了
console.log(info.friend.name) // james
数值的表示
在ES6中规范了二进制和八进制的写法,另外在ES2021新增特性:数字过长时,可以使用_作为连接符
const num1 = 100 // 十进制
// b -> binary
const num2 = 0b100 // 二进制
// o -> octonary
const num3 = 0o100 // 八进制
// x -> hexadecimal
const num4 = 0x100 // 十六进制
console.log(num1, num2, num3, num4) // 100 4 64 256
// 大的数值的连接符(ES2021 ES12)
const num = 100_000_000
console.log(num) // 100000000
Symbol 的基本使用
Symbol是什么呢?Symbol是 ES6 中新增的一个基本数据类型,翻译为符号。可当函数一样使用。
那么为什么需要 Symbol 呢?
在ES6之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突;
- 比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性;
- 比如开发中我们使用混入,那么混入中出现了同名的属性,必然有一个会被覆盖掉;
Symbol 就是为了解决上面的问题,用来生成一个独一无二的值。
Symbol 值是通过Symbol 函数来生成的,生成后可以作为属性名;也就是在ES6中,对象的属性名可以使用字符串,也可以使用 Symbol 值;
Symbol 函数执行后每次创建出来的值都是独一无二。
// 1.ES6中Symbol的基本使用
const s1 = Symbol()
const s2 = Symbol()
console.log(s1, s2) // Symbol() Symbol()
console.log(s1 === s2) // false
// ES2019(ES10)中, 还可以给 Symbol 添加一个描述(description)
// 因为表明看Symbol都是一样,这个描述可以做个区分
const s3 = Symbol("aaa")
console.log(s3.description) // aaa
console.log(s3); // Symbol(aaa)
对象中定义了 Symbol 属性,通过 遍历 / Object.keys 等,是无法获取这些属性的。
Object.getOwnPropertySymbols(对象)
需要使用Object.getOwnPropertySymbols(对象)
,它将会返回一个Symbol 属性组成的数组。
当然Object.getOwnPropertyDescriptors()
获取所有属性描述符的方式也能遍历出来Symbol。
// 2.Symbol值作为属性,三种添加属性的方式都适用
// 2.1.在定义对象字面量时使用
const key1 = Symbol()
const obj = {
[key1]: "abc",
[Symbol('key2')]: 666 // 不要这样添加 Symbol 值属性,因为每次执行结果都不一样
}
console.log(obj[Symbol('key2')]); // undefined,找不到原来的属性
// 2.2.新增属性
const key3 = Symbol()
obj[key3] = "nba"
// 2.3.Object.defineProperty方式
const key4 = Symbol('key4')
Object.defineProperty(obj, key4, {
enumerable: true,
configurable: true,
writable: true,
value: "mba"
})
// 读取 Symbol 值的属性
console.log(obj[key1])
// 注意: 不能通过.语法获取,.语法只会去找字符串属性,[]才有计算表达式的能力
console.log(obj.key1) // undefined
// 3.使用Symbol作为key的属性名,通过 遍历 / Object.keys 等,是无法获取这些属性的
console.log(Object.keys(obj)) // [ ] 一个空数组,好像没有属性
console.log(Object.getOwnPropertyNames(obj)) // [ ]
// 需要 Object.getOwnPropertySymbols 来获取所有Symbol 属性
console.log(Object.getOwnPropertySymbols(obj))
// 遍历对象中 Symbol 属性的属性值
const sKeys = Object.getOwnPropertySymbols(obj)
for (const sKey of sKeys) {
console.log(obj[sKey])
}
相同值的Symbol
如果我们现在就是想创建相同的 Symbol ,我们可以使用Symbol.for(key)
方法,并且这个 key 也会成为这个 Symbol 的描述。
同时我们也可以通过Symbol.keyFor(Symbol值)
方法来获取对应的 key。
const sa = Symbol.for("aaa")
const sb = Symbol.for("aaa")
console.log(sa === sb) // true
console.log(sa); // Symbol(aaa) 这个 key 也会成为这个 Symbol 的描述
const key = Symbol.keyFor(sa)
console.log(key) // aaa
const sc = Symbol.for(key)
console.log(sa === sc) // true