ECMAScript2015(ES6)之基础知识

作用域

什么是作用域?

几乎所有编程语言就是在变量中存储值,并且能读取和修改此值。事实上,在变量中存储值和取出值的能力,给程序赋予了状态。 如果没有这样的概念,一个程序虽然可以执行一些任务,但是它们将会受到极大的限制而且不会非常有趣。 但是这些变量该存储在哪,又该如何读取?为了完成这个目标,需要制定一些规则,这个规则就是:作用域。

常见的作用域主要分为几个类型:全局作用域、函数作用域、块状作用域、动态作用域。

对象类型
global/window全局作用域
function函数作用域(局部作用域)
{}块状作用域
this动态作用域

TIP

如果一个 变量 或者其他表达式不在 “当前的作用域”,那么JavaScript机制会继续沿着作用域链上查找直到全局作用域(global或浏览器中的window)如果找不到将不可被使用。 作用域也可以根据代码层次分层,以便子作用域可以访问父作用域,通常是指沿着链式的作用域链查找,而不能从父作用域引用子作用域中的变量和引用

全局作用域

变量在函数或者代码块 {} 外定义,即为全局作用域。不过,在函数或者代码块 {} 内未定义的变量也是拥有全局作用域的(不推荐)。

var course = "es5"

// 此处可调用 course 变量
function myFunction() {
    // 函数内可调用 course 变量
}

上述代码中变量 course 就是在函数外定义的,它是拥有全局作用域的。这个变量可以在任意地方被读取或者修改,当然如果变量在函数内没有声明(没有使用 var 关键字),该变量依然为全局变量。

// 此处可调用 course 变量

function myFunction() {
    course = "es"
    // 此处可调用 course 变量
}

以上实例中 course 在函数内,但是拥有全局作用域,它将作为 global 或者 window 的属性存在。

注意

在函数内部或代码块中没有定义的变量实际上是作为 window/global 的属性存在,而不是全局变量。换句话说没有使用 var 定义的变量虽然拥有全局作用域,但是它是可以被 delete 的,而全局变量不可以。
参考:https://www.runoob.com/js/js-scope.html

函数作用域

在函数内部定义的变量,就是局部作用域。函数作用域内,对外是封闭的,从外层的作用域无法直接访问函数内部的作用域!

function bar() {
    var testValue = 'inner'
}

console.log(testValue) // 报错:ReferenceError: testValue is not defined

如果想读取函数内的变量,必须借助 return 或者闭包。

function bar(value) {
    var testValue = 'inner'

    return testValue + value
}

console.log(bar('fun')) // "innerfun"

这是借助 return 的方式,下面是闭包的方式:

function bar(value) {
    var testValue = 'inner'

    var rusult = testValue + value

    function innser() {
        return rusult
    }

    return innser()
}

console.log(bar('fun')) // "innerfun"

通俗的讲,return 是函数对外交流的出口,而 return 可以返回的是函数,根据作用域的规则,函数内部的子函数是可以获取函数作用域内的变量的。

块状作用域

在其他编程语言中,块状作用域是很熟悉的概念,但是在JavaScript中不被支持,就像上述知识一样,除了全局作用域就是函数作用域,一直没有自己的块状作用域。在 ES6 中已经改变了这个现象,块状作用域得到普及。关于什么是块,只要认识 {} 就可以了。

if (true) {
    let a = 1
    console.log(a)
}
动态作用域

在 JavaScript 中很多同学对 this 的指向时而清楚时而模糊,其实结合作用域会对 this 有一个清晰的理解。不妨先来看下这段代码:

window.a = 3

function test() {
    console.log(this.a)
}

test.bind({
    a: 2
})() // 2
test() // 3

在这里 bind 已经把作用域的范围进行了修改指向了 `{ a: 2 }`,而 this 指向的是当前作用域对象,是不是可以清楚的理解了呢?

接下来我们再思考另一个问题:作用域是在代码编写的时候就已经决定了呢,还是在代码执行的过程中才决定的?

var course = " es"

// 此处可调用 course 变量
function myFunction() {
    // 函数内可调用 course 变量
}

在看看这段代码,写代码的时候就知道 course 就是全局作用域,函数内部的用 var 定义的变量就是函数作用域。这个也就是专业术语:词法作用域。 通俗的讲变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。 相反,只能在执行阶段才能决定变量的作用域,那就是动态作用域。

推荐阅读

Let

ES6 新增了let命令,用来声明变量。

1. let 声明的全局变量不是全局对象window的属性

这就意味着,你不可以通过 window. 变量名 的方式访问这些变量,而 var 声明的全局变量是 window 的属性,是可以通过 window. 变量名 的方式访问的。

var a = 5
console.log(window.a) // 5
let a = 5
console.log(window.a) // undefined
2. 用let定义变量不允许重复声明

这个很容易理解,使用 var 可以重复定义,使用 let 却不可以。

var a = 5
var a = 6

console.log(a) // 6

如果是 let ,则会报错

let a = 5
let a = 6
// VM131:1 Uncaught SyntaxError: Identifier 'a' has already been declared
//   at <anonymous>:1:1
3. let声明的变量不存在变量提升
function foo() {
    console.log(a)
    var a = 5
}

foo() //undefined

上述代码中, a 的调用在声明之前,所以它的值是 undefined,而不是 Uncaught ReferenceError。实际上因为 var 会导致变量提升,上述代码和下面的代码等同:

function foo() {
    var a
    console.log(a)
    a = 5
}

foo() //undefined

而对于 let 而言,变量的调用是不能先于声明的,看如下代码:

function foo() {
    console.log(a)
    let a = 5
}

foo()
// Uncaught ReferenceError: Cannot access 'a' before initialization

在这个代码中, a 的调用是在声明之前,因为 let 没有发生变量提升,所有读取 a 的时候,并没有找到,而在调用之后才找到 let 对 a 的定义,所以按照 tc39 的定义会报错。

4. let声明的变量具有暂时性死区

只要块级作用域内存在 let 命令,它所声明的变量就绑定在了这个区域,不再受外部的影响。

var a = 5
if (true) {
    a = 6
    let a
}
// Uncaught ReferenceError: Cannot access 'a' before initialization

上面代码中,存在全局变量 a ,但是块级作用域内 let 又声明了一个局部变量 a ,导致后者绑定这个块级作用域,所以在let声明变量前,对 a 赋值会报错。

ES6 明确规定,如果区块中存在 letconst 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”

有时“暂时性死区”比较隐蔽,比如:

function foo(b = a, a = 2) {
    console.log(a, b)
}
foo()
// Uncaught ReferenceError: Cannot access 'a' before initialization
5. let 声明的变量拥有块级作用域

let实际上为 JavaScript 新增了块级作用域

{
    let a = 5
}
console.log(a) // undefined

a 变量是在代码块 {} 中使用 let 定义的,它的作用域是这个代码块内部,外部无法访问。

我们再看一个项目中很常见的 for 循环:

for (var i = 0; i < 3; i++) {
    console.log('循环内:' + i) // 0、1、2
}
console.log('循环外:' + i) // 3

如果改为 let 会怎么样呢?

for (let i = 0; i < 3; i++) {
    console.log('循环内:' + i) // 0、1、2
}
console.log('循环外:' + i) // ReferenceError: i is not defined

继续看下面两个例子的对比,这时 a 的值又是多少呢?

if (false) {
    var a = 5
}
console.log(a) // undefined
if (false) {
    let a = 5
}
console.log(a)
// Uncaught ReferenceError: a is not defined

思考

请问 i 的值是多少?

for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i)
    })
}
// 3、3、3

答案是3次3。
但我希望的值是0、1、2,也就是每次保存住循环时候 i 的值,应该如何做呢?

方案1:闭包

for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j)
        })
    })(i)
}

方案2:使用let

for (let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i)
    })
}

大家可以把上面这段代码粘贴到 babel 中:https://www.babeljs.cn/repl , 看下经过 babel 转化为的 ES5 代码是什么样子的。

大家会发现其实 babel 把这段代码转化成了闭包的形式:

"use strict"

var _loop = function _loop(i) {
    setTimeout(function() {
        console.log(i)
    })
}

for (var i = 0; i < 3; i++) {
    _loop(i)
}

相信通过这个思考题,大家对于 let 关键字的使用会有进一步的理解。

总结

使用let声明的变量:

  • 不属于顶层对象window
  • 不允许重复声明
  • 不存在变量提升
  • 暂时性死区
  • 块级作用域

Const

不能被改变的叫做常量,请大家思考在 ES5 中如何定义一个常量呢?

ES5 中可以使用 Object.defineProperty() 来实现定义常量:

Object.defineProperty(window, 'PI', {
    value: 3.14,
    writable: false
})
console.log(PI)
PI = 5
console.log(PI)

const 除了具有 let 的块级作用域和不会变量提升外,还有就是它定义的是常量,在用 const 定义变量后,我们就不能修改它了,对变量的修改会抛出异常。

const PI = 3.1415

console.log(PI)

PI = 5

console.log(PI)
// Uncaught TypeError: Assignment to constant variable.

这个代码块中因为对 PI 尝试修改,导致浏览器报错,这就说明 const 定义的变量是不能被修改的,它是只读的。聪明的同学一定会发现只读属性是不是一定要进行初始化呢?

const PI

PI = 3.1415
// Uncaught SyntaxError: Missing initializer in const declaration

注意

const 声明的变量必须进行初始化,不然会抛出异常 Uncaught SyntaxError: Missing initializer in const declaration。

重点来喽

const obj = {
    name: 'test',
    age: 18
}
obj.school = 'abc'
console.log(obj)
// {name: "test", age: 18, school: "abc"}

大家会发现 const 定义的 obj 竟然被改变了… 这到底是为什么呢?有点懵啊…

这时我们就需要了解JS中的变量是如何存储的,见下图:
基本数据类型存储在 栈内存 中,引用数据类型存储在 堆内存 中然后在栈内存中保存 引用地址

TIP

const 实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。

如何让对象或者数组这种引用数据类型也不被改变呢?

Object.freeze(obj)

注意

Object.freeze() 只是浅层冻结,只会对最近一层的对象进行冻结,并不会对深层对象冻结。

总结

使用const声明的常量:

  • 不属于顶层对象window
  • 不允许重复声明
  • 不存在变量提升
  • 暂时性死区
  • 块级作用域

解构赋值

在 ES6 中新增了变量赋值的方式:解构赋值。允许按照一定模式,从数组和对象中提取值,对变量进行赋值。如果对这个概念不了解,我们可以快速展示一个小示例一睹风采:

let arr = [1, 2, 3]
let a = arr[0]
let b = arr[1]
let c = arr[2]

想从数组中找出有意义的项要单独赋值给变量,在 ES6 中就可以这样写了:

let [a, b, c] = [1, 2, 3]

TIP

解构赋值重点是在赋值,赋值的元素是要拷贝出来赋值给变量,赋值的元素本身是不会被改变的。

是不是非常的简洁,在解构赋值里用的最多的就是 Object 和 Array ,我们可以分别来看下两者的解构赋值是如何操作的。

数组的解构赋值

上述的小示例已经在描述数组的解构赋值了,可是那只是开始。

  • 赋值元素可以是任意可遍历的对象

    赋值的元素不仅是数组,它可以是任意可遍历的对象

    let [a, b, c] = "abc" // ["a", "b", "c"]
    let [one, two, three] = new Set([1, 2, 3])
    
  • 左边的变量

    被赋值的变量还可以是对象的属性,不局限于单纯的变量。

    let user = {}
    [user.firstName, user.secondName] = 'Kobe Bryant'.split(' ')
    
    console.log(user.firstName, user.secondName) // Kobe Bryant
    
  • 循环体

    解构赋值在循环体中的应用,可以配合 entries 使用。

    let user = {
      name: 'John',
      age: 30
    }
    
    // loop over keys-and-values
    for (let [key, value] of Object.entries(user)) {
      console.log(`${key}:${value}`) // name:John, then age:30
    }
    

    当然,对于 map 对象依然适用:

    let user = new Map()
    user.set('name', 'John')
    user.set('age', '30')
    
    for (let [key, value] of user.entries()) {
      console.log(`${key}:${value}`) // name:John, then age:30
    }
    
  • 可以跳过赋值元素

    如果想忽略数组的某个元素对变量进行赋值,可以使用逗号来处理。

    // second element is not needed
    let [name, , title] = ['John', 'Jim', 'Sun', 'Moon']
    
    console.log( title ) // Sun
    
  • rest 参数

    let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]
    
    console.log(name1) // Julius
    console.log(name2) // Caesar
    
    // Note that type of `rest` is Array.
    console.log(rest[0]) // Consul
    console.log(rest[1]) // of the Roman Republic
    console.log(rest.length) // 2
    

注意

我们可以使用 rest 来接受赋值数组的剩余元素,不过要确保这个 rest 参数是放在被赋值变量的最后一个位置上。

  • 默认值

    如果数组的内容少于变量的个数,并不会报错,没有分配到内容的变量会是 undefined。

    let [firstName, surname] = []
    
    console.log(firstName) // undefined
    console.log(surname) // undefined
    

    当然你也可以给变量赋予默认值,防止 undefined 的情况出现:

    // default values
    let [name = "Guest", surname = "Anonymous"] = ["Julius"]
    
    console.log(name)    // Julius (from array)
    console.log(surname) // Anonymous (default used)
    
对象解构赋值
  • 基本用法
解构赋值除了可以应用在 Array,也可以应用在 Object。基本的语法如下:

> let {var1, var2} = {var1:…, var2…}

大致的意思是我们有一个 Object 想把里面的属性分别拿出来而无需通过调用属性的方式赋值给指定的变量。具体的做法是在赋值的左侧声明一个和 Object 结构等同的模板,然后把关心属性的 value 指定为新的变量即可。

```
let options = {
  title: "Menu",
  width: 100,
  height: 200
}

let {title, width, height} = options

console.log(title)  // Menu
console.log(width)  // 100
console.log(height) // 200
```

TIP

在这个结构赋值的过程中,左侧的“模板”结构要与右侧的 Object 一致,但是属性的顺序无需一致。

上述的赋值左侧是采用了对象简写的方式,类似于:

let {title: title, width: width, height: height} = options

如果不想这么写或者想使用其他的变量名,可以自定义的,如下:

let {width: w, height: h, title} = options
  • 默认值

    当然,这个赋值的过程中也是可以指定默认值的,这样做:

    let options = {
      title: "Menu"
    }
    
    let {width = 100, height = 200, title} = options
    
    console.log(title)  // Menu
    console.log(width)  // 100
    console.log(height) // 200
    
  • rest 运算符

    如果我们想象操作数组一样,只关心指定的属性,其他可以暂存到一个变量下,这就要用到 rest 运算符了

    let options = {
      title: "Menu",
      height: 200,
      width: 100
    }
    
    let {title, ...rest} = options
    
    // now title="Menu", rest={height: 200, width: 100}
    console.log(rest.height)  // 200
    console.log(rest.width)   // 100
    
  • 嵌套对象

    如果一个 Array 或者 Object 比较复杂,它嵌套了 Array 或者 Object,那只要被赋值的结构和右侧赋值的元素一致就好了。

    let options = {
      size: {
        width: 100,
        height: 200
      },
      items: ["Cake", "Donut"],
      extra: true    // something extra that we will not destruct
    }
    
    // destructuring assignment on multiple lines for clarity
    let {
      size: { // put size here
        width,
        height
      },
      items: [item1, item2], // assign items here
      title = 'Menu' // not present in the object (default value is used)
    } = options
    
    console.log(title)  // Menu
    console.log(width)  // 100
    console.log(height) // 200
    console.log(item1)  // Cake
    console.log(item2)  // Donut
    
字符串解构赋值

可以当做是数组的解构:

let str = 'imooc'

let [a, b, c, d, e] = str

console.log(a, b, c, d, e)

Array

在 ES6 中新增了很多实用的原生 API,方便开发者对 Array 的操控性更强,如 for…of、from、of、fill、find、findIndex等。

ES5 中数组遍历方式

let arr = [1, 2, 3, 2, 4]

for循环

for (let i = 0; i < arr.length; i++) {
    console.log(arr[i])
}

后来语法有所升级,到 ES5 遍历数组的 API 多了起来,其中有 forEach、every、filter等,同样的功能可以用 forEach 、 map 、 every 等方法来实现。

forEach() 没有返回值,只是针对每个元素调用func

arr.forEach(function(elem, index, array) {
    if (arr[i] == 2) {
        continue
    }
    console.log(elem, index)
})

这个语法看起来要简洁很多,不需要通过索引去访问数组项,然而它的缺点也是很明显,不支持 break、continue 等。

[1, 2, 3, 4, 5].forEach(function(i) {
    if (i === 2) {
        return;
    } else {
        console.log(i)
    }
})

这段代码的"本意"是从第一个元素开始遍历,遇到数组项 2 之后就结束遍历,不然打印出所遍历过的数值项。可是,事实让你大跌眼镜,因为它的输出是 1, 3, 4, 5。

forEach 的代码块中不能使用 break、continue,它会抛出异常。

map() 返回新的数组,每个元素为调用func的结果

let result = arr.map(function(value) {
    value += 1
    console.log(value)
    return value
})
console.log(arr, result)

filter() 返回符合func条件的元素数组

let result = arr.filter(function(value) {
    console.log(value)
    return value == 2
})
console.log(arr, result)

some() 返回boolean,判断是否有元素符合func条件

let result = arr.some(function(value) {
    console.log(value)
    return value == 4
})
console.log(arr, result)

every() 返回boolean,判断每个元素都符合func条件

let result = arr.every(function(value) {
    console.log(value)
    return value == 2
})
console.log(arr, result)

同样完成刚才的目标,使用 every 遍历就可以做到 break 那样的效果,简单的说 return false 等同于 break,return true 等同于 continue。如果不写,默认是 return false。

every 的代码块中不能使用 break、continue,它会抛出异常。

reduce() 接收一个函数作为累加器

let sum = arr.reduce(function(prev, cur, index, array) {
    return prev + cur
}, 0)
console.log(sum)
let max = arr.reduce(function(prev, cur) {
    return Math.max(prev, cur)
})
console.log(max)
let res = arr.reduce(function(prev, cur) {
    prev.indexOf(cur) == -1 && prev.push(cur)
    return prev
}, [])
console.log(res)

有的同学会说,还有 for…in 可以遍历数组。

for (var index in array) {
    console.log(array[index]);
}

说的没错,for…in 确实可以遍历数组,而且还支持 continue、break等功能,但是它真的没有瑕疵吗?如果 array 有自定义属性,你发现也会被遍历出来(显然不合理)。这是因为 for…in 是为遍历对象创造的({a:1, b:2}),不是为数组设计的。

注意

for…in不能用于遍历数组。
for…in代码块中不能有 return,不然会抛出异常。

ES6 中数组遍历方式 for…of

接下来就要步入正题,说说我们今天的主角:for…of。

for (let val of [1, 2, 3]) {
    console.log(val);
}
// 1,2,3

上述代码中轻松实现了数组的遍历,乍一看没有绝对它有非常强大之处。我们不得不强调下,for…of 的来历和作用。

for (variable of iterable) {

}

看下这个伪代码,of 后面是 iterable 既不是 for 循环规定的 array,也不是 for…in 规定的 Object,而是 iterable。如果查查 iterable 的含义就很直观的感受到 for…of 遍历的是一切可遍历的元素(数组、对象、集合)等,不要小瞧这个功能,因为在 ES6 中允许开发者自定义遍历,换句话说任何数据结构都可以自定义一个遍历,这个遍历是不能被 for、for…in 理解和实现的。很抽象吧?Iterator 是如何实现的这是ES6的新增语法,后面课程中 Iterator 一节会讲。

for (let item of arr) {
    console.log(item)
}

for (let item of arr.values()) {
    console.log(item)
}

for (let item of arr.keys()) {
    console.log(item)
}

for (let [index, item] of arr.entries()) {
    console.log(index, item)
}

TIP

for…of是支持 break、continue、return的,所以在功能上非常贴近原生的 for。

Array.from()

数组是开发中经常用到的数据结构,它非常好用。在 JavaScript 的世界里有些对象被理解为数组,然而缺不能使用数组的原生 API,比如函数中的 arguments、DOM中的 NodeList等。当然,还有一些可遍历的对象,看上去都像数组却不能直接使用数组的 API,因为它们是伪数组(Array-Like)。要想对这些对象使用数组的 API 就要想办法把它们转化为数组,传统的做法是这样的:

let args = [].slice.call(arguments);
let imgs = [].slice.call(document.querySelectorAll('img'));

基本原理是使用 call 将数组的 api 应用在新的对象上,换句话说是利用改变函数的上下文来间接使用数组的 api。在 ES6 中提供了新的 api 来解决这个问题,就是 Array.from,代码如下:

let args = Array.from(arguments);
let imgs = Array.from(document.querySelectorAll('img'));

TIP

伪数组具备两个特征,1. 按索引方式储存数据 2. 具有length属性;如:

let arrLike = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3
}

惊喜

难道 Array.from 只能用来将伪数组转换成数组吗,还有其他用法吗?这要来看下 Array.from 的几个参数:

语法: Array.from(arrayLike[, mapFn[, thisArg]])

参数含义必选
arrayLike想要转换成数组的伪数组对象或可迭代对象Y
mapFn如果指定了该参数,新数组中的每个元素会执行该回调函数N
thisArg可选参数,执行回调函数 mapFn 时 this 对象N

看了这几个参数至少能看到 Array.from 还具备 map 的功能,比如我们想初始化一个长度为 5 的数组,每个数组元素默认为 1,之前的做法是这样的:

let arr = Array(6).join(' ').split('').map(item => 1)
// [1,1,1,1,1]

这样写虽然也能实现,但是用起来比较繁琐,使用 Array.from 就会简洁很多。

Array.from({
    length: 5
}, function() {
    return 1
})

Array.of()

Array.of() 方法创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型。

Array.of() 和 Array 构造函数之间的区别在于处理整数参数:Array.of(7) 创建一个具有单个元素 7 的数组,而 Array(7) 创建一个长度为7的空数组(注意:这是指一个有7个空位(empty)的数组,而不是由7个undefined组成的数组)。

Array.of(7); // [7]
Array.of(1, 2, 3); // [1, 2, 3]

Array(7); // [ , , , , , , ]
Array(1, 2, 3); // [1, 2, 3]

语法: Array.of(element0[, element1[, ...[, elementN]]])

参数含义必选
elementN任意个参数,将按顺序成为返回数组中的元素Y

Array.prototype.fill()

fill() 方法用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引。

let array = [1, 2, 3, 4]
array.fill(0, 1, 2)
// [1,0,3,4]

这个操作是将 array 数组的第二个元素(索引为1)到第三个元素(索引为2)内的数填充为 0,不包括第三个元素,所以结果是 [1, 0, 3, 4]

技巧

我们前面有提到用 Array.from 初始化为一个长度固定,元素为指定值的数组。如果用 fill 是否可以达到同样的效果呢?

Array(5).fill(1)
// [1,1,1,1,1]

TIP

fill 不具备遍历的功能,它是通过指定要操作的索引范围来进行,通过这道题目可以看出不指定索引会对所有元素进行操作

语法: arr.fill(value[, start[, end]])

参数含义必选
value用来填充数组元素的值Y
start起始索引,默认值为0N
end终止索引,默认值为 this.lengthN

Array.prototype.find()

find() 方法返回数组中满足提供的测试函数的第一个元素的值,否则返回 undefined。

let array = [5, 12, 8, 130, 44];

let found = array.find(function(element) {
    return element > 10;
});

console.log(found);
// 12

语法: arr.find(callback[, thisArg])

参数含义必选
callback在数组每一项上执行的函数,接收 3 个参数,element、index、arrayY
thisArg执行回调时用作 this 的对象N

Array.prototype.findIndex()

findIndex()方法返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1。其实这个和 find() 是成对的,不同的是它返回的是索引而不是值。

let array = [5, 12, 8, 130, 44];

let found = array.find(function(element) {
    return element > 10;
});

console.log(found);
// 1

语法: arr.findIndex(callback[, thisArg])

参数含义必选
callback在数组每一项上执行的函数,接收 3 个参数,element、index、arrayY
thisArg执行回调时用作 this 的对象N

Array.prototype.copyWithin()

在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。

语法: arr.copyWithin(target, start = 0, end = this.length)

参数含义必选
target从该位置开始替换数据。如果为负值,表示倒数Y
start从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算N
end到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算N
let arr = [1, 2, 3, 4, 5]
console.log(arr.copyWithin(1, 3))
// [1, 4, 5, 4, 5]

Function

默认参数

对于函数而言,经常会用到参数,关于参数的默认值通常都是写在函数体中,如在 ES5 的时候大家都会这么写:

function foo(x, y) {
    y = y || 'world'
    console.log(x, y)
}
foo('hello', 'imooc')
foo('hello', 0)

当一个函数有很多参数涉及初始化的时候,这样写代码极其丑陋,所以在 ES6 中改变了对这种知识的写法:

function foo(x, y = 'world') {
    console.log(x, y)
}
foo('hello', 0)

TIP

函数参数是从左到右解析,如果没有默认值会被解析成 undefined

如果我们想让具体某个参数使用默认值,我们可以使用 undefined 进行赋值,如下段代码所示:

function f(x, y = 7, z = 42) {
    return x + y + z
}
console.log(f(1, undefined, 43)) // 51

在ES6中我们不仅可以给参数默认赋值具体的数值,同时参数赋值支持参数的逻辑运算进行赋值,如下段代码所示:

function f(x, y = 7, z = x + y) {
    return z * 0.5
}

console.log(f(1, 7)) // 4

再看一个例子:

function ajax(url, {
    body = '',
    method = 'GET',
    headers = {}
} = {}) {
    console.log(method)
}

ajax('http://www.imooc.com', {
    method: 'POST'
})

拓展

在函数体内,有时候需要判断函数有几个参数,一共有2个办法。在 ES5 中可以在函数体内使用 arguments 来判断。

function foo(a, b = 1, c) {
    console.log(arguments.length)
}
foo('a', 'b') //2

然而在 ES6 中不能再使用 arguments 来判断了,但可以借助 Function.length 来判断。

function foo(a, b = 1, c) {
    console.log(foo.length)
}
foo('a', 'b') // 1

细心的同学发现 Function.length 结果和 arguments 的结果不同!没错,Function.length 是统计第一个默认参数前面的变量数:

function foo(a = 2, b = 1, c) {
    console.log(foo.length)
}
foo('a', 'b') // 0

Rest 参数

在写函数的时候,部分情况我们不是很确定参数有多少个,比如求和运算,之前都是这么做的:

function sum() {
    let num = 0
    Array.prototype.forEach.call(arguments, function(item) {
        num += item * 1
    })
    return num
}

console.log(sum(1, 2, 3)) // 6
console.log(sum(1, 2, 3, 4)) // 10

其实在上面说过,这个代码在 ES5 中可以这么写,在 ES6 就不能这么写了,因为 arguments 的问题。现在需要这样写:

function sum(...nums) {
    let num = 0
    nums.forEach(function(item) {
        num += item * 1
    })
    return num
}

console.log(sum(1, 2, 3)) // 6
console.log(sum(1, 2, 3, 4)) // 10

当然,Rest Parameter 也可以和其他参数一起来用,比如:

function sum(base, ...nums) {
    let num = base
    nums.forEach(function(item) {
        num += item * 1
    })
    return num
}

console.log(sum(30, 1, 2, 3)) // 36
console.log(sum(30, 1, 2, 3, 4)) // 40

注意

arguments 不是数组,所以不能直接使用数组的原生 API 如 forEach,而 Rest Parameter 是数组,可以直接使用数组的原生 API。

扩展运算符

Spread Operator 和 Rest Parameter 是形似但相反意义的操作符,简单的来说 Rest Parameter 是把不定的参数“收敛”到数组,而 Spread Operator 是把固定的数组内容“打散”到对应的参数。示例如下:

function sum(x = 1, y = 2, z = 3) {
    return x + y + z
}

console.log(sum(...[4])) // 9
console.log(sum(...[4, 5])) // 12
console.log(sum(...[4, 5, 6])) // 15

大家可以好好体会下前面两个示例,Rest Parameter 用来解决函数参数不确定的场景,Spread Operator 用来解决已知参数集合应用到固定参数的函数上,如果没有这个语法,可能需要这样做:

function sum(x = 1, y = 2, z = 3) {
    return x + y + z
}

console.log(sum.apply(null, [4])) // 9
console.log(sum.apply(null, [4, 5])) // 12
console.log(sum.apply(null, [4, 5, 6])) // 15

箭头函数

箭头函数可以说是 ES6 很大的福利了,不管你是函数式爱好者还是面向对象开发者,函数是必须要用到的东西。之前声明函数需要使用 function,如下:

function hello() {
    console.log('say hello')
}
// 或

let hello = function() {
    console.log('say hello')
}

现在可以这样做了:

let hello = () => {
    console.log('say hello')
}

如果带参数该怎么做呢?

let hello = (name) => {
    console.log('say hello', name)
}
// 或者

let hello = name => {
    console.log('say hello', name)
}

TIP

如果只有一个参数,可以省略括号,如果大于一个参数一定要记得带括号
函数的声明和参数写的很清楚了,那么对于返回值有什么要注意的地方呢?

  • 如果返回值是表达式

    如果返回值是表达式可以省略 return 和 {}

  let pow = x => x * x
  • 如果返回值是字面量对象

    如果返回值是字面量对象,一定要用小括号包起来

  let person = (name) => ({
      age: 20,
      addr: 'Beijing City'
  })

拓展

看上去箭头函数真的很漂亮,可是它有什么神秘之处吗?this,对,就是它。普通函数和箭头函数对 this 的处理方式是截然不同的。

let foo = {
    name: 'es',
    say: function() {
        console.log(this.name)
    }
}

console.log(foo.say()) // es

这是用普通函数的写法,say 在被调用之后,this 指向的是调用 say 方法的对象,显示是 foo 对象,所以 this === foo this.name 也就是 foo.name。

let foo = {
    name: 'es',
    say: () => {
        console.log(this.name, this)
    }
}
console.log(foo.say()) // undefined

因为箭头函数中对 this 的处理是定义时,this 的指向也就是 foo 外层的所指向的 window,而 window 没有 name 属性,所以结果是 undefined。

总结

1、箭头函数中this指向定义时所在的对象,而不是调用时所在的对象

2、箭头函数不可以当作构造函数

3、箭头函数不可以使用arguments对象

Object

属性简洁表示法

在 ES6 之前 Object 的属性必须是 key-value 形式,如下:

  let name = 'lalala'
  let age = 18
  let obj = {
      name: name,
      age: age,
      study: function() {
          console.log(this.name + '正在学习')
      }
  }

在 ES6 之后是可以用简写的形式来表达:

  let name = 'lalala'
  let age = 18
  let obj = {
      name,
      age,
      study() {
          console.log(this.name + '正在学习')
      }
  }

属性名表达式

在 ES6 可以直接用变量或者表达式来定义Object的 key。

  let s = 'school'
  let obj = {
      foo: 'bar',
      [s]: 'test'
  }

Object.is()

判断两个对象是否相等。

let obj1 = { // new Object()
    name: 'lalala',
    age: 18
}

let obj2 = { // new Object()
    name: 'lalala',
    age: 18
}
console.log(obj1 == obj2) // false

console.log(Object.is(obj1, obj2)) // false

let obj2 = obj1

console.log(Object.is(obj1, obj2)) // true

Object.assign()

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象,它将返回目标对象。

const target = {
    a: 1,
    b: 2
}
const source = {
    b: 4,
    c: 5
}

const returnedTarget = Object.assign(target, source)

console.log(target)
// expected output: Object { a: 1, b: 4, c: 5 }

console.log(returnedTarget)
// expected output: Object { a: 1, b: 4, c: 5 }

基本语法

Object.assign(target, …sources)

参数含义必选
target目标对象Y
sources源对象N

TIP

从语法上可以看出源对象的个数是不限制的(零个或多个),如果是零个直接返回目的对象,如果是多个相同属性的会被后边的源对象的属相覆盖。

let s = Object.assign({
    a: 1
})
// {a: 1}

注意

如果目的对象不是对象,则会自动转换为对象

let t = Object.assign(2)
// Number {2}
let s = Object.assign(2, {
    a: 2
})
// Number {2, a: 2}

如果对象属性具有多层嵌套,这时使用Object.assign()合并对象会怎么样呢?

let target = {
    a: {
        b: {
            c: 1
        },
        e: 4,
        f: 5,
        g: 6
    }
}
let source = {
    a: {
        b: {
            c: 1
        },
        e: 2,
        f: 3
    }
}
Object.assign(target, source)
console.log(target)

我们惊奇的发现, g 属性消失了…

注意

Object.assign()对于引用数据类型属于浅拷贝。

TIP

对象的浅拷贝:浅拷贝是对象共用的一个内存地址,对象的变化相互印象。
对象的深拷贝:简单理解深拷贝是将对象放到新的内存中,两个对象的改变不会相互影响。

思考

  1. 如果目标对象传入的是 undefined 和 null 将会怎样呢?
  2. 如果源对象的参数是 undefined 和 null 又会怎样呢?
  3. 如果目标对象是个嵌套的对象,子对象的属性会被覆盖吗?

对象的遍历方式

如何能够遍历出对象中每个key和value的值呢?

let obj = {
    name: 'lalala',
    age: 18,
    school: 'test'
}

Array中演示过,for…in不能够用于遍历Array,for…in的作用是用于遍历对象的。

for (let key in obj) {
    console.log(key, obj[key])
}

Object.keys()用于返回对象所有key组成的数组。

Object.keys(obj).forEach(key => {
    console.log(key, obj[key])
})

Object.getOwnPropertyNames()用于返回对象所有key组成的数组。

Object.getOwnPropertyNames(obj).forEach(key => {
    console.log(key, obj[key])
})

Reflect.ownKeys()用于返回对象所有key组成的数组。

Reflect.ownKeys(obj).forEach(key => {
    console.log(key, obj[key])
})

手写一个基础深拷贝:

//检查类型
let checkType = data => {
    return Object.prototype.toString.call(data).slice(8, -1)
}
let deepClone = target => {
    let targetType = checkType(target)
    let cloneTarget
    if (targetType === 'Object') {
        cloneTarget = {}
    } else if (targetType === 'Array') { 
        cloneTarget = []
    } else {
        return target
    }
    for (let key in target) {
        let value = target[key]
        let valueType = checkType(value)
        if (valueType === 'Object' || valueType === 'Array') {
            cloneTarget[key] = deepClone(value) //递归
        } else { 
            cloneTarget[key] = value
        }
    }
    return cloneTarget
}
  • 20
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值