文章输出主要来源:拉勾大前端高新训练营(链接)。小哥哥小姐姐请不要嫌弃啰嗦,下面肯定都是干货。
1. 简介
ECMAScript是JavaScript(语言层面)的规范,JavaScript是ECMAScript的一种实现。ECMA-262规范
JavaScript的构成
- 浏览器端:
ECMAScript
,DOM
,DOM
- Node:
ECMAScript
+ Node API (fs、net、etc.
)
2. 作用域与常量
2.1 块级作用域let
ES6之前的函数作用域与闭包
JavaScript最初只有全局作用域与函数作用域,变量通过var
定义。在缺乏块级作用域时,使用for循环等易于出错,需借助闭包技术进行变量的临时保存。
闭包的产生也与函数作用域离不开关系,外层函数中定义的变量可以被内层函数所访问,外层函数返回一个新的函数,在内层函数中使用外层函数所定义的变量,执行外层函数后,其中定义的变量由于被使用并未被内存释放,因而再运行内层函数仍可访问该变量,通过函数作用域保存变量的技术也就是闭包的技术。
例:
// example1 闭包的普通使用
function outer() {
var hello = 'hello ';
return function(name) {
console.log(hello + name);
}
}
const sayHello = outer();
// outer已经运行过,再sayHello,仍可访问到outer中定义的hello变量
sayHello('world') // hello world
// example2 循环注册事件
function memoryIndex(index) { // index为外层函数隐式定义的变量
return function() {
console.log(index)
}
}
var domList = []; // 模拟dom元素
// 使用闭包
for (var i = 0; i < 10; i++) {
var click = memoryIndex(i);
domList.push({click: click});
}
domList[5].click(); // 5
domList[2].click(); // 2
domList[8].click(); // 8
// 不使用闭包,变量没有被暂存,最终打印全局定义的i,全部为10
var domList2 = []
for (var i = 0; i < 10; i++) {
var click = function() {
console.log(i)
};
domList2.push({click: click});
}
domList2[5].click(); // 10
domList2[2].click(); // 10
domList2[8].click(); // 10
ES6带来的let
let
可定义块级作用域变量,在一个{}
块内可以有自己的作用域变量,如上方不使用闭包的代码改写为以下方式:
let domList2 = []
for (let i = 0; i < 10; i++) {
var click = function() {
console.log(i)
};
domList2.push({click: click});
}
domList2[5].click(); // 8
domList2[2].click(); // 2
domList2[8].click(); // 5
babel repl代码转换工具可以将ES6+语法转换为ES5, 通过转换过后的代码可以发现,转换过后的代码也是通过闭包进行变量的暂存。
for循环中的作用域
for循环
中的作用域为两层。for (let i = 0; i < 3; i++)
括号中的i为第一层,在{}
循环体中再定义i
变量,该变量为第二层作用域,不会与外层i有冲突
for(let i = 0; i < 3; i++) {
let i = 100;
console.log(i);
}
// 输出 100 100 100
变量提升
var
定义的变量存在变量提升,在声明变量前使用变量,变量值为undefined
let
定义的变量不存在变量提升,在生命变量之前使用变量会报错变量未定义
2.2 const
常量定义
const
在let的基础上增加了只读特性。通过const
定义的变量值不能被修改(若为引用变量,不能修改其指针指向,内部属性仍可被修改)。
const
定义变量必须初始化。
2 解构
解构包含数组解构
,对象结构
, 通过解构赋值可以简化代码编写
2.1 数组解构
- 通过位置解构出在数组中对应位置的变量
const arr = [1,2,3,4];
const [first, , third] = arr;
console.log(first, third); // 1 3
- 剩余变量通过
...
rest语法结构出一个剩余变量的数组
const arr = [1,2,3,4];
const [first, ...rest] = arr;
console.log(first, rest); // 1 [ 2, 3, 4 ]
- 超出数组长度的解构变量会被赋值为undefined
const arr = [1,2];
const [first, second, third] = arr;
console.log(first, second, third); // 1 2 undefined
- 解构默认值,通过给解构变量赋值设置默认值
const arr = [1,2];
const [first, second, third = 'third'] = arr;
console.log(first, second, third); // 1 2 third
- 设置默认值后,对应位置为undefined的元素不会重新赋值给该变量
// example1
const arr = [1,null, undefined, false];
const [first, second, third = 'third', forth] = arr;
console.log(first, second, third, forth); // 1 null third false
2.2 对象结构
- 通过属性字面量结构对应属性的值
const obj = {
hello: 'hello',
world: 'world'
}
const { hello, world } = obj;
console.log(hello, world) // hello world
-
剩余变量
...
rest语法结构剩余的属性拼出新对象 -
未定义的属性被结构出来为
undefined
-
结构默认值,与数组使用方式一致
-
重命名,可以通过重命名对解构的变量重新命名
const obj = { hello: 'hello', world: 'world' } const { hello: h, world: w } = obj; console.log(h, w) // hello world
3 字符串扩展
3.1 模板字符串
基础使用
通过**** 反引号定义模板字符串,其中可以使用换行,单引号,双引号等。通过
${variable}`插值表达式的方式可嵌入合法js语句
高级使用
带标签的模板字符串: 使用带标签的模板字符串,需要先定义一个函数(标签函数)
// 参数列表为:第一个是通过模板字符串中变量对模板字符串进行分割,产生的字符串数组,其余参数依次为模板字符串中的变量
function tagFunction(strings, name, age) {
console.log(strings, name, age);
const sex = gender ? 'man' : 'woman';
return strings[0] + name + strings[1] + sex + strings[2];
}
const name = 'tom', gender = true;
// 使用方式,标签函数+模板字符串。结果为标签函数的返回值。
const result = tagFunction`hello, ${name} is a ${gender}.`
console.log('result:', result)
// [ 'hello, ', ' is a ', '.' ] tom true
// result: hello, tom is a man.
3.2 字符串扩展方法
includes(subStr)
: 字符串是否包含subStr
startsWith(subStr)
: 字符串是否已subStr
开头endsWith(subStr)
: 字符串是否已subStr
结尾
4 函数扩展
4.1 参数默认值
参数默认值与结构默认值的使用方法类似,。
注意:
在使用函数默认值时候,该参数必须在参数列表末尾
function sayHello (name = 'Tom') {
console.log('hello ' + name);
}
sayHello(); // hello Tom
sayHello('Jerry'); // hello Jerry
4.2 剩余参数
剩余参数的使用与数组解构中的剩余变量结构用法一致
function test(first, ...rest) {
console.log(first, rest);
}
test(1,2,3); // 1 [2, 3]
4.3 展开运算符
...
不仅可以用于剩余参数,也可以进行展开操作。
可以展开数组,对象。
对于函数参数可以将数组展开作为函数的参数,将数组内的元素依次展开放入函数参数列表、
function test(first, second, ...rest) {
console.log(first, second, rest);
}
const params = [1,2,3,4,5,6]
test(...params) // 1 2 [3, 4, 5, 6]
4.4 箭头函数
函数体不使用花括号
函数体不使用花括号时,只能是个JavaScript表达式,其值为函数最终返回值
const add = (a, b) => a + b;
函数体使用花括号
函数体使用花括号时, 返回值要用return 自己写
const add = (a, b) => {
return a + b;
}
箭头函数不会改变this的指向
箭头函数中没有this机制,不会改变this指向。
const person = {
name: 'tom',
sayHello: function() {
console.log(`hello ${this.name}`)
},
sayHello2: () => {
console.log(`hello ${this.name}`)
}
}
person.sayHello() // hello tom
person.sayHello2() // hello undefined
5 对象扩展
5.1 对象字面量增强
1. 属性名与变量名相同可进行省略
普通属性
const name = 'tom', age = 18;
const person = {
name,
age
}
console.log(person) // { name: "tom", age: 18 }
方法
const name = 'tom', age = 18;
const person = {
name,
age,
sayHello (name) {
console.log(`hello ${name}`);
}
}
person.sayHello('jerry') // hello jerry
2. 计算属性
通过[expression]
动态添加对象属性名,表达式计算的结果即为对象的属性名
const obj = {
['hello_' + 'world']: 'hello world'
}
obj.hello_world // "hello world"
5.2 Object.assign
Object.asign
可以对对象进行浅拷贝,与...
展开运算符的作用一致。
const obj1 = { hello: 'hello' }
const obj2 = { world: 'world' }
const result1 = Object.assign({}, obj1, obj2);
const result2 = {...obj1, ...obj2};
console.log(result1, result2) // {hello: "hello", world: "world"} {hello: "hello", world: "world"}
5.3 Object.is
Object.is()
判断两个变量是否相等。
相等符号不能解决的问题:
+0 === -0
: trueNaN === NaN
: false
Object.is()
添加了这些值的比较问题
Object.is(+0, -0); // false
Object.is(NaN, NaN); // true
6 Proxy代理
vue2.x使用的响应式原理底层使用了Object.defineProperty
对对象进行代理,该方法不能实现对对象方法的代理,也无法监听数组的变化。
ES6中提供的Proxy可以更好的对对象进行代理,vue3的响应式系统也采用了Proxy。不过Proxy无法被polyfill,导致其对于低版本浏览器支持性不好。
6.1 基本使用方法:
通过get
,set
拦截对属性的访问与设置,可以在操作前做一些自定义的操作
const person = {
name: 'tom',
age: 18
}
// Proxy第一个值传入被代理对象,第二个值为拦截的配置项
const personProxy = new Proxy(person, {
get(target, property) {
return property in target ? target[property] : 'default';
},
set(target, property, value) {
if (property === 'age' && !Number.isInteger(value)) {
throw new TypeError(`${value} is not an int`);
}
target[property] = value;
}
})
console.log(personProxy.name); // tom
console.log(personProxy.age); // 18
personProxy.age = '18'; // TypeError: 18 is not an int
6.2 Proxy对比Object.defineProperty的优势
Object.defineProperty
只能监听属性的读写,无法监听到delete操作,对对象方法的调用等
Proxy对对象的代理更加强大。
删除对象属性的代理
const person = {
name: 'tom',
age: 18
}
const personProxy = new Proxy(person, {
deleteProperty(target, property) {
console.log('delete', property);
delete target[property];
}
})
delete personProxy.age; // delete age
console.log(person) // { name: 'tom' }
Proxy可代理的所有操作:
Handler | 触发方式 |
---|---|
get | 读取某个属性 |
set | 写入某属性 |
has | in操作符 |
deleteProperty | delete操作符 |
getPrototypeOf | Object.getPrototypeOf() |
setPrototypeOf | Object.setPrototypeOf() |
isExtensible | Object.isExtensible() |
preventExtensions | Object.preventExtensions() |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor() |
defineProperty | Object.defineProperty() |
ownKeys | Object.getOwnPropertyNames()、Object.getOwnPropertySymbols() |
apply | 调用一个函数 |
construct | 用new 调用一个函数 |
监听数组对象操作
const list = [];
const listProxy = new Proxy(list, {
set(target, property, value) {
console.log('set', property, value);
target[property] = value;
return true;
}
})
listProxy.push(100)
// set 0 100
// set length 1
Proxy可以通过非侵入式的方式完成对对象的操作,Object.defineProperty需要操作原对象
7 Reflect 反射
Reflect
是一个静态类,无法通过new
的方式创建新实例,只能调用其静态方法
Reflect
内部定义了对对象的13中操作(之前14种,废弃了一种),与Proxy处理对象的13中操作方法对应,且为Proxy处理对象的默认操作。
Reflect的价值:
统一了一套用于操作对象的API
例:
const obj = {
name: 'tom',
age: 18
}
// 传统做法
console.log('name' in obj);
console.log(delete obj['age']);
console.log(Object.keys(obj));
// Reflect做法
console.log(Reflect.has(obj, 'name'))
console.log(Reflect.deleteProperty(obj, 'age'))
console.log(Reflect.ownKeys(obj))
8 Promise
通过链式调用解决异步编程回调函数嵌套过深的问题。
9 Class
类的定义
传统方式:通过构造函数进行定义,通过prototype显式原型添加对象实例的共享属性
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = functoin(name) {
console.log(`hello ${name}`)
}
ES6之后新方法:通过class关键词定义类
class Person {
constructor(name) {
this.name = name;
}
sayHello(name) {
console.log(`hello ${name}`)
}
}
注意:
class定义类为严格模式,严格模式下函数中的this为undefined
静态方法
静态方法也叫类方法,由类进行调用。实例方法通过new出一个实例,通过实例进行方法的调用。
class Person {
constructor(name) {
this.name = name;
}
sayHello(name) {
console.log(`hello ${name}`)
}
create(name) {
return new Person(name);
}
}
const tom = Person.create('tom');
类的继承
ES6中通过extends
关键词实现继承,super可以调用父类的构造函数。
class Student extends Person {
constructor(name) {
super(name)
}
}
10 新增数据结构
10.1 Set 集合
集合中不会存在重复的元素,可用于数组去重。
使用方法:
-
set.add()
添加元素 -
set.forEach()
遍历 -
set.size
获取集合长度 -
set.has(item)
判断集合中是否存在某个值 -
set.delete(iten)
删除集合中元素 -
set.clear()
清空集合
const set = new Set();
s.add(1).add(2); // 集合的add方法会返回集合本身,可实现链式调用
// forEach遍历
set.forEach(item => console.log(item))
// for of 遍历
for (let item of set) {
console.log(item);
}
console.log(set.size); // 获取集合长度
console.log(set.has(1)); // 查看集合中是否存在1
set.delete(1); // 删除集合中1
set.clear(); // 清空集合
10.2 Map
字面量对象的key无法使用复杂数据结构作为key,使用复杂数据结构作为key,key会通过toString()
进行转换。
例:
const obj = {};
obj[true] = 'value';
obj[123] = 'value';
obj[{a: 1}] = 'value';
console.log(Object.keys(obj)); // [ '123', 'true', '[object Object]' ]
map数据结构真正实现了key - value键值对映射,key也可用于存储复杂数据结构。
const map = new Map();
const tom = { name: 'tom' };
map.set(tom, 'this is tom');
console.log(map); // Map { { name: 'tom' } => 'this is tom' }
console.log(map.get(tom)); // this is tom
map支持的操作方法:
map.set(key, value)
添加键值对映射map.get(key)
通过key获取valuemap.has(key)
通过key查看是否存在某个映射map.delete(key)
通过key删除映射关系map.clear()
清空mapmap.forEach((val, key ) => {})
通过forEach进行遍历
10.3 Symbol 原始类型
Symbol是ES6新增的基础类型,通过Symbol类型可以创建一个独一无二的符号。
const s = Symbol();
const s1 = Symbol()
console.log(s); // Symbol()
console.log(typeof s) // symbol
console.log(s === s1) // false
通过给Symbol传递一个参数,描述Symbol
const tom = Symbol('tom');
const jerry = Symbol('jerry');
console.log(tom, jerry); // Symbol(tom) Symbol(jerry)
Symbol可以作为对象的key
const tom = Symbol('tom');
const person = {
age: 18
}
person[tom] = 'tom';
console.log(person); // {age: 18, Symbol(tom): "tom"}
借助Symbol实现私有成员
// a.js
const name = Symbol();
const person = {
[name] = 'tom';
sayHello() {
console.log(`hello ${tom}`);
}
}
// b.js
// 无法创建相同的Symbol,无法直接访问私有成员
person.sayHello();
Symbol.for(key)
Symbol.for(key)
静态方法在全局维护了一个字符串与Symbol的映射关系,其中的key如果不为字符串,则会被转为字符串,由于维护的是字符串与Symbol的映射关系,Symbol.for('hello') === Symbol.for('hello')
;Symbol.for('true') === Symbol.for(true)
(布尔true会被转换为字符串'true')
Symbol常量
Symbol内部还维护着许多Symbol常量,用于作为内部方法的标识,可以让自定义对象实现js内置的接口。
例:自定义toString标签
const obj = {
[Symbol.toStringTag]: 'TomObject'
}
console.log(obj.toString()) // [object TomObject]
注意:
通过Symbol定义的对象属性,使用for in 循环,Object,keys()时候都无法获取到。使用
JSON.stringify(obj)
也会忽略Symbol属性
通过Object.getOwnPropertySymbols(obj)
可获取到对象中通过Symbol
定义的属性名。
11 循环与遍历
11.1 for of循环
通过for of进行的循环可以使用break关键词结束循环。for of 循环可对实现了迭代器的数据结构进行遍历。
其他遍历方法:
array.forEach()
无法使用breakfor in
对象专用遍历array.map()
遍历并生成新数组array.filter
遍历并挑选符合条件的元素生成新数组array.every
遍历,所有元素满足条件返回true
,否则返回false
array.some
遍历,有元素满足条件返回true
,否则返回false
for of遍历map
通过for of 遍历map,item为[key, value]
的数, 可通过数组解构方式获取key,value
const map = new Map();
map.set('a', 'aa');
map.set('b', 'bb');
for(let [key, value] of map) {
console.log(key, value);
}
for of
无法直接遍历普通对象,只能遍历实现了迭代器接接口的数据结构。
12 Iterator 迭代器
迭代器接口的实现是for of
循环的前提条件
12.1 Iterable可迭代接口
const obj = {
[Symbol.iterator]: function() {
return {
next: function() {
return {
value: 'tom',
done: true
}
}
}
}
}
- obj实现了可迭代接口(
Iterable
),其约定对象必须有Symbol.iterator
属性用于返回迭代器的方 法 [Symbol.iterator]
属性对应的函数返回值实现了Iterator
接口,其约定对象内部必须有用于迭代的next()
方法- 在
next()
方法中返回的结果实现了IterationResult
接口,内部必须有value
表示被迭代到的数据,done
属性(布尔值)表示迭代是否完成。
例:可迭代的object
const obj = {
store: ['foo', 'bar', 'baz'],
[Symbol.iterator]: function() {
let index = 0;
const self = this;
return {
next: function() {
const result = {
value: self.store[index],
done: index >= self.store.length
}
index++;
return result;
}
}
}
}
for(let item of obj) {
console.log(item);
}
// fop bar baz
13 Generator生成器
函数名前加*
定义生成器函数。
生成器函数执行后会返回一个生成器,生成器函数也实现了迭代器接口。
function *foo() {
console.log('tom');
return 100;
}
const result = foo(); // result为生成器对象,函数并不会立即执行。
console.log(result.next()) // {value: 100, done: true}
yield关键词
yield
会暂停函数的执行,yield的值会作为next的结果中的value值。
function *foo() {
console.log('1111');
yield 100;
console.log('2222');
yield 200;
console.log('3333');
yield 300;
}
const g = foo();
console.log(g.next()) // 1111 {value: 100, done: false}
console.log(g.next()) // 2222 {value: 200, done: false}
console.log(g.next()) // 3333 {value: 300, done: false}
console.log(g.next()) // {value: undefined, done: true}
14 ES 2016
数组实例的includes
方法
array.indexOf
无法查找NaN
,array.includes()
可以查找
const arr = ['foo', 'bar', 'baz', NaN]
console.log(arr.includes('foo')) // true
console.log(arr.includes(NaN)) // true
指数运算符
// 2的10次方
console.log(2 ** 10) // 1024
15 ES 2017
1. 对象的Object.values(obj)方法:返回对象的值的数组
2. 对象的Object.entries(obj)方法: 返回对象的[key,valye]数组
3. Object.getOwnPropertyDescriptors(obj):获取对象属性的完整描述信息
通过Object.assign复制的对象无法正确复制getter,setter属性。可借助以上方法与Object.defineProperties
实现对象的完整复制
const obj = {
firstName: 'Tom',
lastName: 'Wang',
get fullName() {
return this.firstName + ' ' + this.lastName;
}
}
console.log(obj.fullName); // Tom Wang
const obj1 = Object.assign({}, obj);
obj1.firstName = 'Jerry';
console.log(obj1.fullName);// Tom Wang 与预期不符,因为fullNmae是个getter属性,没能正确复制过来
解决方法:
const obj = {
firstName: 'Tom',
lastName: 'Wang',
get fullName() {
return this.firstName + ' ' + this.lastName;
}
}
console.log(obj.fullName); // Tom Wang
const propertyDescriptors = Object.getOwnPropertyDescriptors(obj);
console.log(propertyDescriptors);
const obj1 = Object.defineProperties({}, propertyDescriptors);
obj1.firstName = 'Jerry';
console.log(obj1.fullName); // Jerry Wang
4. 字符串填充方法: String.prototype.padStart(length, padStr)
,String.prototype.padEnd(length, padStr)
5. 允许函数最后一个元素后面增加一个尾逗号: function(a,b,){}
6. Async/Await语法