三、模板字符串/标签模块字符串/ES6中函数及箭头函数相关内容补充/展开运算符/深拷贝,浅拷贝的理解

2022/8/4 ES6模板字符串标签模板字符串函数的默认参数函数的剩余参数箭头函数的补充展开语法展开运算符展开运算符的浅拷贝引用赋值深拷贝/浅拷贝数组表示方式

# 1、模板字符串和标签模块字符串

# 1.1.模板字符串的基本使用

  • 在ES6之前,如果我们想要将字符串和一些动态的变量(标识符)拼接到一起,是非常麻烦和丑陋的(ugly)。

  • ES6允许我们使用字符串模板来嵌入JS的变量或者表达式来进行拼接:

    • 首先,我们会使用 `` 符号(反引号)来编写字符串,称之为模板字符串;
    • 其次,在模板字符串中,我们可以通过 ${expression} 来嵌入动态的内容;
    //1、ES6之前拼接字符串和其他标识符
    const name = "why"
    const age = 18
    const height = 1.88
    console.log("my name is " + name + ", age is " + age + ", height is " + height)
    //2、ES6提供模板字符串 `` (反引号)
    //注:``这个单引号必须是tab键上面的那个键创建出来的
    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)//age double is 36
    
    function doubleAge() {
     return age * 2
    }
    
    const info2 = `double age is ${doubleAge()}`
    console.log(info2)//double age is 36
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

# 1.2.新增的调用函数的方式:标签模块字符串

  • 模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals)。

  • 我们一起来看一个普通的JavaScript的函数:

    function foo(...args) {
      console.log(args)
    }
    foo('Hello World')//[ 'Hello World' ]
    
    1
    2
    3
    4
  • 如果我们使用标签模板字符串,并且在调用的时候插入其他的变量:

    • 模板字符串被拆分了;
    • 第一个元素是数组,是被模块字符串拆分的字符串组合;
    • 后面的元素是一个个模块字符串传入的内容;
    function foo(...args) {
    console.log(...args)
    }
    // 1.函数正常调用方式
    foo("Hello", "World")//Hello World
    
    // 2.另外调用函数的方式: 标签模块字符串
    // foo``
    
    // 2.1.如:foo`Hello World` --->  相当于foo(['Hello World']) 
    foo`Hello World`; //[ 'Hello World' ]
    
    // 2.2.如:foo`Hello${name}Wo${age}rld` ---> 相当于foo(['Hello','Wo','rld'],name,age)
    const name = "why"
    const age = 18
    foo`Hello${name}Wo${age}rld` //[ 'Hello', 'Wo', 'rld' ] why 18
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

# 1.3.应用场景:React的styled-components库

veg1d1.png

# 2、ES6中函数及箭头函数相关内容补充

# 2.1.函数的默认参数

  • 在ES6之前,我们编写的函数参数是没有默认值的,所以我们在编写函数时,如果有下面的需求:

    • 传入了参数,那么使用传入的参数;
    • 没有传入参数,那么使用一个默认值;
  • 而在ES6中,我们允许给函数一个默认值:

// ES5以及之前给参数默认值
/**
    * 缺点:
    *  1.写起来很麻烦, 并且代码的阅读性是比较差
    *  2.这种写法是有bug
    */
function foo1(m, n) {
  m = m || "aaa"//如果这里m传进来的值是:0,null,undefined,'',NaN,因为他们转换成布尔类型都为false;所以m最后拿到的值都会是aaa,
  n = n || "bbb"
  console.log(m, n)
}

foo1(0, 666)  //aaa 666  这里我明明传了0,却给我打印aaa,我想拿到0却拿不到(则在业务中写这种代码可能会出现bug)

// 1.ES6可以给函数参数提供默认值
function foo(m = "aaa", n = "bbb") {
  console.log(m, n)
}
foo()  //aaa bbb
foo(NaN, "")  //NaN ''

// 2.当传进来的参数为对象时,函数参数默认值的写法(需要用到解构)
function printInfo({ name, age } = { name: "lyk", age: 24 }, m) {//这里name,age默认值分别是:lyk,24
  console.log(name, age, m)//kobe 40
}
printInfo({ name: "kobe", age: 40 })//kobe 40 undefined
printInfo(1, 2)//undefined undefined 2 --> { name, age }= 1  所以name和age是undefined

// 另外一种写法(推荐用这种写法)
function printInfo1({ name = "lyk", age = 24 } = {}, m) {
  console.log(name, age, m)//lyk 24
}
printInfo1()//lyk 24 undefined
printInfo1(1, 2)//lyk 24 2 --> { name = "lyk", age = 24 } = 1  所以name是lyk,age是24

// 3.有默认值的形参最好放到最后
function bar(x, y, z = 30) {
  console.log(x, y, z)
}
bar(10, 20) //10 20 30
bar(undefined, 10, 20)  //undefined 10 20

// 4.有默认值的函数的length属性
function baz(x, y, z, m, n = 30, a) {
  console.log(x, y, z, m, n, a)//1,2,3,4,5,6
  console.log(arguments)
  //Arguments(6) [1, 2, 3, 4, 5, 6, callee: (...), Symbol(Symbol.iterator): ƒ]
}
baz(1, 2, 3, 4, 5, 6)
console.log(baz.length)//4  
//函数参数设置了默认值会影响函数的length的大小,包含默认值本身及后面的参数都不算进去,所以上面baz函数的length为4(之前有讲过)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

# 2.2.函数的剩余参数

  • ES6中引用了rest parameter,可以将不定数量的参数放入到一个数组中:
    • 如果最后一个参数是 ... 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组;
function foo(m,n,...args) {
  console.log(m,n)//1 2
  console.log(args)//[3,4,5,6,7,8,9]
}
foo(1,2,3,4,5,6,7,8,9)
1
2
3
4
5
  • 那么剩余参数和arguments有什么区别呢?

    • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参;
    • arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作;
    • arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6中提供并且希望以此来替代arguments的;
  • 剩余参数必须放到最后一个位置,否则会报错。

// function foo1(...args, m, n) {//...args只能放在最后一个参数,不能写在前面
//   console.log(m, n)
//   console.log(args)
//   console.log(arguments)
// }
// foo1(20, 30, 40, 50, 60)
// Rest参数必须是最后一个形式参数 (上面函数foo1执行会报如下1的错误)
// 1、Rest parameter must be last formal parameter

function foo(m, n = m + 1) {
  console.log(m, n)
}
foo(10);//10 11

var arr = [1,2,3,4,5,6]
var item
var lyk
[item,...lyk] = arr
console.log(...lyk)// 2 3 4 5 6
console.log(lyk)//[2, 3, 4, 5, 6 ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 2.3.函数箭头函数的补充

  • 在前面我们已经学习了箭头函数的用法,这里进行一些补充:
    • 箭头函数是没有显式原型prototype的,所以不能作为构造函数,使用new来创建对象;
    • 详解:箭头函数没有prototype,所以箭头函数不能new一个实例对象(因为new一个实例对象时会有p.__proto__ = Person.prototype操作)
    • 之前内容补充:箭头函数也没有this和arguments,会去上层作用域找
var bar = () => {//箭头函数没有prototype,所以箭头函数不能new一个实例对象 也没有this跟arguments,这两个参数都会去上层作用域找
  console.log(this)//window
  console.log(arguments)//arguments is not defined(报错)
}
console.log(bar.prototype)//undefined
const b = new bar()// bar is not a constructor(报错)
1
2
3
4
5
6

# 3、展开语法 - 展开运算符是浅拷贝

# 3.1.展开语法的使用

  • 展开运算符:...

  • 展开语法(Spread syntax):

    • 可以在函数调用/数组构造时,将数组表达式或者string在语法层面展开;
    • 还可以在构造字面量对象时, 将对象表达式按key-value的方式展开;
  • 展开语法的场景:

    • 在函数调用时使用;
    • 在数组构造时使用;
    • 在构建对象字面量时,也可以使用展开运算符,这个是在ES2018(ES9)中添加的新特性;
  • 注意:展开运算符其实是一种浅拷贝;

const arr = ["abc", "cba", "nba"]
const str1 = "lyk1"
const obj = { name: "lyk", age: 24 }

// 1.函数调用时:用来接收剩余参数
function foo(x, y, z, ...args) {
  console.log(x, y, z, args)
}
// 1.1.ES6之前的方式借用apply方法
foo.apply(null, arr)//abc cba nba []  

// 1.2.ES6中直接用展开语法方式
foo(...arr)//abc cba nba []  
foo(...str1) //l y k ['1']

// 1.3.foo(...obj) --> 不可以这样来使用
// foo(...obj) // 在函数的调用时, 用展开运算符, 将对应的展开数据, 进行迭代 ; 而对象不是可迭代的
// 可迭代对象: 数组/string/arguments


// 2.构造数组时
const newarr = [...arr, ...str1]
console.log(newarr)//['abc', 'cba', 'nba', 'l', 'y', 'k', '1']


// 3.构建对象字面量时ES2018(ES9)
const info = {
  ...obj,
  address: "广州市",
  ...arr,
  str1
}
console.log(info)  //{0: 'abc', 1: 'cba', 2: 'nba', name: 'lyk1', age: 24, address: '广州市', str1:'lyk1'}
console.log(info['0'])  // abc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 3.2.展开语法进行的浅拷贝

const info = {
  name: "lyk",
  friend: { name: "kobe" }
}

console.log({...info})  //{ name: 'lyk', friend: { name: 'kobe' } }
const obj = { ...info, name: "curry" }  //展开运算符
console.log(obj)  //{ name: 'curry', friend: { name: 'kobe' } }
obj.friend.name = "james"  //1操作:间接改变了info对象里面的friend对象的name属性(浅拷贝)

console.log(obj)  //{ name: 'curry', friend: { name: 'james' } }
console.log(info)  //{ name: 'lyk', friend: { name: 'james' } }
console.log(info.friend.name)  //james 上面 1操作 间接改变了info对象里面的friend对象的name属性(浅拷贝)
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 深入理解深拷贝,浅拷贝,具体看下面4;带你从内存中看就好理解了

# 4、深拷贝/浅拷贝和引用赋值的深入理解及在内存中的表现

# 4.1.引用赋值

  • 对象使用的是引用赋值。当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在堆中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
  • 下面我们具体来看一个例子来进行理解:
const obj = {
  name: 'kobe',
  friend: {
    name: 'james'
  }
}

let info = obj //发生了引用赋值
obj.name = 'lyk'
console.log(info.name)//lyk
info.friend.name = 'rose'
console.log(obj.friend.name)//rose
info = {}//新的引用赋值给了info;不在指向obj对象的引用
console.log(obj)//{ name: 'lyk', friend: { name: 'rose' } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

vm9B0H.png

  • 值传递定义补充:是指在调用函数时将 实际参数 复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
function foo(num) {//函数调用前初始化num = undefined,调用后,这里是值传递为100
  console.log(num) //100
  num = 200//这里不会意外创建全局变量或者修改全局变量的num值;因为函数内部在初始化的时候已经将形参的num初始化为undefined了,(这里找到的是函数初始化的num,所以改的也是它)
  console.log(num)//200
}
const num = 100
foo(num)
console.log(num)//100  这里的值还是100
1
2
3
4
5
6
7
8

# 4.2.浅拷贝

  • 定义:对一个对象进行clone生成新的对象,新的对象要开辟一块新的内存来存储,新对象中的基本类型属性和String类型属性都会开辟新的空间存储,但是如果是引用类型的属性,那这个引用类型的属性还是指向原对象的引用属性内存,当对新的对象或原对象的引用属性做出改变的时候,两方的引用属性类型的值同时做出改变。

  • 特点

    1. 属性是基本类型,拷贝的就是基本类型的值。因为基本数据类型是值传递,所以基本类型的拷贝,一方修改了对象的值互不影响。
    2. 属性是内存地址(引用类型),因为引用类型是引用传递,拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址中的属性,就会影响到另一个对象。
  • 下面我们具体来看一个例子来进行理解:

const obj = {
  name:'kobe',
  friend:{
    name:'james'
  }
}

const info = {...obj}//浅拷贝

info.name = 'curry'
console.log(obj.name)//kobe

info.friend.name = "rose"
console.log(obj.friend.name)//rose
console.log(obj)//{ name: 'kobe', friend: { name: 'rose' } }
console.log(info)//{ name: 'curry', friend: { name: 'rose' } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

vmPVzV.png

# 4.3.深拷贝

  • 定义:创建一个新对象,将原对象的各个属性的值拷贝过来。深拷贝要把复制对象所引用的对象都复制一遍。
  • 特点
    1. 拷贝的属性是基本类型,拷贝的就是基本类型的值。因为基本数据类型是值传递,所以基本类型的拷贝,一方修改了对象的值互不影响。(与浅拷贝一样)
    2. 拷贝的属性是引用类型,深拷贝会创建一个对象空间,再拷贝内容。两个对象互不影响。
  • 下面我们具体来看一个例子来进行理解:
const obj = {
  name:'kobe',
  friend:{
    name:'james'
  }
}

const info = JSON.parse(JSON.stringify(obj))//深拷贝

info.name = 'curry'
console.log(obj.name)//kobe

info.friend.name = "rose"
console.log(obj.friend.name)//james
console.log(obj)//{ name: 'kobe', friend: { name: 'james' } }
console.log(info)//{ name: 'curry', friend: { name: 'rose' } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

vmCvxf.png

# 5、数值的表示

  • 在ES6中规范了二进制和八进制和十六进制的写法:
//十进制(我们日常生活中写的数字都是十进制)
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
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 另外在ES2021新增特性:数字过长时,可以使用_作为连接符(为了阅读 大数值 方便)
// ES2021 即:ES12新增特性
const num = 100_000_000_000_000
1
2
最后更新时间: 2022/08/06, 00:48:27
彩虹
周杰伦