ES6
概述
ECMAScript, JavaScript, NodeJS, 它们的区别是什么?
ECMAScript:简称ES,是一个语言标准(循环,判断,变量,数组等数据类型)
JavaScript: 运行再浏览器端的语言,该语言使用ES标准.
ES + Web api = JavaScript
Nodejs: 运行在服务器端的语言,该语言使用ES标准.
ES + node api = JavaScript
无论JavaScript,还是Nodejs,它们都是ES的超集
ECMAScript有哪些关键版本
ES3.0: 1993
ES5.0: 2009
ES6.0: 2015, 从该版本之后,不再使用数字作为编号,而使用年份,每年发布一个新版本
ES7.0: 2016
为什么ES6如此重要
ES6解决了JS无法开发大型应用的语言层面的问题.
如何应对兼容性问题
之后的课程会介绍如何解决
学习本课程需要哪些前置知识
HTML, CSS, JavaScript
这套课程难不难?
块级绑定
使用var
声明变量
允许重复声明导致数据被覆盖
1
2
3
4
5
6
7
8
9
10var a = 1;
function print() {
console.log(a);
}
// 假设这里有一千行代码
var a = 2;
print(); // 2变量提升: 怪异的数据访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22if (Math.random() < 0.5) {
var a = 'abc';
console.log(a);
} else {
console.log(a);
}
console.log(a);
// abc
// abc
// 或
// undefined
// undefined
// 因为变量提升,所以实际如下
var a;
if (Math.random() < 0.5) {
a = 'abc';
console.log(a);
} else {
console.log(a);
}变量提升: 闭包问题
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
34var container = document.querySelector('.container');
for(var i=1; i<=10; i++) {
var btn = document.createElement('button');
btn.innerHTML = i;
container.appendChild(btn);
btn.onclick = function() {
console.log(i);
}
}
// 发现无论点击哪一个按钮,都输出11
// 因为实际上是这样
var i;
var container = document.querySelector('.container');
for(i=1; i<=10; i++) {
var btn = document.createElement('button');
btn.innerHTML = i;
container.appendChild(btn);
btn.onclick = function() {
console.log(i);
}
}
// 过去为了解决这样的问题,需要使用立即执行函数
var container = document.querySelector('.container');
for (var i = 1; i <= 10; i++) {
var btn = document.createElement('button');
btn.innerHTML = i;
container.appendChild(btn);
(function (i) {
btn.onclick = function () {
console.log(i);
};
})(i);
}全局变量挂载到全局对象: 全局对象成员污染问题
1
2var abc = "123";
console.log(window.abc);
使用let
声明变量
let
声明的变量,不会挂载到全局对象
let
声明的变量,不允许在当前作用域重复声明(包括块级作用域
)
块级作用域: 代码执行时遇到花括号,产生块级作用域,遇到结束花括号,块级作用域销毁.
let
声明的变量,不能在初始化之前使用(存在暂时性死区(TDZ)
).当代码运行到该变量的初始化代码时,会从暂时性死区中移除.
在循环中,用let
声明的循环变量,会特殊处理,每次进入循环体,都会开启一个新的作用域,并且将循环变量绑定到该作用域.每次循环使用的是一个全新的作用域.
在循环中,使用let声明的循环变量,在循环结束后会销毁.
1 | let container = document.querySelector('.container'); |
使用const
声明常量
和let
基本相同,除了const
必须初始化,且值不能修改.
注意如果用const声明对象类型,只要地址不变就可以.地址里面的内容可以变
在实际开发中,尽量使用const
,当需要改变时,再使用let
字符串和正则表达式
更好的Unicode
支持
早期,由于存储空间宝贵,Unicode
使用16位二进制来存储文字.我们将一个16位的二进制编码叫做一个码元(code Unit
)
后来,由于技术的发展,Unicode
对文字编码进行了扩展,将某些文字扩展到了32位(占用两个码元),并且,将某个文字对应的二级制数字叫做码点(Code Point
)
1 | console.log('𠮷'.length) |
ES6为了解决这个困扰,为字符串提供了方法:codePointAt(n)
,可以得到序号为n
的码元的码点(往后判断).
1 | const text = '𠮷'; |
同时,ES6为正则表达式添加了一个flag
:u
,如果添加了该配置,匹配的时候使用码点匹配
更多的字符串API
一下均为字符串的实例方法
includes
1 | str.includes('substr', startIndex) |
判断字符串中是否包含指定的子字符串
startWith
判断是否以substr
开始
1 | str.startWith('substr') |
endWith
判断是否以substr
结束
1 | str.endWith('substr') |
repeat
返回一个字符串重复n次的结果
1 | str.repeat(n) |
*正则中的粘连标记
标记名: y
含义: 匹配时,完全按照正则对象中的lastIndex
位置开始匹配,并且匹配的位置必须在lastIndex
位置
1 | const text = "Hello World!!!" |
模板字符串
ES6之前处理字符串繁琐的两个方面:
- 多行字符串
- 字符串拼接
ES6提供了模板字符串的写法用``来包裹字符串
如果要在字符串中使用表达式,则在``中用${}包裹即可
*模板字符串标记
标记是一个函数,参数如下
- 参数1: 被插值分割的字符串的数组
- 剩余参数: 插值的值
1 | const love1 = '苹果'; |
用处,转义字符当普通字符输出,可以使用String.raw
模板字符串标记
1 | console.log('abc\nbcd'); |
下面这种情况,当用户在text中写入标签时,如<h1>jdsfj</h1>
,就会导致输出为标题.我们本意想让他输出纯文本.
1 | <!DOCTYPE html> |
则进行如下改造
1 | <!DOCTYPE html> |
函数
参数默认值
在书写时,直接给形参赋的值就是默认值
对arguments的影响
非严格模式下,在函数内部修改形参,会导致arguments
的值也发生改变.
1 | function sum(a, b) { |
但是在严格模式下,在函数内部修改形参,arguments
的值不会发生改变
1 |
|
只要给函数加上参数默认值,该函数会自动变成严格模式下的规则:arguments
和形参脱离
留意暂时性死区
形参和ES6中的let
和const
一样,具有作用域,并且根据参数的声明顺序,存在暂时性死区(TDZ)
1 | function test(a, b = a) { |
1 | function test(a = b, b) { |
剩余参数
使用arguments
的缺陷
- 如果和形参配合使用,容易导致混乱
- 从语义上,使用
arguments
获取参数,由于形参缺失,无法从函数定义上理解函数的真实意图
ES6的剩余参数,专门用于收集末尾的所有参数到一个形参数组中.
一个函数最多只能有一个剩余参数,而且必须是最后一个形参
展开运算符
使用...要展开的东西
函数柯里化
用户固定某个函数前面的参数,得到一个新的函数,新的函数调用时,接收剩余参数
1 | function curry(func, ...args) { |
明确函数的双重用途
ES6提供了一个特殊的API,可以使用该API在函数内部,判断该函数是否使用了new
来调用
1 | new.target |
如果没有使用new
来调用函数,则返回undefined
如果使用new
调用函数,则得到的是new
关键字后面的函数本身
箭头函数
回顾: this
指向
- 通过对象调用函数,
this
指向调用对象 - 直接调用函数,
this
指向全局对象 - 通过
new
调用函数,this
指向新创建的对象 - 通过
apply,call,bind
调用函数,this
指向指定的数据 - DOM事件函数,
this
指向事件源
1 | const obj = { |
在过去需要通过使用闭包来解决
1 | const obj = { |
现在可以直接使用箭头函数.
使用语法
1 | const func = (arg) => { |
注意细节
箭头函数没有this,arguments,new.target
,它只能继承定义位置的执行上下文的对应变量,与如何调用无关
应用场景
- 事件处理函数
- 异步处理函数
- 临时函数
- 为了绑定外层this
对象
新增的对象字面量语法
成员速写
如果对象字面量初始化时,如果一个成员的值来源于同名变量的值,则可以直接简写
1 | const name = "zhangsan" |
方法速写
对象字面量初始化时,方法可以省略冒号
和function
关键字.
1 | const ojb = { |
计算属性名
有的时候,初始化对象时,某些属性名可能来源于某个表达式的值,在ES6,可以使用中括号来表示该属性名是通过计算得到的.
1 | const pro1 = 'name'; |
Object的新增API
以下都是静态方法
Object.is
用于判断两个对象是否相等,基本上和严格相等一致
1 | console.log(NaN===NaN) // false |
Object.assign
1 | const obj = Object.assign(obj1,obj2); |
Object.getOwnProperNames的枚举顺序
Object.getOwnPropertyNames
方法之前就存在,只不过,官方没有明确要求,对属性的顺序如何排列,并没有明确要求.
ES6规定了该方法返回的数组排序方式与for...in
循环或Object.keys()
方法获取的顺序一致.
根据现代 ECMAScript 规范,遍历顺序是明确定义的,并且在实现之间是一致的。在原型链的每个组件中,所有非负整数键(可以是数组索引的键)将首先按值升序遍历,然后按属性创建的时间升序遍历其他字符串键。
Object.setPrototypeOf
用于设置某个对象的隐式原型
1 | Object.setPrototypeOf(obj1, obj2) |
面向对象简介
面向对象: 一种编程思想,和具体语言无关.
对比面向过程:
- 面向过程:思考的切入点是功能的步骤
- 面向对象:思考的切入点是对象的划分
类: 构造函数的语法糖
传统构造函数的问题
- 属性和原型方法定义分离,降低了可读性
- 原型成员可以被枚举
- 默认情况下,构造函数仍然可以被当作普通函数使用
类的特点
- 类声明不会被提升,与
let
和const
一样,存在暂时性死区 - 类中所有代码均在严格模式下执行
- 类的所有方法都是不可枚举的
- 类的所有方法内部都无法被当作构造函数使用
- 类的构造器必须使用
new
来调用
类的其他书写方式
可计算的成员名
1 | const pro1 = 'name' |
getter和setter
1 | class Animal { |
静态成员
使用static
关键字定义的成员即静态成员
1 | class Animal { |
字段初始化器(ES7)
字段初始化器相当于在constructor
中加上这些内容
1 | class Animal { |
- 使用
static
的字段初始化器,添加的是静态成员. - 没有使用
static
的字段初始化器,添加的成员位于对象上,注意,不是位于原型上 - 箭头函数在字段初始化器位置上,this指向当前对象
1 | class Animal { |
类表达式
1 | const A = class { |
装饰器(ES7)
类的继承
如果两个类A和B,如果可以描述为B是A,则A和B形成继承关系
如果B是A,则:
- B继承自A
- A派生B
- B是A的子类
- A是B的父类
如果A是B的父类,则B会自动拥有A中所有实例成员
新的关键字
extends
:继承,用于类的定义super
:- 直接当作函数调用,当作父类的构造函数
- 如果定义了construcor,且该类是子类,必须在访问子类的constructor的this之前,调用super
- 如果子类不写constructor,则会自动
- 当作对象调用
- 在实例方法中,指向父类原型
- 在静态方法中,指向父类本身
- 直接当作函数调用,当作父类的构造函数
抽象类
1 | class Animal { |
解构
符号
普通符号
符号是ES6新增的一个数据类型,它通过使用函数Symbol(符号描述)
来创建
符号设计的初衷,是为了给对象设置私有属性
私有属性:只能在对象内部使用,外面无法使用.
符号具有以下特点:
- 没有字面量
- 使用
typeof
得到的类型是symbol
- 每次调用
Symbol
得到的符号永远不相等,无论符号名是否相同 - 符号可以作为对象的属性名存在,这种属性称之为符号属性
- 开发者可以通过精心的设计,让这些属性无法通过常规方式被外界访问.
- 符号属性是不能被枚举的,因此,在
for-in
循环中无法读取到符号属性,Object.keys
方法也无法读取到符号属性 Object.getOwnPropertyNames
尽管可以得到所有无法枚举的属性,但是仍然无法读取到符号属性- ES6新增了
Object.getOwnPropertySymbols
方法,可以读取符号
- 符号无法被隐式转换,因此不能被用于数学运算,字符串拼接或其他隐式转换的场景,但符号可显示地转换为字符串,通过
String
构造函数进行转换即可.console.log
之所以可以输出符号,是因为它在内部进行了显示转换.
共享符号
根据某个符号描述能够得到同一个符号
1 | Symbol.for("符号描述") // 获取共享符号 |
知名(公共,具名)符号
知名符号是一些具有特殊含义的共享符号,通过Symbol的静态属性得到
ES6延续了ES5的思想:减少魔法,暴露内部实现!
因此,ES6用知名符号暴露了某些场景的内部实现
Symbol.hasInstance
该符号用于定义构造函数的静态成员,它将影响
instanceof
的判定1
2
3obj instanceof A
// 等效于
A[Symbol.hasInstance](obj)Symbol.isConcatSpreadable
该符号会影响到数组的cancat方法
1
2
3
4const arr = [3];
const result = arr.concat(56, [5, 6, 7, 8]);
console.log(result)
// [3, 56, 5, 6, 7, 8]Symbol.toPrimitive
该知名符号会影响类型转换的结果
1
2
3
4
5
6
7
8
9
10
11
12const obj = {
a: 1,
b: 2,
};
// 会依次尝试以下方法,直达obj转化为基本类型
console.log(obj[Symbol.toPrimitive]) // undefined
console.log(obj.valueOf()) // {a: 1, b: 2}
console.log(obj.toString()) // [object Object]
console.log(obj + 123); // [object Object]1231
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const obj = {
[Symbol.toPrimitive](hint) {
if (hint === "number") {
return 42; // 数值上下文
}
if (hint === "string") {
return "Hello"; // 字符串上下文
}
return "default"; // 默认上下文
}
};
console.log(+obj); // 42
console.log(`${obj}`); // "Hello"
console.log(obj + ""); // "default"Symbol.toStringTag
该知名符号会影响
Object.prototype.toString
的返回值
- 其他知名符号
迭代器和生成器
迭代器
背景知识
什么是迭代
从一个数据集合中按照一定的顺序,不断取出数据的过程
迭代和遍历的区别?
迭代强调的是依次取数据,并不保证取多少,也不保证把所有的数据取完
遍历强调的是完整性,要把整个数据依次全部取出
迭代器
对迭代过程的封装,在不同的语言中有不同的表现形式,通常为对象
迭代模式
一种设计模式,用于统一迭代的过程,并规范了迭代器的规格:
- 迭代器应该具有得到下一个数据的能力
- 迭代器应该具有判断是否还有后续数据的能力
JS中的迭代器
JS规定,如果一个对象具有next()
方法,并且该方法返回一个对象,该对象的格式如下
1 | {value: 值, done:是否迭代完成} |
则认为该对象是一个迭代器
含义:
next
方法:用于得到下一个数据- 返回的对象
value
下一个对象的数据done
是否结束
1 | const arr = [1, 2, 3, 4, 5]; |
1 | function createFeivoIterator() { |
可迭代协议与for-of
循环
概念回顾
- 迭代器(iterator): 一个具有next方法的对象, next方法返回下一个数据并且能指示是否迭代完成.
- 迭代器创建函数(iterator creator): 一个返回迭代器的函数
可迭代协议
ES6规定,如果一个对象具有知名符号属性Symbol.iterator
,并且属性值是一个迭代器创建函数,则该对象是可迭代的(iterable)
思考:如何知晓一个对象是否是可迭代的?
思考:如何遍历一个可迭代对象?
for-of
循环
for-of
循环用于遍历可迭代对象,格式如下
1 | //迭代完成后循环 |
展开运算符与可迭代对象
展开运算符可以将可迭代对象展开,这样可以轻松将其转换为数组
生成器
- 什么是生成器?
生成器是通过构造函数Generator
创建的对象,生成器既是一个迭代器,同时又是一个可迭代对象.
- 如何创建生成器?
生成器的创建,必须使用生成器函数(Generator Function)
- 如何书写一个生成器函数呢?
1 | function* mthod() { |
1 | function* test() { |
- 生成器函数内部是如何执行的?
生成器函数内部是为了给生成器提供迭代数据
每次调用生成器的next方法,将导致生成器函数运行到下一个yield关键字位置
yield是一个关键字,该关键字只能在生成器函数内部使用,表达产生一个迭代数据.
有哪些需要注意的细节
- 生成器函数可以有返回值,返回值表示第一次
done: true
时对应的value
值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function* test() {
yield 1;
yield 2;
return 4;
yield 3;
}
const generator = test();
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 4, done: true }
// { value: undefined, done: true }- 调用生成器的next方法时,可以传递参数,传递的参数会交给
yield
表达式的返回值 - 第一次调用next方法时,传参没有任何意义
在生成器函数内部还可以调用其他生成器函数,但是要注意加上
*
1
2
3
4
5
6
7
8
9
10function* t1() {
yield "a"
yield "b"
}
function* test() {
yield* t1();
yield 1;
yield 2;
yield 3;
}
- 生成器函数可以有返回值,返回值表示第一次
生成器的其他API
- return方法: 调用该方法,可以提前结束生成器函数,从而让整个迭代过程结束
- throw方法: 调用该方法,在生成器中产生一个错误
生成器的应用-异步任务控制
ES6之后有了Promise,但是async
和await
要ES7才有.
1 | function* task() { |
代理与反射
属性描述符
Property Descriptor
属性描述符,用于描述一个属性的相关信息.
通过Object.getOnwPropertyDescriptor(对象,"属性名")
来得到某对象的某属性的属性描述符
1 | const obj = { |
通过Object.getOnwPropertyDescriptors(对象)
来得到该对象的所有属性的属性描述符
1 | const obj = { |
value
: 属性值configurable
: 该属性描述符是否可以被修改enumerable
: 该属性是否可以被枚举,for-in
,Object.keys()``Object.values()
等方法writable
: 该属性的值是否可以被修改
如果需要为某个对象添加属性
或修改属性
时,配置其属性描述符,可以使用下面的代码
1 | Object.defineProperty(对象,"属性名",属性描述符) |
1 | const obj = { |
也可以使用下面的代码为多个属性配置属性描述符
1 | const obj = { |
存取器属性
注意(
get
,set
)是不能与(value
,writable
)属性共存的,只能配置两者之一
属性描述符中,如果配置了get
和set
中的任何一个,则该属性,不再是一个普通属性,而是变成了存取器属性.
get
和set
配置均为函数,如果一个属性是存取器属性,则读取该属性时,会运行get方法,将get方法的返回值作为属性值;如果给该属性赋值,则会运行set方法
1 | const obj = { |
1 | const obj = { |
存取器属性存在的最大意义,在于可以控制属性的读取和赋值
Reflect
Reflect是什么?
Reflect
是一个内置的JS对象,它提供了一系列方法,可以让开发者通过调用这些方法,访问一些JS底层功能由于它类似于其他语言的反射,因此取名为
Reflect
它可以做什么?
使用Reflect可以实现诸如
属性的赋值与取值
,调用普通函数
,调用构造函数
,判断属性是否在对象中
等等功能这些功能不是已经存在了吗?为什么还需要Reflect实现一次?
有一个重要的理念,在ES5就被提出:较少魔法,让代码更加纯粹
有这种里面很大程度上是受到函数式编程的影响
ES6进一步贯彻了这种理念,它认为,对属性内存的控制,原型链的修改,函数的调用等等,这些都属于底层实现,属于一种魔法,因此,需要将它们提取处理啊,形成一个正常的API,并高度聚合到某个对象中,于是,就造就了
Reflect
对象因此,你可以看到Reflect对象中的很多API都可以使用过去的某种语法或其他API实现
它里面到底提供了哪些API呢?
Reflect.set(target,propertyKey, value)
:设置对象target
的属性propertyKey
的值为value
,等同于给对象的属性赋值Reflect.get(target, propertyKey)
:读取对象target
的属性propertyKey
的值为,等同于读取对象的属性值Reflect.apply(target, thisArgument, argumentsList)
:调用一个指定的函数,并绑定this和参数列表.等同于函数调用Reflect.deleteProperty(target, propertyKey)
: 删除一个对象的属性Reflect.defineProperty(target, propertyKey, attribute)
:类似于Object.defineProperty
,不同的是如果配置出现问题,返回false
而不是报错Reflect.construct(target, argumentsList)
:用构造函数的方式创建一个对象Reflect.has(target, propertyKey)
:判断一个对象是否拥有一个属性- 其他API: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
Proxy
代理:提供了修改底层实现的方式
1 | new Proxy(target, handler) |
1 | const obj = { |
1 | const obj = { |
应用-观察者模式
有一个对象,是观察者,它用于观察另外一个对象的属性值变化,当属性值变化后会收到一个通知,可能会做一些事情.
在过去,我们可以使用如下模式,但是可能导致空间浪费和数据不一致
1 |
|
使用代理可以如下
1 |
|
应用-偷懒的构造函数
1 | class User { |
应用-可验证的函数参数
1 | function sum(a, b) { |
增强的数组功能
新增的数组API
静态方法
Array.of(...args)
:使用指定的数组项创建一个新数组Array.from(arg)
:通过给定的类数组或可迭代对象,创建一个新的数组
实例方法
find(callback)
:用于查找满足条件的第一个元素findIndex(callback)
:用于查找满足条件的第一个元素的下标fill(data)
:用指定的数据填充满数组所有的内容copyWithin(target, start?, end?)
:在数组内部完成复制includes(data)
判断数组是否包含某个值,使用sameValueZero
比较算法.与Object.is
的区别是,sameValueZero
认为+0
和-0
相等.
类型化数组
JS
中所有的数字,均使用双精度浮点数保存(64bit, 1bit符号位, 11bit阶码, 52位尾数)
类型化数组:用于优化多个数字的存储
具体分为
- Int8Array: 8位有符号整数
- UInt8Array: 8位无符号整数
- Int16Array
- UInt16Array
- Int32Array
- UInt32Array
- 如何创建类型化数组
1 | const arr = new Int8Array(10); |
- 得到长度
1 | const arr = new Int8Array(10); |
- 其他的用法跟普通数组一致,但是:
- 不能增加和删除数据,类型化数组的长度固定
- 一些返回数据的方法,返回的数组是同类型化的数组
ArrayBuffer
ArrayBuffer
:一个对象,用于存储一块固定内存大小的数据.
1 | new ArrayBuffer(字节数) |
1 | const obj = new ArrayBuffer(10); |
可通过属性byteLength
得到字节数,可以通过方法slice
得到新的ArrayBuffer
,
读写ArrayBuffer
- 使用
DataView
1 | const obj = new ArrayBuffer(10); |
- 使用类型化数组
实际上,每一个类型化数组都对应一个ArrayBuffer
,如果没有手动指定ArrayBuffer
,类型化数组创建时,会新建一个ArrayBuffer
制作黑白图片
1 |
|