模板字符串
基本使用
在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 />被分割的字符串会将形成的数组传给函数的第一个参数,`${}`表达式的值会传给函数第二个参数,多个表达式就依次往后面的参数传。```javascriptfunction 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' ] zsfunction 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是赋值给了同为参数的xfunction foo(x, y = function() { x = 3; console.log(x) }) {console.log(x) // 参数 x 未赋值,为 undefinedvar x = 2 // 在函数内部作用域中声明了变量 x = 2y() // 执行参数函数,并打印了同为参数的 x = 3console.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 30console.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 constructorconst 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 }
注意:**展开运算符其实是一种浅拷贝**。如果要展开的数组或对象中有引用类型时,修改引用类型中的值要注意。浅拷贝就是复制的时候只会复制引用类型的内存地址,而不是真的在堆内存中再新建一个一模一样的复杂类型。所以改了复杂类型中的值,改动将会影响所有对该复杂类型的引用。```javascriptconst 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 -> binaryconst num2 = 0b100 // 二进制// o -> octonaryconst num3 = 0o100 // 八进制// x -> hexadecimalconst num4 = 0x100 // 十六进制console.log(num1, num2, num3, num4) // 100 4 64 256// 大的数值的连接符(ES2021 ES12)const num = 100_000_000console.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) // aaaconsole.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) // trueconsole.log(sa); // Symbol(aaa) 这个 key 也会成为这个 Symbol 的描述const key = Symbol.keyFor(sa)console.log(key) // aaaconst sc = Symbol.for(key)console.log(sa === sc) // true
