本文参考:阮一峰的博客
var、let、const的区别
let和const是块级作用域;
同一作用域下let和const不可声明同名变量,而var可以;
var存在变量提升,let和const不存在变量提升,所以存在暂时性死区;
const一旦声明必须赋值,不能用null占位,且值不可更改;
var声明的变量会挂载在window上,而let和const声明的变量不会。
变量的解构赋值
- 数组的解构赋值
let [a, b, c] = [1, 2, 3];
// a 1
// b 2
// c 3
- 对象的解构赋值
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
// foo aaa
// bar bbb
- 字符串的解构赋值
const [a, b, c, d, e] = 'hello';
// a "h"
// b "e"
// c "l"
// d "l"
// e "o"
- 函数参数的解构赋值
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]
字符串扩展
1.遍历器
for (let codePoint of 'hi') {
console.log(codePoint)
}
// "h"
// "i"
2.模板字符串
let a = `Hello ${name}, how are you ${time}?`
3.新增方法
'Hello world!'.includes('o') // true
'Hello world!'.startsWith('Hello') // true
'Hello world!'.endsWith('!') // true
'hello'.repeat(2) // "hellohello"
'x'.padStart(5, 'ab') // 'ababx'
'x'.padEnd(5, 'ab') // 'xabab'
' abc '.trimStart() // "abc "
' abc '.trimEnd() // " abc"
[...'aabbcc'.matchAll('[a-b]')].toString() // a,a,b,b
'aabbcc'.replaceAll('b', '_') // 'aa__cc'
Number的扩展
Number.isFinite(15); //true 用来检查一个数值是否为有限的(finite)
Number.isNaN(NaN) //true 用来检查一个值是否为NaN
Number.parseInt('12.34') //12 将全局方法parseInt()移植到Number对象上面
Number.parseFloat('123.45#') // 123.45 将全局方法parseFloat(),移植到Number对象上面
Number.isInteger(25) // rue 判断一个数值是否为整数
Math的扩展
Math.trunc(4.1) //4 去除一个数的小数部分,返回整数部分
Math.sign(-5) //-1 判断一个数到底是正数、负数、还是零
Math.cbrt(27) //3 计算一个数的立方根
函数的扩展
1.参数默认值
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello', '') // Hello
2.rest参数
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
console.log(item);
});
}
var a = [];
push(a, 1, 2, 3)
3.name属性
function foo() {}
foo.name // "foo"
4.箭头函数
var sum = (num1, num2) => num1 + num2;
//函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
//不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
//不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
//不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
5.Function.prototype.toString()
返回函数代码本身,以前会省略注释和空格,修改后返回一模一样的原始代码
6.catch 命令的参数省略
try {
// ...
} catch {
// ...
}
箭头函数与普通函数的区别
- 箭头函数是匿名函数,不能作为构造函数,不能使用new
- 箭头函数不能绑定arguments,要用rest参数解决
- 箭头函数没有原型属性
- 箭头函数的this永远指向其上下文的this
- 箭头函数不能绑定this,会捕获其所在的上下文的this值,作为自己的this值
箭头函数的this
- 箭头函数自己没有this指针,所以会顺着原型链去找,所以它的this指向定义时所在的对象
- 箭头函数调用call和apply是没有效果的,第一个参数会被忽略
- 箭头函数调用bind可以返回一个改变this指向的函数
数组的扩展
// 扩展运算符
console.log(...[1, 2, 3]) // 1 2 3
// Array.from用于将两类对象转为真正的数组:类似数组的对象和可遍历的对象
function foo() {
var args = Array.from(arguments);
}
// Array.of用于将一组值转换为数组
Array.of(3, 11, 8) // [3,11,8]
// 数组实例的copyWithin()方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组
[1, 2, 3, 4, 5].copyWithin(0, 3, 4) // [4, 2, 3, 4, 5]
// 数组实例的find方法,用于找出第一个符合条件的数组成员
[1, 4, -5, 10].find((n) => n < 0) // -5
// 数组实例的findIndex方法,用于找出第一个符合条件的数组成员所在位置
[1, 5, 10, 15].findIndex((n) => n > 9) // 2
// 数组实例的fill方法使用给定值,填充一个数组
['a', 'b', 'c'].fill(7) // [7, 7, 7]
// 数组实例的entries()返回数组的所有键值对
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
// 0 "a"
// 1 "b"
}
// 数组实例的keys()返回数组的所有键名
for (let index of ['a', 'b'].keys()) {
console.log(index);
// 0
// 1
}
// 数组实例的values()返回数组的所有键值
for (let elem of ['a', 'b'].values()) {
console.log(elem);
// 'a'
// 'b'
}
// 数组实例的 includes()返回一个布尔值,表示某个数组是否包含给定的值
[1, 2, 3].includes(2) // true
对象的扩展
1.属性的简洁表示:
const foo = 'bar';
const baz = {foo};
2.属性名表达式
obj['a' + 'bc'] = 123;
3.方法的name属性
const person = {
sayName() {}
};
person.sayName.name
4.属性遍历:
for...in // 只遍历对象自身的和继承的可枚举的属性
Object.keys(obj) // 返回对象自身的所有可枚举的属性的键名
Object.getOwnPropertyNames(obj) // 包含对象自身的所有属性的键名
Object.getOwnPropertySymbols(obj) // 包含对象自身的所有 Symbol 属性的键名
Reflect.ownKeys(obj) // 包含对象自身的(不含继承的)所有键名
5.super,指向当前对象的原型对象,只能用在对象的方法之中
Object的新增方法
Object.is('foo', 'foo') // true 比较两个值是否相等
Object.assign(target, source1, source2) // 用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)
Object.getOwnPropertyDescriptors(obj) // 返回某个对象属性的描述对象
Object.setPrototypeOf(object, prototype) // 设置一个对象的原型对象(prototype),返回参数对象本身
Object.getPrototypeOf(obj) // 读取一个对象的原型对象
Object.keys(obj) // 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名
Object.values(obj) // 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值
Object.entries(obj) // 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组
Object.fromEntries(entries) // Object.entries()的逆操作,用于将一个键值对数组转为对象
扩展运算符的应用
- 替代函数的 apply 方法
// ES5 的写法
Math.max.apply(null, [14, 3, 77])
// ES6 的写法
Math.max(...[14, 3, 77])
// 等同于
Math.max(14, 3, 77);
- 复制数组/对象(浅拷贝)
const a2 = [...a1];
const o2 = {...o1};
- 合并数组/对象
const arr = [...arr1, ...arr2, ...arr3]
const obj = {...obj1, ...obj2, ...obj3}
- 将字符串转为数组
[...'hello'] // [ "h", "e", "l", "l", "o" ]
- 任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。
let nodeList = document.querySelectorAll('div');
let array = [...nodeList];
运算符扩展
- 指数运算符:多个指数运算符连用时,是从最右边开始计算的。
2 ** 2 // 4
2 ** 3 // 8
- 链判断运算符:如果读取对象内部的某个属性,往往需要判断一下,属性的上层对象是否存在。
const firstName = message?.body?.user?.firstName || 'default';
- Null 判断运算符:读取对象属性的时候,如果某个属性的值是null或undefined,有时候需要为它们指定默认值。
const animationDuration = response.settings.animationDuration || 300;
- 逻辑赋值运算符
// 或赋值运算符
x ||= y
// 等同于
x || (x = y)
Symbol
let symbol = Symbol("foo"); // 定义
symbol.description // "foo" // 获取description
Object.getOwnPropertySymbols(obj) // 获取指定对象的所有 Symbol 属性名
// Symbol.for接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局
Symbol.for('foo')
//Symbol.keyFor返回一个已登记的 Symbol 类型值的key
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
Set、Map
Set类似于数组,不同之处在于不存在重复值。
- 属性:constructor、size
- 操作方法:add()、delete()、clear()、has()
- 遍历方法:keys()、values()、entries()、forEach()
- 遍历方法的应用:
// 数组去重
let unique = [...new Set([3, 5, 2, 2, 5, 5])]; // [3, 5, 2]
// 数组的map和filter方法可以间接用于Set
let set = new Set([1, 2, 3]);
set = new Set([...set].map(x => x * 2)); // 返回Set结构:{2, 4, 6}
let set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter(x => (x % 2) == 0)); // 返回Set结构:{2, 4}
// 取并集、交集、差集
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);
// 并集
let union = new Set([...a, ...b]); // Set {1, 2, 3, 4}
// 交集
let intersect = new Set([...a].filter(x => b.has(x))); // set {2, 3}
// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x))); // Set {1}
Map类似于对象,不同之处在于key可以是任何类型的数据。
- 属性:size
- 操作方法:set(key,value)、get()、delete()、clear()、has()、
- 遍历方法:keys()、values()、entries()、forEach()
- Map与其他数据结构的互相转换:
// Map 转 数组
const myMap = new Map()
.set(true, 7)
.set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
// 数组 转 Map
new Map([
[true, 7],
[{foo: 3}, ['abc']]
])
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }
// Map 转 对象
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;
}
// 对象 转 Map
let map = new Map(Object.entries(obj)); // 方法一
// 方法二
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
WeakSet、WeakMap
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别:
- WeakSet 的成员只能是对象,而不能是其他类型的值;
- WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet之中。
- 不能遍历
方法:add()、delete()、has()
应用场景:储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏。
WeakMap 结构与Map结构类似,也是用于生成键值对的集合。WeakMap与Map的区别有两点:
- WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
- WeakMap的键名所指向的对象,不计入垃圾回收机制。
- 不能遍历
方法:get()、set()、has()、delete()
应用场景: 在网页的 DOM 元素上添加数据,就可以使用WeakMap结构。当该 DOM元素被清除,其所对应的WeakMap记录就会自动被移除。
Proxy
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写
var proxy = new Proxy(target, handler)
// target是所要代理的目标对象,这里是{}
// handler是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作
var obj = new Proxy({}, {
//target是目标对象
//propKey是属性名
//receiver是proxy实例本身
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return Reflect.get(target, propKey, receiver);
},
set: function (target, propKey, value, receiver) {
console.log(`setting ${propKey}!`);
return Reflect.set(target, propKey, value, receiver);
}
});
Proxy 支持的拦截操作(13个)
get(target, propKey, receiver) //拦截对象属性的读取,比如proxy.foo和proxy['foo']
set(target, propKey, value, receiver) //拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值
has(target, propKey) //拦截propKey in proxy的操作,返回一个布尔值
deleteProperty(target, propKey) //拦截delete proxy[propKey]的操作,返回一个布尔值
ownKeys(target) //拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性
getOwnPropertyDescriptor(target, propKey) //拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象
defineProperty(target, propKey, propDesc) //拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值
preventExtensions(target) //拦截Object.preventExtensions(proxy),返回一个布尔值
getPrototypeOf(target) //拦截Object.getPrototypeOf(proxy),返回一个对象
isExtensible(target) //拦截Object.isExtensible(proxy),返回一个布尔值
setPrototypeOf(target, proto) //拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截
apply(target, object, args) //拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)
construct(target, args) //拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)
this指向:
目标对象内部的this关键字会指向 Proxy 代理
Proxy 拦截函数内部的this,指向的是handler对象
Reflect
Reflect对象一共有 13 个静态方法,与proxy的拦截一一对应
Reflect.apply(target, thisArg, args)
Reflect.construct(target, args)
Reflect.get(target, name, receiver)
Reflect.set(target, name, value, receiver)
Reflect.defineProperty(target, name, desc)
Reflect.deleteProperty(target, name)
Reflect.has(target, name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
Promise对象
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// success
}, function(error) {
// failure
//一般会省掉这个函数,改为在catch里统一捕获异常,如下代码所示
});
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
特点:
对象的状态不受外界影响。
一旦状态改变,就不会再变,任何时候都可以得到这个结果。
缺点:
无法取消Promise,一旦新建它就会立即执行,无法中途取消。
如果不设置回调函数,Promise内部抛出的错误,不会反映到外部。
当处于pending(等待)状态时,无法得知目前进展到哪一个阶段,是刚刚开始还是即将完成。
状态变化:
pending→fulfilled
pending→rejected
方法:
//只有p1、p2、p3全部返回fulfilled,p才是fulfilled,否则是rejected
let p = Promise.all([promise1, promise2, promise3]);
//只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变
let p = Promise.race([promise1, promise2, promise3]);
// 只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;
// 如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。
let p = Promise.any([promise1, promise2, promise3]);
//将现有对象转为 Promise 对象
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
//返回一个新的 Promise 实例,该实例的状态为rejected
const p = Promise.reject('出错了');
手写一个简单的Promise
class MyPromise {
constructor(callback) {
this.state = "pending";
this.resolveData = null;
this.rejectData = null;
this.quequ = [];
callback(
// resolve
resolveData => {
setTimeout(() => {
this.state = "fulfilled";
this.resolveData = resolveData;
for (const item of this.quequ) {
item.res(item.resolve(resolveData));
}
}, 500);
},
// reject
rejectData => {
this.state = "rejected";
this.rejectData = rejectData;
for (const item of this.quequ) {
item.reject(rejectData);
}
}
);
}
then(resolve, reject) {
return new MyPromise((res, rej) => {
if (this.state === "fulfilled") {
res(resolve(this.resolveData));
} else if (this.state === "rejected") {
reject(this.rejectData);
} else {
this.quequ.push({
resolve,
reject,
res
});
}
});
}
}
Iterator 和 for…of 循环
Iterator 的遍历过程是这样的:
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
for…of和其他遍历的比较
for循环:太麻烦不友好
forEach:无法中途跳出forEach循环,break命令或return命令都不能奏效
for…in:遍历键名,会遍历到原型链上的键,适合遍历对象,可以与break、continue和return配合使用
for…of:遍历键值,适合遍历数组,可以与break、continue和return配合使用
Generator 函数
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
async 函数
async 函数是 Generator 函数的语法糖,让异步操作更为方便。
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
等同于
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
Class
class的用法:本质上是构造函数的语法糖
- 属性还是放在Point.prototype.constructor 上
- 函数还是放在Point.prototype上
- constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。
Point.prototype.constructor === Point // true
- 类的所有实例共享一个原型对象
- 在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为
类不存在变量提升
// ES5写法
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
// Class
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var p = new Point(1, 2);
// x和y都是实例对象point自身的属性(因为定义在this对象上),所以hasOwnProperty()方法返回true
// toString()是原型对象的属性(因为定义在Point类上),所以hasOwnProperty()方法返回false
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
// 类的所有实例共享一个原型对象
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__ // true
// 取值函数和存值函数
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123; // setter: 123
inst.prop // 'getter'
class的静态方法:类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
如果静态方法包含this关键字,这个this指的是类,而不是实例
父类的静态方法,可以被子类继承
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod() // TypeError: foo.classMethod is not a function
class实现继承:通过extends关键字
- 子类必须在constructor方法中调用super方法,否则新建实例时会报错
- 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错
- 父类的静态方法,也会被子类继承
- Object.getPrototypeOf方法可以用来从子类上获取父类
class Point {
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
Module与CommonJS的区别
- CommonJS 模块输出的是一个值的拷贝,可以修改值;Module 输出的是值的引用,只读状态,不能修改值。
- CommonJS 模块是运行时加载,Module 是编译时输出接口。
- CommonJS 模块的require()是同步加载模块,Module 的import命令是异步加载,有一个独立模块依赖的解析阶段。