EMAScript定义了语言的标准。
我们的node环境是由ECMAScript+NodeAPI组成的。
浏览器环境是由
ECMAScript+DOM+BOM组成的。
let和const
在ES6之前,只有一种定义变量的方式,即var。通过var定义的变量会遇到很多奇怪的问题,为了解决该问题,ES6定义了块级作用域并引入了let和const关键字。
var
简单举几个var会产生的问题。
if (true) {
var foo = 'xhh'
}
console.log(foo) // xhh`
for (var i = 0; i < 3; i++) {
for (var i = 0; i < 3; i++) {
console.log(i)
}
// 打印了三次,两个for循环中使用了相同的i,当内层for循环执行结束之后,i的值是3,则不会执行外部的for循环
console.log('内层结束 i = ' + i)
}
var elements = [{}, {}, {}]
for (var i = 0; i < elements.length; i++) {
elements[i].onclick = function () {
console.log(i)
}
}
// 打印的值为2,变量i绑定到了全局对象上 可以用let或者闭包来解决该问题
elements[0].onclick()
简单举一个变量提升的例子
console.log(foo);
var foo = 'xhh';
等价于下面的代码
var foo = undefined;
console.log(foo); // undefined
foo = 'xhh';
let
let引入了块级作用域的概念,通过let声明的变量仅在该代码块内有效。针对上面的几个例子,可以将var更改为let,则可以正常执行。
{
let foo = 'xhh'
}
console.log(foo); // ReferenceError: foo is not defined
// for 循环会产生两层作用域
for (let i = 0; i < 3; i++) {
let i = 'foo'
console.log(i) // foo
}
const
const也是块级作用域的,与let的区别是一旦使用const声明后,就不可以更改变量的指向了。
const foo = 'foo';
foo = 'bar'; // TypeError: Assignment to constant variable.
const obj = {}
// 对于数据成员的修改是没有问题的 因为声明后obj指向的是一个对象的地址,而操作obj对象时相当于对地址上的内容做操作,本质上地址是没有变化的
obj.name = 'xhh'
obj = {} // TypeError: Assignment to constant variable. 更改地址指向则会报错
Array destructuring
在日常工作中,曾经review过其他小伙伴的代码,其中充满了arr[0]、arr[1]类似的代码,看上去满头雾水。
其实可以通过数组结构的方式来使代码更语义化一些。举一个简单的例子。
const arr = ['X', 'HH', 'XHH'];
// 常规的取值方式是使用arr[0], arr[1], arr[2]的方式来取值,ES6版本之后我们可以使用如下方式来结构
const [lastName, firstName, fullName] = arr;
// 当不需要前面两个变量时,也可以通过 ','来分割,避免产生多余变量
const [, , name] = arr;
// 同样的对于数组我们也可以指定默认值
const [, , , sex = 'MALE'] = arr;
console.log(sex); //MALE 可以看到即使我们在数组中没有声明 也是可以获取到对应值的
Object destructuring
- 解构
const obj = { name: 'XHH', age: 18 }
let objName = obj.name;
let objAge = obj.age;
const { name, age } = obj
console.log(name) // 可以看到代码已经变得相当简洁了,如果要取的属性更多,那么效果就会更明显
- 变量重命名
// 当要解构的变量名与已定义变量名相同时,可以给待解构的变量名起别名
const name = 'tom'
const { name: objName } = obj
console.log(objName)
- 变量默认值
const user = {firstname: 'X'};
const {firstname:fullName = 'XHH', role='Admin'} = user;
console.log(role); // Admin
Template string
模板字符串
const name = 'tom'
// 可以通过 ${} 插入表达式,表达式的执行结果将会输出到对应位置
const msg = `hey, ${name} --- ${1 + 2} ---- ${Math.random()}`
console.log(msg)
// 带标签的模板字符串
// 模板字符串的标签就是一个特殊的函数,
// 使用这个标签就是调用这个函数
// const str = console.log`hello world`
const name = 'tom'
const gender = false
function myTagFunc (strings, name, gender) {
// console.log(strings, name, gender)
// return '123'
const sex = gender ? 'man' : 'woman'
return strings[0] + name + strings[1] + sex + strings[2]
}
const result = myTagFunc`hey, ${name} is a ${gender}.`
console.log(result)
String ext methods
// 字符串的扩展方法
const message = "Error: foo is not defined.";
message.startsWith("Error"); // true
message.endsWith("."); // true
message.includes("foo"); // true
Parameter defaults
举一个简单的例子
function map(arr) {
// arr = arr || [];
arr.map((item) => item + 1);
}
为了避免arr为空报错的情况,我们会用||添加一个默认值。
现在我们可以直接在声明函数的时候为arr指定默认值,如下。
function map(arr = []) {
arr.map((item) => item + 1);
}
Rest parameter
function foo (first, ...args) {
console.log(first); // 1
console.log(args) // [2,3,3]
}
foo(1, 2, 3, 4)
Spread parameter
// 展开数组参数
const arr = ['foo', 'bar', 'baz']
// console.log(
// arr[0],
// arr[1],
// arr[2],
// )
// console.log.apply(console, arr)
console.log(...arr)
Arrow
使用箭头函数可以让函数写起来更简单
function inc (number) {
return number + 1
}
// 最简方式
const inc = n => n + 1
// arr.filter(function (item) {
// return item % 2
// })
// 常用场景,回调函数
arr.filter(i => i % 2)
arrow处理this。我们知道箭头函数定义的函数中是没有自己的this的,它会去最近的作用域内寻找依附的this。
const test = () => this // 浏览器环境中执行
console.log(test()); // window
// 箭头函数与 this
// 箭头函数不会改变 this 指向
const person = {
name: 'tom',
// sayHi: function () {
// console.log(`hi, my name is ${this.name}`)
// }
sayHi: () => {
console.log(`hi, my name is ${this.name}`)
},
sayHiAsync: function () {
const _this = this
setTimeout(function () {
console.log(_this.name)
// console.log(this); // Timeout
}, 1000)
// console.log(this) // person
setTimeout(() => {
console.log(this.name)
// console.log(this) // person
}, 1000)
}
}
person.sayHiAsync()
Tips: 常规的
react component中,定义的类属性函数是需要绑定作用域的,此时我们可以使用箭头函数来简化代码。
Object
对象字面量, 当变量名与对象的属性名相同时,可以使用如下方式进行简写。
// 对象字面量
const bar = "345";
const obj = { bar };
// 等价于 const obj = {bar:bar}
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 }
console.log(target === returnedTarget);
// true
Object.is()方法判断两个值是否为同一个值。
// 一般我们的比较会使用 == 或者 === , == 一般是值的比较 ===是值与类型的比较
// 但是 === 无法判断 +0与-0是否相等,两个NaN也判断为不相等
console.log(
// 0 == false // => true
// 0 === false // => false
// +0 === -0 // => true
// NaN === NaN // => false
// Object.is(+0, -0) // => false
// Object.is(NaN, NaN) // => true
)
简单看一下Object.is的Polyfill
if (!Object.is) {
Object.is = function(x, y) {
// SameValue algorithm
if (x === y) { // Steps 1-5, 7-10
// Steps 6.b-6.e: +0 != -0
return x !== 0 || 1 / x === 1 / y;
} else {
// Step 6.a: NaN == NaN
return x !== x && y !== y;
}
};
}
Proxy
// Proxy 对象
// const person = {
// name: 'zce',
// age: 20
// }
// const personProxy = new Proxy(person, {
// // 监视属性读取
// get (target, property) {
// return property in target ? target[property] : 'default'
// // console.log(target, property)
// // return 100
// },
// // 监视属性设置
// set (target, property, value) {
// if (property === 'age') {
// if (!Number.isInteger(value)) {
// throw new TypeError(`${value} is not an int`)
// }
// }
// target[property] = value
// // console.log(target, property, value)
// }
// })
// personProxy.age = 100
// personProxy.gender = true
// console.log(personProxy.name)
// console.log(personProxy.xxx)
// Proxy 对比 Object.defineProperty() ===============
// 优势1:Proxy 可以监视读写以外的操作 --------------------------
// const person = {
// name: 'zce',
// age: 20
// }
// const personProxy = new Proxy(person, {
// deleteProperty (target, property) {
// console.log('delete', property)
// delete target[property]
// }
// })
// delete personProxy.age
// console.log(person)
// 优势2:Proxy 可以很方便的监视数组操作 --------------------------
// const list = []
// const listProxy = new Proxy(list, {
// set (target, property, value) {
// console.log('set', property, value)
// target[property] = value
// return true // 表示设置成功
// }
// })
// listProxy.push(100)
// listProxy.push(100)
// 优势3:Proxy 不需要侵入对象 --------------------------
// const person = {}
// Object.defineProperty(person, 'name', {
// get () {
// console.log('name 被访问')
// return person._name
// },
// set (value) {
// console.log('name 被设置')
// person._name = value
// }
// })
// Object.defineProperty(person, 'age', {
// get () {
// console.log('age 被访问')
// return person._age
// },
// set (value) {
// console.log('age 被设置')
// person._age = value
// }
// })
// person.name = 'jack'
// console.log(person.name)
// Proxy 方式更为合理
const person2 = {
name: 'zce',
age: 20
}
const personProxy = new Proxy(person2, {
get (target, property) {
console.log('get', property)
return target[property]
},
set (target, property, value) {
console.log('set', property, value)
target[property] = value
}
})
personProxy.name = 'jack'
console.log(personProxy.name)
| handler 方法 | 触发方式 |
|---|---|
| get | 读取某个属性 |
| set | 写入某个属性 |
| has | in 操作符 |
| deleteProperty | delete 操作符 |
| getProperty | Object.getPropertypeOf() |
| setProperty | Object.setPrototypeOf() |
| isExtensible | Object.isExtensible() |
| preventExtensions | Object.preventExtensions() |
| getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor() |
| defineProperty | Object.defineProperty() |
| ownKeys | Object.keys() 、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols() |
| apply | 调用一个函数 |
| construct | 用 new 调用一个函数 |
如有有没有用过的API,可以去MDN查找一下。
Reflect
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的。
-
Reflect.apply(target, thisArgument, argumentsList)对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和
Function.prototype.apply()功能类似。 -
Reflect.construct(target, argumentsList[, newTarget\])对构造函数进行
new操作,相当于执行new target(...args)。 -
Reflect.defineProperty(target, propertyKey, attributes)和
Object.defineProperty()类似。如果设置成功就会返回true -
Reflect.deleteProperty(target, propertyKey)作为函数的
delete操作符,相当于执行delete target[name]。 -
Reflect.get(target, propertyKey[, receiver\])获取对象身上某个属性的值,类似于
target[name]。 -
Reflect.getOwnPropertyDescriptor(target, propertyKey)类似于
Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符, 否则返回undefined. -
Reflect.has(target, propertyKey)判断一个对象是否存在某个属性,和
in运算符 的功能完全相同。 -
返回一个包含所有自身属性(不包含继承属性)的数组。(类似于
Object.keys(), 但不会受enumerable影响). -
Reflect.preventExtensions(target)类似于
Object.preventExtensions()。返回一个Boolean。 -
Reflect.set(target, propertyKey, value[, receiver\])将值分配给属性的函数。返回一个
Boolean,如果更新成功,则返回true。 -
Reflect.setPrototypeOf(target, prototype)设置对象原型的函数. 返回一个
Boolean, 如果更新成功,则返回true。
Class
// class 关键词
// function Person (name) {
// this.name = name
// }
// Person.prototype.say = function () {
// console.log(`hi, my name is ${this.name}`)
// }
class Person {
constructor (name) {
this.name = name
}
say () {
console.log(`hi, my name is ${this.name}`)
}
}
const p = new Person('tom')
p.say()
Static method
// static 方法
class Person {
constructor (name) {
this.name = name
}
say () {
console.log(`hi, my name is ${this.name}`)
}
static create (name) {
return new Person(name)
}
}
// 有点像函数式编程里面函子的构造方式
const tom = Person.create('tom')
tom.say()
class extends
// extends 继承
class Person {
constructor (name) {
this.name = name
}
say () {
console.log(`hi, my name is ${this.name}`)
}
}
class Student extends Person {
constructor (name, number) {
super(name) // 父类构造函数
this.number = number
}
hello () {
super.say() // 调用父类成员
console.log(`my school number is ${this.number}`)
}
}
const s = new Student('jack', '100')
s.hello()
// TODO 补充一下js的继承方式
Set
// Set 数据结构
const s = new Set()
s.add(1).add(2).add(3).add(4).add(2)
// console.log(s)
// s.forEach(i => console.log(i))
// for (let i of s) {
// console.log(i)
// }
// console.log(s.size)
// console.log(s.has(100))
// console.log(s.delete(3))
// console.log(s)
// s.clear()
// console.log(s)
// 应用场景:数组去重
const arr = [1, 2, 1, 3, 4, 1]
// const result = Array.from(new Set(arr))
const result = [...new Set(arr)]
console.log(result)
// 弱引用版本 WeakSet
// 差异就是 Set 中会对所使用到的数据产生引用
// 即便这个数据在外面被消耗,但是由于 Set 引用了这个数据,所以依然不会回收
// 而 WeakSet 的特点就是不会产生引用,
// 一旦数据销毁,就可以被回收,所以不会产生内存泄漏问题。
const fn = () => {};
const obj = {};
const set = new Set([fn, fn, obj, obj]);
console.log(set.size); // 2
Map
一直很好奇,既然对象是就是键值对的形式,为什么还会有map的产生呢,简单看一下下面的代码
const obj = {}
obj[true] = 'value'
obj[123] = 'value'
obj[{ a: 1 }] = 'value'
// 可以看到对象的所有键值在存储的时候都被转换成了字符串,这种情况如果键值是对象,那么存储很容易产生漏数据的情况
console.log(Object.keys(obj)) // [ '123', 'true', '[object Object]' ]
console.log(obj['[object Object]']) // value
const m = new Map();
const tom = { name: "tom" };
const tom2 = { name: "tom" };
m.set(tom, 90);
m.set(tom2, 100);
// 可以看到map的键值是没有限制的
console.log(m); // Map(2) { { name: 'tom' } => 90, { name: 'tom' } => 100 }
console.log(m.get(tom)); // 90
// // m.has()
// // m.delete()
// // m.clear()
// m.forEach((value, key) => {
// console.log(value, key)
// })
// 弱引用版本 WeakMap
// 差异就是 Map 中会对所使用到的数据产生引用
// 即便这个数据在外面被消耗,但是由于 Map 引用了这个数据,所以依然不会回收
// 而 WeakMap 的特点就是不会产生引用,
// 一旦数据销毁,就可以被回收,所以不会产生内存泄漏问题。
Symbol
symbol是一个基本类型
typeof Symbol() === 'symbol' // true
它的出现是为了解决可拓展对象属性命名重复的问题, 下面是一个常见的例子,可以看到不同的文件中对相同的对象属性做了覆盖,这样就很容易出现bug。
// common.js
const A = {};
// A.js
A.foo = 1;
// B.js
A.foo = 2;
下面我们看一下Symbol是如何解决该问题的。
// 两个 Symbol 永远不会相等
console.log(
Symbol() === Symbol()
) // false
// Symbol 描述文本
console.log(Symbol('foo'))
console.log(Symbol('bar'))
console.log(Symbol('baz'))
// 使用 Symbol 为对象添加用不重复的键
const obj = {}
obj[Symbol()] = '123'
obj[Symbol()] = '456'
console.log(obj) // { [Symbol()]: '123', [Symbol()]: '456' }
// 案例2:Symbol 模拟实现私有成员
// a.js ======================================
const name = Symbol()
const person = {
[name]: 'zce',
say () {
console.log(this[name])
}
}
// 只对外暴露 person
// b.js =======================================
// 由于无法创建出一样的 Symbol 值,
// 所以无法直接访问到 person 中的「私有」成员
// person[Symbol()]
person.say()
// Symbol 补充
console.log(
Symbol("foo") === Symbol("foo") // false
);
// Symbol 全局注册表 ----------------------------------------------------
// 如果有可能会被重用,那么可以使用Symbol.for来声明, 声明后会转换成字符串存储在全局注册表中
const s1 = Symbol.for("foo");
const s2 = Symbol.for("foo");
console.log(s1 === s2);
console.log(Symbol.for(true) === Symbol.for("true")); // true
// 此处同样会涉及到键值转换成字符串重复的问题,需要注意
console.log(Symbol.for({ a: 1 }) === Symbol.for({ b: 1 })); // true
// 内置 Symbol 常量 ---------------------------------------------------
console.log(Symbol.iterator) // Symbol(Symbol.iterator)
console.log(Symbol.hasInstance) // Symbol(Symbol.hasInstance)
const obj = {
[Symbol.toStringTag]: 'XObject'
}
console.log(obj.toString()) // [object XObject]
// Symbol 属性名获取 ---------------------------------------------------
// 需要注意 for循环遍历、Object.keys()和JSON.stringify是没有办法拿到Symbol定义的属性的
const obj = {
[Symbol()]: "symbol value",
foo: "normal value",
};
for (var key in obj) {
console.log(key); //foo
}
console.log(Object.keys(obj)); // [ 'foo' ]
console.log(JSON.stringify(obj)); // {"foo":"normal value"}
console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol() ]
For of
我们知道常规的遍历方式比较多。
- for(let i = 0; i < arr.length; i++)
- forEach
- for(let key in obj)
- 普通for循环代码比较啰嗦,常常会有一些临时变量
- forEach没有办法终止循环,一般要使用some和every来配置使用
- for in只可以遍历对象
es6引入了for of的遍历方式,请看如下案例。
// for...of 循环
const arr = [100, 200, 300, 400]
// for (const item of arr) {
// console.log(item)
// }
// for...of 循环可以替代 数组对象的 forEach 方法
// arr.forEach(item => {
// console.log(item)
// })
// for (const item of arr) {
// console.log(item)
// if (item > 100) {
// break
// }
// }
// forEach 无法跳出循环,必须使用 some 或者 every 方法
// arr.forEach() // 不能跳出循环
// arr.some()
// arr.every()
// 遍历 Set 与遍历数组相同
// const s = new Set(['foo', 'bar'])
// for (const item of s) {
// console.log(item)
// }
// 遍历 Map 可以配合数组解构语法,直接获取键值
// const m = new Map()
// m.set('foo', '123')
// m.set('bar', '345')
// for (const [key, value] of m) {
// console.log(key, value)
// }
// 普通对象不能被直接 for...of 遍历
const obj = { foo: 123, bar: 456 }
for (const item of obj) { // TypeError: obj is not iterable
console.log(item)
}
此处抛出了一个问题,为什么对象没有办法被for of 遍历,可以看到报错信息是 obj is not iterable。我们可以去简单看一下Array,Set,Map他们的原型上面都有一个Symbol(Symbol.iterator)属性,该属性是一个函数,提供了对for of的支持,如果对象实现了该方法,我们认为该对象是一个可迭代对象,即iterable。
下面我们来看一下下面的例子,去除迭代器函数,每次调用next方法会返回{value: any, done: boolean} 的一个对象,当无内容可遍历时, done的属性值为true, value为undefined。
// 迭代器(Iterator)
const set = new Set(['foo', 'bar', 'baz'])
const iterator = set[Symbol.iterator]() // [Set Iterator] { 'foo', 'bar', 'baz' }
console.log(iterator.next()) // { value: 'foo', done: false }
console.log(iterator.next()) // { value: 'bar', done: false }
console.log(iterator.next()) // { value: 'baz', done: false }
console.log(iterator.next()) // { value: undefined, done: true }
console.log(iterator.next()) // { value: undefined, done: true }
while (true) {
const current = iterator.next()
if (current.done) {
break // 迭代已经结束了,没必要继续了
}
console.log(current.value)
}
我们使用对象模拟一下该实现,代码如下:
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 (const item of obj) {
console.log('循环体', item)
}
可以看到此时已经不在报错了,这里我们使用的是设计模式中的迭代器模式,迭代器模式的好处是使用者不需要知道可迭代对象内部的实现,可迭代的内容完全由可迭代对象来指定。
一个简单的todo list小应用
// 迭代器设计模式
// 场景:你我协同开发一个任务清单应用
// 我的代码 ===============================
const todos = {
life: ['吃饭', '睡觉', '打豆豆'],
learn: ['语文', '数学', '外语'],
work: ['喝茶'],
// 提供统一遍历访问接口
each: function (callback) {
const all = [].concat(this.life, this.learn, this.work)
for (const item of all) {
callback(item)
}
},
// 提供迭代器(ES2015 统一遍历访问接口)
[Symbol.iterator]: function () {
const all = [...this.life, ...this.learn, ...this.work]
let index = 0
return {
next: function () {
return {
value: all[index],
done: index++ >= all.length
}
}
}
}
}
// 你的代码 ===============================
// for (const item of todos.life) {
// console.log(item)
// }
// for (const item of todos.learn) {
// console.log(item)
// }
// for (const item of todos.work) {
// console.log(item)
// }
todos.each(function (item) {
console.log(item)
})
console.log('-------------------------------')
for (const item of todos) {
console.log(item)
}
Generator
生成器函数,这不是一个新鲜的东西,在Python中也有类似的概念,简单看一下下面的代码。生成器函数相当于是惰性执行的,调用之后不会立刻执行,需要有人去调用next方法才会将代码向下执行,每次调用next会停留在yield关键字处,等待下次调用。
function * foo () {
console.log('1111')
yield 100
console.log('2222')
yield 200
console.log('3333')
yield 300
}
const generator = foo()
console.log(generator.next()) // 第一次调用,函数体开始执行,遇到第一个 yield 暂停 { value: 100, done: false }
console.log(generator.next()) // 第二次调用,从暂停位置继续,直到遇到下一个 yield 再次暂停 { value: 200, done: false }
console.log(generator.next()) // 。。。 { value: 300, done: false }
console.log(generator.next()) // 第四次调用,已经没有需要执行的内容了,所以直接得到 undefined { value: undefined, done: true }
一个常见的小应用就是银行的发号器
// Generator 应用
// 案例1:发号器
function * createIdMaker () {
let id = 1
while (true) {
yield id++
}
}
const idMaker = createIdMaker()
console.log(idMaker.next().value)
console.log(idMaker.next().value)
console.log(idMaker.next().value)
console.log(idMaker.next().value)
基于这种思想,我们可以优化一下上面的对象遍历代码。
const todos = {
life: ['吃饭', '睡觉', '打豆豆'],
learn: ['语文', '数学', '外语'],
work: ['喝茶'],
[Symbol.iterator]: function * () {
const all = [...this.life, ...this.learn, ...this.work]
for (const item of all) {
yield item
}
}
}
for (const item of todos) {
console.log(item)
}
Promise
Promise是一个很大的概念,我会在后面的异步编程的笔记里面补充。