ECMA Script 6

ECMA Script 6(ES2016)学习笔记

TIP

最后更新时间:2019年09月21日

字数:18029

ES6

  • ECMA-262 可以称之为:ES1.0
  • ECMA2015 -> ES6
  • ECMA每年6月份发布一个版本,(不一定是大版本,所以有时候叫ES7也不合适)
  • ESNext:下一代的ES开发语言,或者称之为下一个版本的开发语言

学习资料

Stage-n

提案变成标准需要经过五个阶段

  • stage 0 :展示阶段
  • stage 1 :征求意见阶段
  • stage 2 :草案阶段(bebel配置,一般都是stage2,其实就是意味着可以使用的阶段)
  • stage 3 :候选阶段
  • stage 4 :定案阶段(标准)

TIP

Ecma TC39委员会(任何人都可以提es任何意见和建议哦):快捷连接

ES6之前变量

全局作用域

  • 在任何地方都可以访问的变量,就是全局变量
  • 全局变量生效的作用域,就是全局作用域
var a = "全局";
function fn(){
    console.log(a);
}
fn();//结果:全局
1
2
3
4
5

函数作用域

  • 只能在函数内部访问的变量就是局部变量
  • 对应的作用域就是局部作用域(又称函数作用域)
  • 因为js只有函数可以划分作用域(当前阶段ES5)
function fn(){
    var a = "局部";
}
fn();
console.log(a);// ReferenceError: a is not defined

function fn1() {
    b = '不带var'
}
fn1()
console.log(b); // 不带var
1
2
3
4
5
6
7
8
9
10
11

TIP

函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!

ES6作用域

块级作用域

  • ES6引入了块级作用域,明确允许在块级作用域中声明函数
  • 块级作用域中,函数声明语句的行为类似于let,在块级作用域之外,不可以引用
  • 块级作用域用一对花括号来表示
// ES5之前模拟块级作用域:匿名函数自调用,LLFE,立即执行函数
(function(){
  // TODO
})()

{
	// 这里是块级作用域
}

{{ // 块级作用域可以嵌套 }}
1
2
3
4
5
6
7
8
9
10

let

相当于之前的var

const

  • const翻译过来是常量的意思
  • 所谓常量,就是定义好了,就不能改变了
  • const声明的是一个只读常量,在声明时就需要赋值
    • 如果 const 的是一个对象,对象所包含的值是可以被修改的
    • 抽象一点儿说,就是对象所指向的地址不能改变,而变量成员是可以修改的
const a = 12
const a = 14
console.log(a) 
// Identifier 'a' has already been declared
// 不允许重重复定义
1
2
3
4
5
const a = 12
a = 14
console.log(a) 
// Assignment to constant variable
// 就是定义好了,就不能改变了
1
2
3
4
5
const a = {
  name:"zhangsan"
}
a.name = "lisi"
console.log(a) // {name: "lisi"}
// 如果 const 的是一个对象,对象所包含的值是可以被修改的
1
2
3
4
5
6

let和const特点

1. letconst 都是块级作用域,以{}代码块作为作用域范围 只能在代码块里面使用


if(1) {
    var a = 12
    console.log('a===',a) // a=== 12
}
console.log('a===',a) // a=== 12
if(1) {
    let b = 14
    const c = 16
    console.log('b===',b) // b=== 14
    console.log('c===',c) // c=== 16
}
console.log('b===',b) // b is not defined
console.log('c===',b) // c is not defined (这里其实是执行不到的,执行到也会报错)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

TIP

let可以在{},if,for里声明,其用法同var,但是作用域限定在块级,也就是说出了块级作用域,let和const声明的变量是无法使用的,不是全局作用域

2. 不存在变量提升,只能先声明再使用,否则会报错

var a = 12
function fn() { 
  alert(a) // 12
}
fn()
// 这是正常情况下的,没有毛病,a全局作用域
1
2
3
4
5
6
var a = 12
function fn() { 
  alert(a) // undefined
  var a = 20
}
fn()
//undefined 也是没有问题的,打印的a,会使用函数内部的a,但是因为变量提升,但是没有赋值,所以打印undefined
1
2
3
4
5
6
7
var a = 12
function fn() { 
  alert(a) // a is not defined
  let a = 20
}
fn()
// 这里问题来了,他说a没有定义,可以说明:
// a没有使用外部的全局变量a,同时,let声明的a变量没有做变量提升
1
2
3
4
5
6
7
8

TIP

在代码块内,在声明变量之前,该变量都是不可用的

这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)

3. 在同一个代码块内,不允许重复声明

{
  let a = 11
  let a = 12 // Identifier 'a' has already been declared
  {
    let a = 13 // 可以,因为不在同一个块级作用域里面(大括号)
  }
}
1
2
3
4
5
6
7

let的for循环问题

var a = []
for (let i =0; i<10; i++) {
  a[i] = function () {
    console.log(i)
  }
}
a[6]() // 6
1
2
3
4
5
6
7
  • 变量i是let声明的 当前i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量
  • Javascript之所以知道上一轮循环的值,是因为javascript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算,而不是let声明的i自加
var arr = []
for (var i =0; i<10; i++) {
  arr[i] = function () { 
    console.log(i)
  }
}
arr[4]() // 10

for (let i =0; i<5; i++) {
  let i = "xiaofengge"
  console.log(i) // 打印五次小峰哥,而不是数字i,因为他们其实在不同的作用域
}
1
2
3
4
5
6
7
8
9
10
11
12

TIP

for循环还有一个特别之处,就是设置循环变量那部分是一个父级作用域,而循环体内部是一个单独的子作用域

Object.freeze()冻结一个对象

  • Object.freeze()是ES5新增的特性,可以冻结一个对象,防止对象被修改
  • 冻结指的是不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。防止对象被修改
  • 如果你有一个巨大的数组或Object,并且确信数据不会修改,使用Object.freeze()可以让性能大幅提升
  • Vuejs中可以使用提升性能,同时:Object.freeze()冻结的是值,你仍然可以将变量的引用替换掉,也就是重新赋值(一个不是Object.freeze()的值)

解构赋值

结构示例

let arr = ['11','22']
console.log(arr) //  ["11", "22"]

let [a, b, c] =  ['aaa','bbb','ccc']
console.log(a,b,c) // aaa bbb ccc  这就是最简单的解构赋值

let json = {
  name: "zhangsan",
  age: 18,
  job: "CEO"
}
let { name, age, job } = json

console.log(name, age, job) // zhangsan 18 CEO
1
2
3
4
5
6
7
8
9
10
11
12
13
14

规定:解构赋值要求:左右两边,结构和格式要保持一致,否则有些数据会出问题

解构别名(冒号)

let json = {
  name: "zhangsan",
  age: 18,
  job: "CEO"
}
let { name:n, age:a, job:j } = json
// 解构别名
console.log(n, a, j) // zhangsan 18 CEO
1
2
3
4
5
6
7
8

解构默认值

let [a1, b1, c1="暂无数据"] = ['aaa', 'bbb']
console.log(a1, b1, c1) // aaa bbb 暂无数据
let [a2, b2, c2="暂无数据"] = ['aaa', 'bbb', undefined]
console.log(a2, b2, c2) // aaa bbb 暂无数据
let [a3, b3, c3="暂无数据"] = ['aaa', 'bbb', null]
console.log(a3, b3,c3) // aaa bbb null

let json = {
  name: "zhangsan"
}
let {name, age=18} = json
console.log(name, age) // zhangsan 18
1
2
3
4
5
6
7
8
9
10
11
12

TIP

  • 解构赋值是可以设置默认值的
  • 注意后台传回来的是null的时候,是有值,但是值是null,而不可以像undefined去取到默认值

解构应用

  • import {a ,b , c } from 'xxx'
  • 函数传参解构
  • 数组交换顺序解构(比如交换2个变量的值: [a,b] = [b,a])

字符串模板

ES6新增字符串模板``

let name = "zhangsan"
let age = 18

// 我是zhangsan,我的年龄是18岁
console.log('我是'+name+',我的年龄是'+age+'岁')

// 我是zhangsan,我的年龄是18岁
console.log(`我是${name},我的年龄是${age}岁`)
1
2
3
4
5
6
7
8

includes

字符串或数组查找

  • Str.includes() 和 Array.includes()
  • indexOf()返回的是数值,而includes()返回的是布尔值
  • indexOf() 不能判断NaN,返回为-1 ,includes()则可以判断

startWith

判断以xxx开头

  • str.startWith()

endsWith

判断以xxx结尾

  • str.endsWith()
let str = 'https://www.baidu.com'

console.log(str.startsWith('https')) // true
console.log(str.endsWith('.com')) // false
1
2
3
4

repeat

字符串重复次数

  • str.repeat(次数)
  • 参数次数必须大于等于0的整数,写小数会被默认取整的,不会报错
let str = '你好!'
// 重复三次(后面参数是次数,必须大于等于0)
console.log(str.repeat(3)) // 你好!你好!你好!
1
2
3

padStart和padEnd

  • str.padStart(长度,需要填充的参数):往前填充
  • str.padEnd(长度,需要填充的参数):在后面填充

函数默认参数

ES6中函数的默认参数是可以传递默认值的

function fn(a='你好',b="小峰哥") {  
  console.log(a,b)
}
fn() // 你好 小峰哥
fn('') //  小峰哥 (前面传递的空字符串)


// 解构联合函数默认参数
// 如果都不传,默认参数是:{}
function fn({ a = 0, b = 0 } = {}) {  
  console.log(a,b)
}
fn({a:1}) // 1 0
fn({b:2}) // 0 2
fn({c:3}) // 0 0
fn()      // 0 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

TIP

函数的参数默认是已经定义好了,不能再用let和const去定义

function fn(a,b) {  
  let a = 13 // 报错: Identifier 'a' has already been declared
  console.log(a,b)
}
fn()
1
2
3
4
5

扩展运算符(rest运算符)

写法:三个点   ...
1
  • 扩展和重置
let arr = ['aaa','bbb','ccc']
console.log(arr) // 数组: ["aaa", "bbb", "ccc"]
console.log(...arr) // 数组被展开了:  aaa bbb ccc

// ... 既可以展开,也可以重置
function fn(...a) {  
  console.log(a) // [1, 2, 3, 4, 5, 6] 数组
}
fn(1,2,3,4,5,6)
1
2
3
4
5
6
7
8
9
  • 剩余
// ...c 就是剩余的
function fn(a, b, ...c) {  
  console.log(a,b) // 1 2
  console.log(c) // 数组:  [3, 4, 5, 6]  
}
fn(1,2,3,4,5,6)
1
2
3
4
5
6

TIP

剩余必须放在最后,不允许放在中间,必须是最后一个参数

箭头函数

let fn = () => {
  console.log('我是箭头函数')
}

fn() // 我是箭头函数
1
2
3
4
5

箭头函数如果有返回值,可以直接写在后面

let fn = () => '我是箭头函数!'

function fn1() {
  return '我是箭头函数!!'
}

// 上下2个函数是等价的
console.log(fn()) // 我是箭头函数!
console.log(fn1()) // 我是箭头函数!!
1
2
3
4
5
6
7
8
9

TIP

箭头函数的this永远指向定义函数所在的对象,而不是运行时所在的对象

箭头函数的this永远指向其上下文的 this,任何方法都改变不了其指向,如call(), bind(), apply()

 var name = "lisi"
 let obj = {
   name: 'zhangsan',
   say: function () { 
     setTimeout( function () {
       console.log(this.name) // lisi
     }, 2000);
   }
 }
 obj.say()
1
2
3
4
5
6
7
8
9
10
var name = "lisi"
let obj = {
  name: 'zhangsan',
  say: function () { 
    setTimeout(() => {
      console.log(this.name) // zhangsan
    }, 2000);
  }
}
obj.say()
1
2
3
4
5
6
7
8
9
10

箭头函数不绑定arguments,取而代之用rest(...)参数

function fn() {
  console.log(arguments)
}
fn(1,2,3)

let fn1 = () => {
  console.log(arguments)
}
fn(1,2,3)
fn1(1,2,3) // 报错 arguments is not defined

let fn2 = (...c) => {
  console.log(c)
}
fn2(1,2,3) // [1, 2, 3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

箭头函数作为匿名函数,是不能作为构造函数的,不能使用new

var B = ()=>{
  value:1;
}
var b = new B(); //TypeError: B is not a constructor
1
2
3
4

ES6数组扩展

新增运算符

幂运算符:****** (2个星号)

console.log(2 ** 3) // 8
1

ES6之前

循环

  • for

  • while

  • arr.forEach()

    • 代替普通for
  • arr.map()

    • 非常有用,做数据交互 "映射",重新整理数据结构
    • 正常情况下,需要配合return,返回是一个新的数组
    • 若是没有return,相当于forEach
    • 注意:平时只要用map,一定是要有return
  • arr.filter()

    • 过滤,过滤一些不合格“元素”, 如果回调函数返回true,就留下来
  • arr.some()

    • 类似查找, 数组里面某一个元素符合条件,返回true
  • arr.every()

    • 数组里面所有的元素都要符合条件,才返回true
  • arr.reduce()

    • 从左往右
  • arr.reduceRight()

    • 从右往左

ES6新增循环for...of...

  • 遍历所有数据结构的统一的方法
  • 使用范围:
    • 数组
    • Set
    • Map
    • 类数组
      • 字符串
      • arguments
      • DOM Nodelist对象
      • Generator

对象简洁语法

对象新增

Promise

简单来说,Promise 就是用同步的方式写异步的代码,用来解决回调(异步)问题

js单线程

  • Javascript语言的执行环境是"单线程"(single thread),就是指一次只能完成一件任务
  • 如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推

TIP

注意:

  • 单线程和多线程的区别
  • 线程和进程的区别
  • js是单线程的,浏览器是多进程的

Promise之前

三种解决异步问题的方案:

  • 回调函数(callback)
  • 事件监听
  • 发布/订阅模式(又称:观察者模式)

回调函数(callback)

  • 调是一个函数被作为一个参数传递到另一个函数里,在那个函数执行完后再执行
function fn2(arg) {
    console.log(arr)
}
function fn1(callback){
  setTimeout(function () {
    // f1的任务代码
    let arr = [1,2,3]
    callback(arr);
  }, 1000);
}
// 执行
fn1(fn2) // 1秒后打印 arr
1
2
3
4
5
6
7
8
9
10
11
12

回调并不一定就是异步,他们没有直接关系,我们只是用回调来解决异步的问题

  • 同步回调
    • 很简单,就是链式同步执行,一条线同步走下去
  • 异步回调
    • 其实就是耗时的我们都扔给异步去做,做好了再通知下我们做完了,我们拿到数据继续往下走
    • 例如ajax,nodejs事件循环机制
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);   //第三个参数决定是否采用异步的方式
xhr.send(data);
xhr.onreadystatechange = function(){
    if(xhr.readystate === 4 && xhr.status === 200){
       ///do something
    }
}
1
2
3
4
5
6
7
8
var fs=require('fs');
function getData(callback){
    fs.readFile('a.txt',function(err,data){
        callback && callback(data);
    })
}
getData(function(res){
    console.log(res);
})
1
2
3
4
5
6
7
8
9

事件监听

也就是事件驱动模式,任务的执行不取决代码的顺序,而取决于某一个事件是否发生

  • 常见事件监听函数:

    • on
    • bind
    • listen
    • addEventListener
    • observe
  • 以jQuery写法为例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="box">你好</div>
    <script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.js"></script>
    <script>
        function fn (){
            console.log('div被点击了')
        }
       // 当div被点击了,就会调用fn函数,打印输出
       $('#box').on('click',fn)
    </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 当同一个element元素绑定多个事件时,只有最后一个事件会被添加
el.onclick = function(){
   //处理函数
}

// IE:attachEvent
el.attachEvent("onclick",fn);

// 标准addEventListener
el.addEvenListener("click",fn,false);
1
2
3
4
5
6
7
8
9
10

发布/订阅

  • 发布订阅模式,又称观察者模式

ES6和ES6之后

  • Promise
  • async/await

TIP

Promise的出现让我们告别回调函数,写出更优雅的异步代码

Async/Await提供了一种使得异步代码看起来像同步代码的替代方法

Promise

  • 三种状态:
    • pending-进行中:Pending既不是Resolve也不是Rejected状态;可以理解为Promise对象实例创建时候的初始状态
    • resolved-已完成:Resolve 可以理解为成功的状态
    • rejected-已失败:Rejected 可以理解为失败的状态
var promise = new Promise((resolve, reject) => {
  // 异步处理
  // 成功调用resolve 往下传递参数 且只接受一个参数
  // 失败调用reject  往下传递参数 且只接受一个参数
  if (操作成功) {
    resolve(value)
  } else {
    reject(error)
  }
})
promise.then(function (value) {
  // success
}, function (value) {
  // failure
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Promise.then

对通过new 生成的promise对象为了设置其值在resolve(成功) / reject(失败) 时调用的回调函数,可以使用promise.then()实例方法

promise.then(onFulfilled, onRejected);
1
  • resolve(成功) 时 调用onFulfilled 方法,reject(失败) 时 调用onRejected方法
  • Promise.then 成功和失败时都可以使用
  • Promise.then返回值还是一个Promise,我们可以链式编程一直点下去

Promise.catch

  • 如果出现异常的情况下可以采用promise.then(undefined,onRejected) 这种方式,只指定onRejected回调函数即可,不过针对这种情况下我们有更好的选择是使用catch这个方法
promise.catch(onRejected);
1
  • Promise.catch()方法是promise.then(undefined,onRejected)方法的一个别名,该方法用来注册当promise对象状态变为Rejected的回调函数

Promise.resolve('')

  • 将现有的东西,转成一个promise对象
  • 并且,这个对象是resolve状态(成功状态)
let p1 = Promise.resolve('小峰哥')
p1.then(res => {
    console.log(res) // 成功打印:小峰哥
})

// 等价于
let p2 = new Promise(resolve => {
    resolve('小峰哥')
})
p2.then(res=> {
    console.log(res) // 成功打印:小峰哥
})
1
2
3
4
5
6
7
8
9
10
11
12

Promise.reject('')

  • 现有的东西,转成一个reject对象
  • 并且,这个对象是reject状态(失败状态)

Promise.all

  • Promise.all可以接受一个元素为Promise对象的数组作为参数
  • 当这个数组里面所有的promise对象都变为resolve时,该方法才会返回
  • 如果有一个失败,是不会执行resolve的
var promise1 = new Promise(function(resolve){
    setTimeout(function(){
        resolve(1);
    },3000);
});
var promise2 = new Promise(function(resolve){
    setTimeout(function(){
        resolve(2);
    },1000);
});
Promise.all([promise1,promise2]).then(function(value){
    console.log(value); // 打印[1,2]
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14

注意:Promise.all方法中会按照数组的原先顺序将结果返回

比如我们需要发2个ajax请求时,不管他们的先后顺序,当这2个ajax请求都同时成功后,我们需要执行某些操作的情况下,这种情况非常适合

Promise.race

  • Promise.all 在接收到的所有对象promise都变为FulFilled或者 Rejected状态之后才会继续后面的处理
  • Promise.race的含义是只要有一个promise对象进入FulFilled或者Rejected状态的话,程序就会停止,且会继续后面的处理逻辑

Deferred

  • Deferred 包含 Promise
  • Deferred具备Promise的状态进行操作的特权方法
  • Promise代表了一个对象,这个对象的状态现在还不确定,但是未来一个时间点它的状态要么变为正常值(FulFilled),要么变为异常值(Rejected)
  • 而Deferred对象表示了一个处理还没有结束的这种事实,在它的处理结束的时候,可以通过Promise来取得处理结果

模块化

ES6之前

  • Commonjs
    • 服务端,require
  • AMD
    • require.js
    • curl.js
  • CMD
    • sea,js
  • UMD

TIP

ES6:统一了客户端和服务端规范

script引入模块

type='moduls

script添加type='moduls

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <script type="module" src="./1.js"></script>
    <!-- 
        等同于defer:
        浏览器对于带有type="module"的<script>,都是异步加载,不会造成堵塞浏览器
        即等到整个页面渲染完,再执行模块脚本
        等同于打开了<script>标签的defer属性 
    -->
    <script type="module" src="./1.js" defer></script>
    
    <!-- 也等价于 -->
    <script type="module">
        import {} from './1.js
    </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

TIP

模块化需要放到服务器环境中,file本地访问时无效的,localhost是可以的

export

  • 导出模块
export let a = 1
export let b = 2

// 或者
export {
	a,
    b
}
1
2
3
4
5
6
7
8

export default

  • export default 导出的不用花括号
export default 1
export let a = 2
export let b = 3

//  导入
import x,{a ,b} from './1.js'
1
2
3
4
5
6

import

  • 导入模块
export let a = 1
export let b = 2

// 或者
export {
	a,
    b
}

// 导入
import {a , b} from './1.js'

// 或者
import * as OneModule from './1.js'
console.log(OneModule.a)
console.log(OneModule.b)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

特点(重要)

  • import可以是相对路径,也可以是绝对路径
  • import只会导入一次,无论你引入了多少次
  • import 必须加上花括号{},除非:使用 export default导出
  • import有提升效果,import会自动提升到顶部,首先执行
  • 到处去的内容,如果包里面的内容变化,那么导出的内容对应的数据也会变化的,不像command规范会缓存!

as 别名

let a = 111
export {
    a as aaa
}

// 这里a无法使用了,要使用别名
import { aaa } from './1.js'
1
2
3
4
5
6
7
let a = 111
export {
    a 
}

// 给导出的a 起别名aaa,后面
import { a as aaa } from './1.js'
1
2
3
4
5
6
7

import()

  • 动态引入,类似nodejs的require()
  • 默认inport语法不能写在if里面的,import()可以
  • 返回值:一个Promise()对象
  • 优点:
    • (动态引入)按需加载
    • 可以写到if中
    • 路径也可以写动态的
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <script type="module">
        import('https://cdn.bootcss.com/jquery/3.4.0/jquery.js').then(res => {
            console.log($)
        })
    </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Promise.all([
    import('./1.js'),
    import('./2.js')
]),then(([mod1,mod2]) =>{
    console.log(mod1)
    console.log(mod2)
})
1
2
3
4
5
6
7

TIP

ES6模块化中默认使用严格模式 'use strict'

类(class)

  • ES6中使用class来定义类,其实ES6的类,可以认为是一种语法糖
function Person (name) {
    this.name = name
}
Person.prototype.hi = function () {
    console.log('hi,man')
}
// 或者,合并对象
Object.assign(Person.prototype, {
    hi: function () {
    	console.log('hi,man')
	}
})
// 等价于ES6的写法
class Person {
    constructor (name) {
        this.name = name
    }
    hi () {
        console.log('hi,man')
    }
}

console.log(typeof Person) // function
console.log(Person === Person.prototype.constructor) // true

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

class表达式

const P = class Person {
    constructor (name) {
        this.name = name
    }
    hi () {
        console.log('hi,man')
    }
}
let p1 = new Person();

// 立即执行类,类似匿名函数自调用
let person = new class {
  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(this.name);
  }
}('张三');

person.sayName(); // "张三"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • 注意:这个类的名字是Person,但是只有在内部使用,外部还是使用P

constructor 方法

  • constructor 方法 就是构造方法,而this关键字则代表实例对象
  • 也就是说,ES5 的构造函数,对应 ES6 的类的构造方法
  • constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法
  • 类必须调用new,而构造函数可以不调用new执行

TIP

可以直接对类进行new 操作,使用构造函数使用方法一样

使用类最直接的优点

代码层面

  • 语法糖,代码美观
  • 更偏向于大部分面向对象语言的写法

this问题

  • 在ES6中的class使用类,它的 this 使用会轻松非常多,不像我们es5中的this,使用起来非常麻烦
  • 不太需要fn.call(this, 参数1,参数2) fn.apply(this, 数组) 以及 bind(this) 来修正this指向
  • react 常在 constructor 中使用 bind 来修正 this 方法的指向,因为方法中可能用到setState方法,这个方法必须保证this的正确性

类的方法

  • 在类中的方法都是,我们在ES5中的prototype对应的方法
  • **在类的实例上面调用方法,其实就是调用原型上的方法 **
class Person {
    constructor (name) {
        this.name = name
    }
    // 
    hi () {
        console.log('hi,man')
    }
    hello () {
        console.log('hello')
    }
}
// 等价于
Person.prototype.hi = function () {
    console.log('hi,man')
}
Person.prototype.hello = function () {
    console.log('hello')
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 类的内部所有定义的方法,都是不可枚举的(non-enumerable)
  • 也就是不可以用Object.keys或者Object.getOwnPropertyNames遍历(Person.prototype

变量提升

var

  • 变量会提升,可以重复定义
  • 显示声明为局部,不显示声明为全局变量

let

  • 不支持变量提升,不可以重复定义
  • 未初始化为undefined

const

  • 不支持变量提升,不可以重复定义
  • 一单定义无法修改

function 函数提升

  • 必须是函数声明的形式,不能是函数表达式的形式
  • 函数会首先被提升,然后才是变量
console.log(foo);   
function foo(){
    console.log("函数声明");
}
var foo = "变量";
console.log(foo)
/*
	结果:
		1. 第一个console是 整个foo 函数
		2. 第二个console是 变量
	分析:
	* 变量和函数会被提升,然后函数在前
	* 注意:变量的赋值是不会被提升的
*/
// 提升之后的代码块是:
function foo(){
    console.log("函数声明");
}
var foo
console.log(foo); // 这里是第一个console,也就是这个时候foo 还是function,而不是变量,因为var foo 仅仅是声明了变量,这个时候打印foo 不会被认为是 var foo 对应的 undefined,而是 foo 函数
foo = "变量";
console.log(foo)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

class

  • ES6 不会把类的声明提升到代码头部
  • 这种规定的原因与类的继承有关,必须保证子类在父类之后定义
let Foo = class {};
class Bar extends Foo {
}
// 代码不会报错,因为class不会被提升
// 如果存在class的提升,上面代码就会报错,因为class会被提升到代码头部,而let命令是不提升的,所以导致Bar继承Foo的时候,Foo还没有定义
1
2
3
4
5

取值函数(getter)和存值函数(setter)

class Person {
    constructor (name) {
        this.name = name
    }
    set age (val) {
        console.log('set', val)
    }
    get age () {
        console.log('get')
        return456}
    hi () {
        console.log('hi,man')
    }
}

let p = new Person()
p.age = 123 // set 123
console.log(p.age) // 先输出get 在输出 ---456
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

TIP

  • set 必须有参数,get不允许有参数
  • get的返回值就是我们获取的值

属性表达式

类似属性名可以用表达式

let a = 'fn'
class Person {
    constructor (name) {
        this.name = name
    }
   	// 这里属性表达式,还是挺常用的
    [a] () {
        console.log('我是fn')
    }
}

let p = new Person()
p.fn() // 我是fn
p[a]() // 我是fn
p.a() // 报错,p.a is not a function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

静态方法

类身上的方法,类可以直接调用,实例不可以调用,使用static修饰

class Person {
    constructor (name) {
    }
    static fn () {
        console.log('static方法')
    }
}

let p = new Person()
Person.fn() // console.log('static方法')
p.fn() // 报错,是不允许的-p.fn is not a function
1
2
3
4
5
6
7
8
9
10
11

继承 extends

类可以通过extends关键字来实现继承

class Person {
    constructor (name) {
        this.name = name || '无名氏'
    }
    
    hi () {
        console.log(this.name, '你好')
    }
    // 静态方法
    static say () {
        console.log('say')
    }
}

class Student extends Person {
    // 注意这里是没有调用 constructor 方法的
}

let stu1 = new Student('张三')
Student.say() // say  静态方法被继承了
stu1.hi() // 张三 你好  实例方法也被继承了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • 这里的stu1 继承 Person 类,可以调用Person方法

子类添加 constructor 方法

  • 子类必须在constructor 方法中调用 super方法,否则就会报错
  • 原因:
    • 子类自己的this对象,必须先通过弗雷的构造函数塑造,得到与父类同样的实例属性和方法,然后再进行自己的操作
    • 如果不调用super方法,子类就得不到自己的this
class Student extends Person {
    constructor (name) {
        // 这里关键 super
        super(name)
    }
    hello () {
        console.log('hello')
    }
}
1
2
3
4
5
6
7
8
9

TIP

子类会默认添加constructor方法,即使没有显式定义,任何一个子类也都是有constructor方法的

Object.getPrototypeOf()

用来判断一个类是否继承了另一个类,原因是这个方法可以从子类上获取父类

Object.getPrototypeOf(Student) === Person // true
1

super 关键字

  • 作为函数使用
  • 作为对象使用

super 函数

super作为函数调用时,代表父类的构造函数,子类创建的时候要求调用父类的构造函数

class Person {
    constructor (name) {
        this.name = name || '无名氏'
    }
    // 静态方法
    static say () {
        console.log('say')
    }
}

class Student extends Person {
    constructor (name) {
        // 这里关键 super
        super(name)
    }
}

let stu1 = new Student('张三')
Student.say()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 注意:
    • 虽然这里的super代表父类构造函数,但是这里的this代表的却是子类
    • 也就是这里的super(),相当于 Person.prototype.constructor.call(this)
    • 修改this的指向

TIP

super()只能用在子类的构造函数之中,用在其他地方就会报错

super 对象

super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

class Person {
    constructor (name) {
        this.name = name || '无名氏'
    }
    
    fn1 () {
        console.log('fn1')
    }

    static fn2 () {
        console.log('fn2')
    }
}

class Student extends Person {
    constructor (name) {
        super(name)
    }
    // 普通方法
    fn3 () {
        super.fn1()
        super.fn2()
    }
    // 静态方法
    static fn4 () {
        super.fn2()
        super.fn1()
    }

}

let stu1 = new Student('张三')
stu1.fn3() // 先输出fn1,然后报错,也就是无法调用fn2,无法调用静态方法
Student.fn4() // 先输出fn2,然后报错,也就是无法调用fn1,无法调用普通方法
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

原生构造函数的继承

  • 原生构造函数

    • Boolean()
    • Number()
    • String()
    • Array()
    • Date()
    • Function()
    • RegExp()
    • Error()
    • Object()
  • ES5是无法实现原生构造函数基础的,即使我们改变this指向,也会被忽略

  • ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承

Symbol

  • ES6引入了一种新的原始数据类型
  • 其余数据类型是:
    • undefined
    • null
    • 布尔值(Boolean)
    • 字符串(String)
    • 数值(Number)
    • 对象(Object)
  • 特点:
    • 独一无二,同样创建方法创建出来的Symbol也是不相等的
    • typeof 结果是Symbol

Symbol创建

  • 调用构造函数Symbol()即可创建
  • 注意不允许使用new,因为他是原始数据类型,不是对象
let sym = Symbol('symb')
console.log(sym) // Symbol(symb)

let sym1 = Symbol('symb1')
console.log(sym1) // Symbol(symb1)

let s1 = Symbol('s')
let s2 = Symbol('s')
console.log(s1==s2) // false,不相等,即使参数一样
1
2
3
4
5
6
7
8
9
  • symbol 函数的参数是一个字符串,用于添加对Symbol实例进行描述区分
  • 如果参数是对象,那么就会调用该对象的toString方法,先转化为字符串,然后生成Symbol的值

Symbol常用场景

  • 作为对象的属性,防止对象属性名重复,会被覆盖掉
  • 单利模式创建的时候,推荐使用

Symbol常见坑

  • Symol 作为属性名,遍历对象的时候是不会出现的:
    • for in
    • for of
    • Object.keys()
    • Object.getOwnPropertyNames()
    • JSON.stringify()

Symbol常见方法

Symbol.for()

  • 接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值
  • 如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局
  • 区别:
    • Symbol.for()Symbol()这两种写法,都会生成新的 Symbol
    • 它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值
    • 比如,如果你调用Symbol.for("cat")30 次,每次都会返回同一个 Symbol 值,但是调用Symbol("cat")30 次,会返回 30 个不同的 Symbol 值

Symbol.keyFor()

  • Symbol.keyFor()方法返回一个已登记的 Symbol 类型值的key

generator 函数

  • Generator 函数是 ES6 提供的一种异步编程解决方案

语法形式

function * fnName () {
    yield // 只能和generator 配合使用
}
1
2
3

简单示例

// 定义函数
function * gen () {
    yield 'nihao'
    yield 'zhangsan'
    return 'gen-fn'
}

// 调用
let g1 = gen()
console.log(g1) // 直接执行就是一个对象
console.log(g1.next()) // { value: 'nihao', done: false }
console.log(g1.next()) // { value: 'zhangsan', done: false 
console.log(g1.next()) // { value: 'gen-fn', done: true }
1
2
3
4
5
6
7
8
9
10
11
12
13

async 和 await

Set 和 WeakSet

Map 和 WeakMap

Proxy

Reflect

ES7及其之后

面试常见