# JavaScript 基础
# 原始类型
TIP
JavaScript 中原始类型有六种,原始类型既只保存原始值,是没有函数可以调用的。
# 六种原始类型
- string
- number
- boolean
- null
- undefined
- symbol
注意
为什么说原始类型没有函数可以调用,但'1'.toString()却又可以在浏览器中正确执行?
因为'1'.toString()中的字符串'1'在这个时候会被封装成其对应的字符串对象,以上代码相当于new String('1').toString(),因为new String('1')创建的是一个对象,而这个对象里是存在toString()方法的。
# null 到底是什么类型
现在很多书籍把null解释成空对象,是一个对象类型。然而在早期JavaScript的版本中使用的是 32 位系统,考虑性能问题,使用低位存储变量的类型信息,000开头代表对象,而null就代表全零,所以将它错误的判断成Object,虽然后期内部判断代码已经改变,但null类型为object的判断却保留了下来,至于null具体是什么类型,属于仁者见仁智者见智,你说它是一个bug也好,说它是空对象,是对象类型也能理解的通。
# 对象类型
TIP
在 JavaScript 中,除了原始类型,其他的都是对象类型,对象类型存储的是地址,而原始类型存储的是值。
var a = []
var b = a
a.push(1)
console.log(b) // 输出[1]
2
3
4
在以上代码中,创建了一个对象类型a(数组),再把a的地址赋值给了变量b,最后改变a的值,打印b时,b的值也同步发生了改变,因为它们在内存中使用的是同一个地址,改变其中任何一变量的值,都会影响到其他变量。
# 对象当做函数参数
function testPerson(person) {
person.age = 52
person = {
name: '李四',
age: 18
}
return person
}
var p1 = {
name: '张三',
age: 23
}
var p2 = testPerson(p1)
console.log(p1.age) // 输出52
console.log(p2.age) // 输出18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
代码分析:
testPerson函数中,person传递的是对象p1的指针副本- 在函数内部,改变
person的属性,会同步反映到对象p1上,p1对象中的age属性发生了改变,即值为 52 testPerson函数又返回了一个新的对象,这个对象此时和参数person没有任何关系,因为它分配了一个新的内存地址- 以上分析可以用如下图表示

# typeof 和 instanceof
# typeof
TIP
typeof能准确判断除null以外的原始类型的值,对于对象类型,除了函数会判断成function,其他对象类型一律返回object
typeof 1 // number
typeof '1' // string
typeof true // boolean
typeof undefined // undefined
typeof Symbol() // symbol
typeof [] // object
typeof {} // object
typeof console.log // function
2
3
4
5
6
7
8
9
# instanceof
TIP
instanceof通过原型链可以判断出对象的类型,但并不是百分百准确
function Person(name) {
this.name = name
}
var p1 = new Person()
console.log(p1 instanceof Person) // true
var str = new String('abc')
console.log(str instanceof String) // true
2
3
4
5
6
7
8
# 类型转换
JavaScript中,类型转换只有三种:
- 转换成数字
- 转换成布尔值
- 转换成字符串
# 经典类型面试题
console.log([] == ![]) // true
代码分析:
- 左侧是一个对象(数组)
- 右侧是一个布尔值,对象
[]转换成布尔值true,因为除了null所有对象都转换成布尔值,所以![]结果为false - 此时相当于
对象==布尔值,依据类型转换规则,转换成数字类型进行比较 - 对象(空数组)转换成
0,布尔值false转换成0 - 即
0==0,返回true
类型转换规则,如下图:

# == 和 ===
如何你对上面的例子还一知半解,那么我们来详细介绍一下==和===的规则以及区别。
# ===严格相等
TIP
===叫做严格相等,是指:左右两边不仅值要相等,类型也要相等,例如'1'===1的结果是false,因为一边是string,另一边是number。
console.log('1' === 1) // 输出false
# ==不严格相等
TIP
==不像===那样严格,对于一般情况,只要值相等,就返回true,但==还涉及一些类型转换,它的转换规则如下:
- 两边的类型是否相同,相同的话就比较值的大小,例如
1==2,返回false - 类型不相同会进行类型转换
- 判断的是否是
null和undefined,是的话就返回true - 判断的类型是否是
String和Number,是的话,把String类型转换成Number,再进行比较 - 判断其中一方是否是
Boolean,是的话就把Boolean转换成Number,再进行比较 - 如果其中一方为
Object,且另一方为String、Number或者Symbol,会将Object转换成原始类型后,再进行比较
1 == {id: 1, name: 'AAA'}
↓
1 == '[object Object]'
2
3
# 转 boolean
除了undefined、null、false、0、-0、NaN和空字符串转换成false以外,其他所有值都转换成true,包括所有对象。
# 对象转原始类型
对象转原始类型,会调用内置的[ToPrimitive]函数,对于该函数而言,其逻辑如下:
- 是否已经是原始类型,是则直接返回
- 调用
valueOf(),如果转换为原始类型,则返回 - 调用
toString(),如果转换为原始类型,则返回 - 也可以重写
Symbol.toPrimitive()方法,优先级别最高 - 如果都没有返回原始类型,会报错
var obj = {
value: 0,
valueOf() {
return 1
},
toString() {
return '2'
},
[Symbol.toPrimitive]() {
return 3
}
}
console.log(obj + 1) // 输出4
2
3
4
5
6
7
8
9
10
11
12
13
# 对象转原始类型应用
// 问:如何使if(a==1&&a==2&&a==3) {console.log('true')};正确打印'true'
var a = {
value: 0,
valueOf() {
this.value++
return this.value
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('true') // 输出true
}
2
3
4
5
6
7
8
9
10
11
代码分析:
- 重写对象
a的valueOf()方法,使value属性每次调用时自增 - 当判断
a==1时,第一次调用valueOf()方法,此时value等于 1,判断1==1,继续向下走 - 判断
a==2时,第二次调用valueOf()方法,此时value等于 2,判断2==2,继续向下走 - 判断
a==3时,第三次调用valueOf()方法,此时value等于 3,判断3==3,if判断结束 if条件判断为true && true && true,执行console.log('true'),打印true
# new 构造调用的过程
无论是通过字面量还是通过new进行构造函数调用创建出来的对象,其实都一样。调用new的过程如下:
- 创建一个新对象
- 原型绑定
- 绑定 this 到这个新对象上
- 返回新对象
# this 全解析
JavaScript中的this只有如下几种情况,并按他们的优先级从低到高划分如下:
- 独立函数调用,例如
getUserInfo(),此时this指向全局对象window - 对象调用,例如
stu.getStudentName(),此时this指向调用的对象stu call()、apply()和bind()改变上下文的方法,this指向取决于这些方法的第一个参数,当第一个参数为null时,this指向全局对象window- 箭头函数没有
this,箭头函数里面的this只取决于包裹箭头函数的第一个普通函数的this new构造函数调用,this永远指向构造函数返回的实例上,优先级最高。
var name = 'global name'
var foo = function() {
console.log(this.name)
}
var Person = function(name) {
this.name = name
}
Person.prototype.getName = function() {
console.log(this.name)
}
var obj = {
name: 'obj name',
foo: foo
}
var obj1 = {
name: 'obj1 name'
}
// 独立函数调用,输出:global name
foo()
// 对象调用,输出:obj name
obj.foo()
// apply(),输出:obj1 name
obj.foo.apply(obj1)
// new 构造函数调用,输出:p1 name
var p1 = new Person('p1 name')
p1.getName()
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
# this 解析流程图

# 闭包
TIP
当一个函数能够记住并访问它所在的词法作用域的时候,就产生了闭包,即使函数式在词法作用域之外执行
# 闭包的几种表现形式
TIP
- 返回一个函数
- 作为函数参数传递
- 回调函数
- 非典型闭包 IIFE(立即执行函数表达式)
返回一个函数:这种形式的闭包在JavaScript的代码编写中,是非常常见的一种方式。
var a = 1
function foo() {
var a = 2
// 这就是闭包
return function() {
console.log(a)
}
}
var bar = foo()
// 输出2,而不是1
bar()
2
3
4
5
6
7
8
9
10
11
作为函数参数传递:无论通过何种手段将内部函数传递到它所在词法作用域之外,它都会持有对原始作用域的引用,无论在何处执行这个函数,都会产生闭包。
var a = 1
function foo() {
var a = 2
function baz() {
console.log(a)
}
bar(baz)
}
function bar(fn) {
// 这就是闭包
fn()
}
// 输出2,而不是1
foo()
2
3
4
5
6
7
8
9
10
11
12
13
14
回调函数:在定时器、事件监听、Ajax 请求、跨窗口通信、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。
// 定时器
setTimeout(function timeHandler(){
console.log('timer');
},100)
// 事件监听
$('#container').click(function(){
console.log('DOM Listener');
})
2
3
4
5
6
7
8
9
IIFE:IIFE(立即执行函数表达式)并不是一个典型的闭包,但它确实创建了一个闭包。
var a = 2
;(function IIFE() {
// 输出2
console.log(a)
})()
2
3
4
5
# 经典循环和闭包面试题
TIP
以下代码运行结果是什么,如何改进?
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
2
3
4
5
代码分析:
for循环创建了 5 个定时器,并且定时器是在循环结束后才开始执行for循环结束后,用var i定义的变量i此时等于 6- 依次执行五个定时器,都打印变量
i,所以结果是打印 5 次 6
第一种改进方法:利用IIFE(立即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中
for (var i = 1; i <= 5; i++) {
;(function(j) {
setTimeout(function timer() {
console.log(j)
}, i * 1000)
})(i)
}
2
3
4
5
6
7
第二种方法:setTimeout函数的第三个参数,可以作为定时器执行时的变量进行使用
for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(j) {
console.log(j)
},
i * 1000,
i
)
}
2
3
4
5
6
7
8
9
第三种方法(推荐):在循环中使用let i代替var i
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
2
3
4
5
# 原型、原型链
撰写中......
# 浅拷贝、深拷贝
由于JavaScript中对象是引用类型,保存的是地址,深、浅拷贝的区别是,当拷贝结束后,在一定程度上改变原对象中的某一个引用类型属性的值,新拷贝出来的对象依然受影响的话,就是浅拷贝,反之就是深拷贝。
# 浅拷贝的几种实现方法
TIP
- 利用
Object.assign()方法 - 利用
...扩展运算符
第一种方法: Object.assign()会拷贝原始对象中的所有属性到一个新对象上,如果属性为对象,则拷贝的是对象的地址,改变对象中的属性值,新拷贝出来的对象依然会受影响。
var obj = {
name: '张三',
age: 23,
isStudent: false,
job: {
name: 'FE',
money: 12
}
}
var newObj = Object.assign({}, obj)
obj.job.money = 21
console.log(newObj.name) // 输出张三
console.log(newObj.age) // 输出23
console.log(newObj.job.money) // 输出21,受影响
2
3
4
5
6
7
8
9
10
11
12
13
14
第二种方法:...扩展运算符是ES6新增加的内容
var obj = {
name: '张三',
age: 23,
isStudent: false
}
var newObj = { ...obj }
console.log(newObj.name) // 输出张三
console.log(newObj.age) // 输出23
2
3
4
5
6
7
8
# 深拷贝几种实现方式
TIP
- 配合使用
JSON.parse()和JSON.stringify()两个函数(局限性比较大) - 实现自己的简易深拷贝方法
lodash第三方库实现深拷贝
第一种方法: 利用JSON的序列化和反序列化方法,可以实现简易对象深拷贝,但此种方法有较大的限制:
- 会忽略属性值为
undefined的属性 - 会忽略属性为
Symbol的属性 - 不会序列化函数
- 不能解决循环引用的问题,直接报错
var obj = {
name: '张三',
age: 23,
address: undefined,
sayHello: function() {
console.log('Hello')
},
isStudent: false,
job: {
name: 'FE',
money: 12
}
}
var newObj = JSON.parse(JSON.stringify(obj))
obj.job.money = 21
console.log(newObj.name) // 输出张三
console.log(newObj.age) // 输出23
console.log(newObj.job.money) // 输出12
console.log(newObj.address) // 报错
console.log(newObj.sayHello()) // 报错
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
第二种: 实现自己简易的深拷贝函数
function deepClone(obj) {
function isObject(o) {
return (typeof o === 'object' || typeof o === 'function') && o !== null
}
if (!isObject(obj)) {
throw new Error('非对象')
}
var isArray = Array.isArray(obj)
var newObj = isArray ? [...obj] : { ...obj }
Reflect.ownKeys(newObj).forEach(key => {
newObj[key] = isObject(newObj[key]) ? deepClone(newObj[key]) : newObj[key]
})
return newObj
}
var obj = {
name: 'AAA',
age: 23,
job: {
name: 'FE',
money: 12000
}
}
var cloneObj = deepClone(obj)
obj.job.money = 13000
console.log(obj.job.money) // 输出13000
console.log(cloneObj.job.money) // 输出12000
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
第三种方法: 使用lodash第三方函数库实现(需要先引入 lodash.js)
var obj = {
name: '张三',
age: 23,
isStudent: false,
job: {
name: 'FE',
money: 12
}
}
var newObj = _.cloneDeep(obj)
obj.job.money = 21
console.log(newObj.name) // 输出张三
console.log(newObj.age) // 输出23
console.log(newObj.job.money) // 输出12,不受影响
2
3
4
5
6
7
8
9
10
11
12
13
14
# 继承
在JavaScriptES6 之前,实现继承需要依赖原型、原型链和构造函数等等技术手段组合使用,在 ES6 之后,可以使用Class类继承(并没有真正的类,只是一个语法糖,实质依然是函数)
继承的几种方式
- 原型链实现继承
- 借用构造函数实现继承
- 组合继承
- 寄生组合继承
- 类继承
# 原型链实现继承
TIP
通过重写子类的原型,并将它指向父类的手段实现。这种方式实现的继承,创建出来的实例既是子类的实例,又是父类的实例。 优点:
- 简单,易于实现;
- 父类新增方法、原型属性,子类都能访问到 缺陷:
- 无法实现多继承,因为原型只能被一个实例更改;
- 不能向父类构造函数传参;
- 父类上的引用类型属性.会被所有实例共享,其中一个实例改变时,会影响其他实例
function Animal() {
this.colors = ['red', 'blue']
}
function Dog(name) {
this.name = name
}
Dog.prototype = new Animal()
var dog1 = new Dog('旺财')
var dog2 = new Dog('钢镚')
dog2.colors.push('yellow')
console.log(dog1.colors) // ["red", "blue", "yellow"]
console.log(dog2.colors) // ["red", "blue", "yellow"]
console.log(dog1 instanceof Dog) // true
console.log(dog1 instanceof Animal) // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 借用构造函数实现继承
TIP
借用构造函数实现继承,通过在子类中使用call()方法,实现借用父类构造函数并向父类构造函数传参的目的。
优点:
- 解决了原型链中子类实例共享父类引用属性的问题。
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(call 多个父类对象) 缺陷:
- 无法继承父类原型对象上的属性和方法;
- 实例并非父类的实例,而是子类的实例;
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
function Animal(name) {
this.name = name
this.colors = ['red', 'blue']
}
Animal.prototype.eat = function() {
console.log(this.name + ' is eating!')
}
function Dog(name) {
Animal.call(this, name)
}
var dog1 = new Dog('旺财')
var dog2 = new Dog('钢镚')
dog2.colors.push('yellow')
console.log(dog1.colors) // ["red", "blue"]
console.log(dog2.colors) // ["red", "blue", "yellow"]
console.log(dog1 instanceof Dog) // true
console.log(dog2 instanceof Animal) // false
console.log(dog1.eat()) // 报错
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 组合继承
TIP
组合继承是组合了原型链继承和借用构造函数继承这两种方法,它保留了两种继承方式的优点,但它并不是百分百完美的。
优点:
- 弥补构造函数的缺陷,既可继承实例的属性和方法,也可以继承原型的属性和方法;
- *同时是父子类的实例;*
- 可向父类传递参数
- 函数可复用
缺陷: 每次创建子类实例都执行了两次构造函数(
Parent.call()和new Parent()),虽然这并不影响对父类的继承,但子类创建实例时,原型中会存在两份相同的属性和方法,这并不优雅。
function Animal(name) {
this.name = name
this.colors = ['red', 'blue']
}
Animal.prototype.eat = function() {
console.log(this.name + ' is eatting')
}
function Dog(name) {
Animal.call(this, name)
}
Dog.prototype = new Animal() // 第一次调用
var dog1 = new Dog('dog1') // 第二次调用
var dog2 = new Dog('dog2') // 第三次调用
dog1.colors.push('yellow')
console.log(dog1.name) // 输出dog1
console.log(dog2.colors) // 输出['red','blue']
console.log(dog2.eat()) // 输出dog2 is eatting
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 寄生组合继承
TIP
寄生组合继承是在组合继承的基础上,采用Object.create()方法来改造实现
function Animal(name) {
this.name = name
this.colors = ['red', 'blue']
}
Animal.prototype.eat = function() {
console.log(this.name + ' is eatting')
}
function Dog(name) {
Animal.call(this, name)
}
//Dog.prototype = Animal.prototype 与下面一句效果一致
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog
var dog1 = new Dog('dog1')
var dog2 = new Dog('dog2')
dog1.colors.push('yellow')
console.log(dog1.name) // 输出dog1
console.log(dog2.colors) // 输出['red','blue']
console.log(dog2.eat()) // 输出dog2 is eatting
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Class 实现继承
TIP
运用 ES6 class 新特性来实现继承
class Animal {
constructor(name) {
this.name = name
this.colors = ['red', 'blue']
}
eat() {
console.log(this.name + ' is eatting')
}
}
class Dog extends Animal {
constructor(name) {
super(name)
}
}
var dog1 = new Dog('dog1')
var dog2 = new Dog('dog2')
dog1.colors.push('yellow')
console.log(dog1.name) // 输出dog1
console.log(dog2.colors) // 输出['red','blue']
console.log(dog2.eat()) // 输出dog2 is eatting
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ES6
本章节只介绍 ES6 常考知识点,更多基础知识请直接跳转至[你不知道的 JavaScript(中)](/books/javascript/你不知道的 JavaScript 中卷.md)
# var、let 和 const 的区别
TIP
var声明的变量会提升到作用域的顶部,而let和const不会进行提升var声明的全局变量会被挂载到全局window对象上,而let和const不会var可以重复声明同一个变量,而let和const不会var声明的变量作用域范围是函数作用域,而let和const声明的变量作用域范围是块级作用域。const声明的常量,一旦声明则不能再次赋值,再次赋值会报错(更改对象属性不会,因为对象地址没有变)
作用域提升:
console.log(a) // 输出undefined
console.log(b) // 报错
console.log(PI) // 报错
var a = 'abc'
let b = 'ABC'
const PI = 3.1415
2
3
4
5
6
挂载到全局变量:
var a = 'abc'
let b = 'ABC'
const PI = 3.1415
console.log(window.a) // 输出abc
console.log(window.b) // 输出undefined
console.log(window.PI) // 输出undefined
2
3
4
5
6
7
重复声明变量:
var a = 'abc'
var a
console.log(a) // 输出abc
let b = 'ABC'
let b // 报错
2
3
4
5
6
变量的作用域范围:
function foo() {
var flag = true
if (flag) {
var a = 'abc'
let b = 'ABC'
console.log(a) // 输出abc
console.log(b) // 输出ABC
}
console.log(a) // 输出abc
console.log(b) // 报错
}
foo()
2
3
4
5
6
7
8
9
10
11
12
const 常量:
const PI = 3.1415
PI = 3.1415926 // 报错
2
# 扩展/收缩符
TIP
ES6 新增加的运算符...,称为扩展或者收缩,具体作用取决于到底如何使用。
// ...的扩展
function foo(x, y, z) {
console.log(x, y, z) // 输出1,2,3
}
var arr = [1, 2, 3]
foo(...arr) // 扩展数组:ES6写法
foo.apply(null, arr) // 扩展数组:ES5写法
// ...的收缩
// 1.收集参数:ES6写法
function bar(...arr) {
console.log(arr) // 输出[1,2,3,4,5]
}
// 2.收集参数:ES5写法
function foo() {
var args = Array.prototype.slice.call(arguments)
console.log(args) // 输出[1,2,3,4,5]
}
bar(1, 2, 3, 4, 5)
foo(1, 2, 3, 4, 5)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 解构赋值
TIP
常用的解构赋值,有如下两种情况:
- 对象的解构
- 数组的解构
// 常用解构方式:解构对象 or 解构数组
// ES6之前的获取返回数组和返回对象的方式
function foo() {
return [1, 2, 3]
}
function bar() {
return {
X: 4,
Y: 5,
Z: 6
}
}
var arr = foo()
var a = arr[0]
var b = arr[1]
var c = arr[2]
var obj = bar()
var x = obj.X
var y = obj.Y
var z = obj.Z
console.log(a, b, c) // 输出1,2,3
console.log(x, y, z) // 输出4,5,6
// ES6之后获取返回数组和返回对象的方式
var [A, B, C] = foo()
var { X, Y, Z } = bar()
console.log(A, B, c) // 输出1,2,3
console.log(X, Y, Z) // 输出4,5,6
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
# 字符串模板
TIP
${内容}:字符串模板里的内容可以是变量、函数调用以及表达式。
// 字符串模板
var name = 'why'
var age = 23
var address = '广州'
// ES5拼接字符串
var str = '我叫:' + name + ',我的年龄是:' + age + ',我的地址是:' + address
// ES6模板字符串
var newStr = `我叫:${name},我的年龄是:${age},我的地址是:${address}`
console.log(str) // 输出:我叫:why,我的年龄是:23,我的地址是:广州
console.log(newStr) // 输出:我叫:why,我的年龄是:23,我的地址是:广州
2
3
4
5
6
7
8
9
10
11
12
13
# map 和 set 结构
Map 结构: 对象是创建无序键值对数据结构映射的主要机制,在 ES6 之前,对象的属性只能是字符串,在 ES6 之后,Map结构允许使用对象、数组等作为键。Map结构的方法或者属性如下:
set():新增一个 map 结构的数据get(key):根据键获取值size:获取 map 结构的长度delete(key):根据指定的键删除has(key):判断指定的键是否存在于 map 结构中keys()遍历,values()遍历,entries()键值对遍历clear()清空 map 结构
// Map结构
var map = new Map()
var x = { id: 1 },
y = { id: 2 }
// 设置map数据
map.set(x, 'bar')
map.set(y, 'foo')
// 获取map数据
console.log(map.get(x)) // 输出bar
console.log(map.get(y)) // 输出foo
// 获取map结构的长度
console.log(map.size) // 输出2
// 根据指定键删除map数据
map.delete(x)
// 根据指定的键判断是否存在于map结构中
console.log(map.has(x)) // 输出false
// 遍历map键
for (var key of map.keys()) {
console.log(key) // 输出{id:2}
}
// 遍历map值
for (var value of map.values()) {
console.log(value) // 输出foo
}
// 遍历map键值对
for (var item of map.entries()) {
console.log(item[0]) // 输出y
console.log(item[1]) // 输出{id:2}
}
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
35
36
37
Set 结构: Set是一个集合,它里面的值是唯一的,重复添加会被忽略(Set结构不允许强制类型转换,1和"1"被认为是两个不同的值)。Set结构的方法和属性如下:
add():添加新值size:获取Set结构的长度delete():根据指定的键删除has():判断指定的键是否存在Set集合中keys()遍历、values()遍历、entries()遍历clear():清空Set结构
// Set结构
var set = new Set()
var x = { id: 1 }
var y = { id: 2 }
var a = 1
var b = '1'
var c = true
// 添加Set数据
set.add(x)
set.add(y)
set.add(a)
set.add(b)
set.add(c)
// 获取Set数据的长度
console.log(set.size) // 输出5
// 删除Set数据
set.delete(c)
// 判断某个值是否存在Set结构中
console.log(set.has(c)) // 输出false
// 遍历Set的键
for (var key of set.keys()) {
console.log(key) // 输出{id:1} {id:2} 1 "1"
}
// 遍历Set的值
for (var value of set.values()) {
console.log(value) // 输出{id:1} {id:2} 1 "1"
}
// 遍历Set的键值对
for (var item of set.entries()) {
console.log(item[0]) // 输出 {id:1} {id:2} 1 "1"
console.log(item[1]) // 输出 {id:1} {id:2} 1 "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
35
36
37
38
39
Set 结构的扩展运用: 数组去重、并集、交集、差集
// Set集合的运用:数组的去重、并集、交集、差集
var arr1 = [1, 2, 1, 3, 4, 5]
var arr2 = [4, 5, 6, 7]
// 去重:输出1,2,3,4,5
console.log(Array.from(new Set(arr1)))
// 并集:输出1,2,3,4,5,6,7
var union = Array.from(new Set([...set1, ...set2]))
console.log(union)
// 交集:输出4,5
var intec = Array.from(new Set(arr.filter(x => arr1.includes(x))))
console.log(intec)
// 差集
var diff1 = Array.from(new Set(arr1.filter(x => !arr2.includes(x))))
var diff2 = Array.from(new Set(arr2.filter(x => !arr1.includes(x))))
console.log(diff1) // 输出:1,2,3
console.log(diff2) // 输出:6,7
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Proxy 能干什么
在Vue2.0+的版本中,Vue使用Object.definedProperty()方法来实现数据的响应式,在Vue3.0的开发计划中,作者计划使用 ES6 新增加的Proxy代理来实现数据的响应式,它相比于Object.definedProperty()有如下几个特点:
Proxy可以一次性为所有属性实现代理,无需遍历,性能更佳Proxy能监听到以前使用Object.definedProperty()监听不到的数据变动。- 由于是 ES6 新增加的特性,所以浏览器兼容性方面比
Object.definedProperty()差
let onWatch = function(obj, setBind, getLogger) {
return new Proxy(obj, {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver)
},
set(target, property, value, receiver) {
setBind(value, property)
return Reflect.set(target, property, value)
}
})
}
let obj = { a: 1 }
let p = onWatch(
obj,
(value, property) => {
console.log(`监听到${property}属性的改变,其值为${value}`)
},
(target, property) => {
console.log(`监听到获取属性${property},其值为${target[property]}`)
}
)
p.a = 2 // 监听到a属性的改变,其值为2
console.log(a) // 监听到获取属性a,其值为2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 数组的 map、filter 和 reduce 的区别
map: map方法的作用是生成一个新数组(把原数组中的所有元素做一些变动,放进新数组中)
var newArr = [1, 2, 3].map(v => v * 2)
console.log(newArr) // 输出[2,4,6];
2
filter: filter方法的作用是从原数组中过滤出符合条件的元素,并生成一个新数组
var newArr = [1, 2, 3, 4, 5, 6].filter(item => item % 2 == 0)
console.log(newArr) // 输出[2,4,6];
2
reduce: reduce方法的作用是通过回调函数的形式,把原数组中的元素最终转换成一个值,第一个参数是回调函数,第二个参数是初始值
var arr = [1, 2, 3, 4, 5, 6]
var sum = arr.reduce((account, current) => {
return account + current
}, 0)
console.log(sum) // 21
2
3
4
5
# JavaScript 异步
# JS 为什么是单线程
js 作为浏览器脚本语言,其主要用途是与用户互动,以及操作 DOM。这就决定了它只能是单线程,否则会带来很复杂的同步问题。(假设它同时拥有两个线程,一个线程在 DOM 节点里添加内容,而另一个线程又删除了该节点,此时浏览器应执行哪个线程为准?)
# 并发和并行
并行和并发是两个概念,容易混淆是因为并行和并发在中文意思上相近,其实在英文中,这是完全不相同的东西,并行(parallelism)、并发(concurrency)
概念理解
并行(parallelism):是微观概念,假设 CPU 有两个核心,则我们就可以同时完成任务 A 和任务 B,同时完成多个任务的情况就可以称之为并行。
并发(concurrency):是宏观概念,现在有任务 A 和任务 B,在一段时间内,通过任务之间的切换完成这两个任务,这种情况称之为并发。
# 回调函数
回调函数广泛存在于我们所编写的JavaScript代码中,它表现在事件绑定,Ajax 请求或者其他的情况下,一个回调函数可表现成如下形式
ajax(url, () => {
console.log('这里是回调函数')
})
2
3
回调地狱: 回调函数很好的解决了某些异步情况,但过度滥用回调函数会造成回调地狱,即回调函数过长,嵌套过深。过长或者嵌套过深的回调函数,会让回调函数存在强耦合关系,一旦有一个函数有所改动,那么可能会牵一发而动全身。一个回调地狱可能如下所示:
ajax(firstUrl, () => {
console.log('这里是首次回调函数')
ajax(secondUrl, () => {
console.log('这里是第二次回调函数')
ajax(threeUrl, () => {
console.log('这里是第三次回调函数')
// todo更多
})
})
})
2
3
4
5
6
7
8
9
10
# Generator
在 ES6 之前,一个函数一旦执行将不会被中断,一直到函数执行完毕,在 ES6 之后,由于Generator的存在,函数可以暂停自身,待到合适的机会再次执行。用Generator可以解决回调地狱。
function* fetch() {
yield ajax(url, () => {
console.log('这里是首次回调函数')
})
yield ajax(url, () => {
console.log('这里是第二次回调函数')
})
yield ajax(url, () => {
console.log('这里是第三次回调函数')
})
}
var it = fetch()
var result1 = it.next()
var result2 = it.next()
var result3 = it.next()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Promise
Promise翻译过来就是承诺的意思,Promise一共有三种状态:pending(等待中)、resolve(完成)和reject(拒绝),这个承诺意味着在将来一定会有一个表决,并且只能表决一次,表决的状态一定是resolve(完成)或者reject(拒绝),一个Promise可能会是如下的形式:
// 普通的Promise
function foo() {
return new Promise((resolve, reject) => {
// 第一次表决有效,其后无论是resolve()还是reject()都无效
resolve(true)
resolve(false)
})
}
// Promise解决回调地狱
ajax(url)
.then(res => {
console.log('这里是首次回调函数')
})
.then(res => {
console.log('这里是第二次回调函数')
})
.then(res => {
console.log('这里是第三次回调函数')
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Promise.all(): Promise.all()方法是把一个或者几个Promise组合在一个数组里,只有当数组中的所有Promise全部表决完成,才返回。
var p1 = Promise.resolve(1)
var p2 = new Promise(resolve => {
setTimeout(() => {
resolve(2)
}, 100)
})
var p3 = 3
Promise.all([p1, p2, p3]).then(res => {
console.log(res) // 输出[1,2,3]
})
2
3
4
5
6
7
8
9
10
Promise.race(): Promise.race()方法把一个或者几个Promise组合在一个数组里,只要数组中有一个表决了,就返回。
var p1 = Promise.resolve(1)
var p2 = new Promise(resolve => {
setTimeout(() => {
resolve(2)
}, 100)
})
var p3 = 3
Promise.race([p2, p1, p3]).then(res => {
console.log(res) // 输出1
})
2
3
4
5
6
7
8
9
10
Promise.allSettled(): 该Promise.allSettled()方法返回一个在所有给定的promise已被决议或被拒绝后决议的promise,并带有一个对象数组,每个对象表示对应的promise结果(fulfilled成功,rejected失败)。
const promise1 = Promise.resolve(3)
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'))
const promises = [promise1, promise2]
Promise.allSettled(promises).then(results => results.forEach(result => console.log(result.status)))
// "fulfilled"
// "rejected"
2
3
4
5
6
7
8
Promise.any(): Promise.any()接收一个Promise可迭代对象,只要其中的一个 promise 完成,就返回那个已经有完成值的 promise 。如果可迭代对象中没有一个promise 完成(即所有的 promises 都失败/拒绝),就返回一个拒绝的 promise,返回值还有待商榷:无非是拒绝原因数组或AggregateError类型的实例,它是Error 的一个子类,用于把单一的错误集合在一起。本质上,这个方法和Promise.all()是相反的。
var p1 = Promise.resolve(1)
var p2 = new Promise(resolve => {
setTimeout(() => {
resolve(2)
}, 100)
})
var p3 = 3
Promise.race([p2, p1, p3]).then(res => {
console.log(res) // 输出1
})
2
3
4
5
6
7
8
9
10
# 如何取消一个 promise
# 方法 1: 利用promise.race
function wrap(p) {
let obj = {};
let p1 = new Promise((resolve, reject) => {
obj.resolve = resolve;
obj.reject = reject;
});
obj.promise = Promise.race([p1, p]);
return obj;
}
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(123);
}, 1000);
});
let obj = wrap(promise);
obj.promise.then(res => {
console.log(res);
});
obj.resolve("请求被拦截了");
obj.reject("请求被拒绝了");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 方法二: 封装一个可控的promise对象
function wrap(p){
let res = null
let abort = null
const promise = new Promise((resolve, reject)=>{
res = resolve
abort = reject
})
promise.abort = abort
p.then(res, abort)
return promise
}
const promise = new Promise((resolve, reject)=>{
setTimiout(()=>{
console.log(123)
})
})
const obj = wrap(promise)
obj.then(res=>{
console.log(res)
})
obj.abort('请求被拦截')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# async/await
如果一个方法前面加上了async,那么这个方法就会返回一个Promise,async就是将函数用Promise.resolve()包裹了下,并且await只能配合async使用,不能单独出现。一个async/await可能会是如下的形式:
// 普通的async/await
async function foo() {
let number = await 3 // await自动用promise.resolve()包装
console.log(number)
}
foo()
// async/await解决回调地狱
async function fetch() {
var result1 = await ajax(url1)
var result2 = await ajax(url2)
var result3 = await ajax(url3)
}
fetch()
2
3
4
5
6
7
8
9
10
11
12
13
14
# setInterval、setTimeout 和 requestAnimationFrame
setTimeout setTimeout延时执行某一段代码,但setTimeout由于EventLoop的存在,并不百分百是准时的,一个setTimeout可能会表示如下的形式:
// 延时1s之后,打印hello,world
setTimeout(() => {
console.log('hello,world')
}, 1000)
2
3
4
setInterval: setInterval在指定的时间内,重复执行一段代码,与setTimeout类似,它也不是准时的,并且有时候及其不推荐使用setInterval定时器,因为它与某些耗时的代码配合使用的话,会存在执行积累的问题,它会等耗时操作结束后,一起一个或者多个执行定时器,存在性能问题。一个setInterval可能会表示如下的形式:
setInterval(() => {
console.log('hello,world')
}, 1000)
2
3
requestAnimationFrame: 翻译过来就是请求动画帧,它是 html5 专门用来设计请求动画的 API,它与setTimeout相比有如下优势:
- 根据不同屏幕的刷新频率,自动调整执行回调函数的时机。
- 当窗口处于未激活状态时,
requestAnimationFrame或停止执行,而setTimeout不会 - 自带函数节流功能
var progress = 0
var timer = null
function render() {
progress += 1
if (progress <= 100) {
console.log(progress)
timer = window.requestAnimationFrame(render)
} else {
cancelAnimationFrame(timer)
}
}
//第一帧渲染
window.requestAnimationFrame(render)
2
3
4
5
6
7
8
9
10
11
12
13
# EventLoop 事件循环
# 进程和线程
TIP
JavaScript是单线程执行的,在JavaScript运行期间,有可能会阻塞 UI 渲染,这在一方面说明JavaScript引擎线程和 UI 渲染线程是互斥的。JavaScript被设计成单线程的原因在于,JavaScript可以修改 DOM,如果在 js 工作期间,UI 还在渲染的话,则可能不会正确渲染 DOM。单线程也有一些好处,如下:
- 节省内存空间
- 节省上下文切换时间
- 没有锁的问题存在
进程: CPU 在运行指令及加载和保存上下文所需的时间,放在应用上一个程序就是一个进程,一个浏览器 tab 选项卡就是一个进程
线程: 线程是进程中更小的单位,描述了执行一段指令所需的时间。
# 执行栈
TIP
可以把执行栈看成是一个存储函数调用的栈结构,遵循先进后出的原则,一个执行栈可能表现如下:

# EventLoop
上面讲到函数会在执行栈中执行,那么当遇到异步代码后,该如何处理呢?其实当遇到异步代码的时候,会被挂起在 Task 队列中,一旦执行栈为空,就会从 Task 中拿出需要执行的代码执行,所以本质上讲 JS 中的异步还是同步行为。
如上图,可以看到,不同的异步任务是有区别的,异步任务又可以划分如下:
- 宏任务(
script、setTimeout、setInterval、setImmidiate、I/O、UI Rendering)可以有多个队列 - 微任务(
procress.nextTick、Promise.then、Object.observe、mutataionObserver)只能有一个队列
执行顺序: 当执行栈执行完毕后,会首先执行微任务队列,当微任务队列执行完毕再从宏任务中读取并执行,当再次遇到微任务时,放入微任务队列。
setTimeout(() => {
console.log(1)
Promise.resolve().then(() => {
console.log(2)
})
}, 0)
setTimeout(() => {
console.log(3)
}, 0)
Promise.resolve().then(() => {
console.log(4)
})
console.log(5)
// 输出结果:5 4 1 2 3
2
3
4
5
6
7
8
9
10
11
12
13
14
代码分析:
console.log(5)是唯一的同步任务,首先执行,输出 5- 将所有异步任务放在 Task 队列中,挂起
- 同步任务执行完毕,开始执行微任务队列,即
Promise.then,输出 4 - 微任务队列执行完毕,执行宏任务队列
setTimeout - 宏任务队列中首先执行同步任务,再次遇到微任务,放入微任务队列中,输出 1
- 同步任务执行完毕,执行微任务队列,输出 2
- 微任务队列执行完毕,执行宏任务队列
setTimeout,输出 3