一、变量类型和计算
1. 值类型和引用类型
1.1 值类型
值类型变量存储在栈中(如:Undefined、String、Number、Boolean、Symbol)
1.2 引用类型
引用类型变量的内存地址存储在栈中,值存储在堆中(如:Object、Null、Array、Function)
1.3 浅拷贝
仅能拷贝指向值类型的对象属性,可使用 Object.assign({},obj)
或剩余运算符 {...obj}
1.4 深拷贝
彻底拷贝一个对象,对原对象无影响
/**
* 深拷贝
* @param { Object } obj - 要拷贝的对象
* @returns { Object } result - 拷贝后的副本
*/
function deepClone(obj = {}) {
if (typeof obj !== 'object' || obj == null) return obj
let result
if (obj instanceof Array) {
result = []
} else {
result = {}
}
for(let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = deepClone(obj[key])
}
}
return result
}
2. 类型判断
2.1 typeof 运算符
- 能识别所有值类型
- 能识别函数
- 判断是否是引用类型(不可再细分)
3. 逻辑运算
逻辑运算中,javascript 会自动进行强制类型转换
3.1 字符串拼接
- 数字 + 字符串 = 字符串
- 布尔值 + 字符串 = 字符串
可使用 parseInt(string, radix) 和 Number(obj) 将字符串或对象转换为数字
const num1 = parseInt('10.2') // parseInt 转化的是整数
const num2 = Number('10.2') // 另一种语法糖写法:+'10.2'
console.log(num1) // 10
console.log(num2) // 10.2
3.2 == 运算符
双等号会进行强制类型转换,除了排除 null 和 undefined 外,建议都用 ===
// 举例,以下均为 true
100 == ‘100’
0 == ‘’
0 == false
false == ''
null == undefined
3.3 if 语句和逻辑运算
- truly 变量:!!a === true 的变量
- falsely 变量:!!a === false 的变量
if 语句和逻辑运算符 &&、||、! 等均是通过 truly 和 falsely 变量判断的
二、原型和原型链
1、 class 和继承
1.1 class
- 类(class)包括 constructor、属性、方法
- class 是一个语法糖,实际上是一个 function
``javascript class Student { constructor(name, number) { this.name = name this.number = number } sayHi() { console.log(
姓名:${this.name},学号:${this.number}`) } }
// 通过类声明对象/示例 const xialuo = new Student(‘夏洛’, 100) console.log(xialuo.name) console.log(xialuo.number) xialuo.sayHi()
<a name="NZYqg"></a>
#### 1.2 继承
继承主要有以下关键点
- extends(关键字)
- super(执行父类的构建过程)
- 扩展或重写方法
```javascript
// 父类
class People {
constructor(name) {
this.name = name
}
eat() {
console.log(`${this.name} eat something`)
}
}
// 子类
class Student extends People {
constructor(name, number) {
super(name)
this.number = number
}
sayHi() {
console.log(`姓名:${this.name},学号:${this.number}`)
}
}
// 子类
class Teacher extends People {
constructor(name, major) {
super(name)
this.major = major
}
teach() {
console.log(`姓名:${this.name} 教授 ${this.major}`)
}
}
const xialuo = new Student('夏洛', 100)
console.log(xialuo.name)
console.log(xialuo.number)
xialuo.sayHi()
xialuo.eat()
const wanglaoshi = new Teacher('王老师', '语文')
console.log(wanglaoshi.name)
console.log(wanglaoshi.major)
xialuo.sayHi()
xialuo.eat()
2、类型判断 instanceof
判断是否通过该方法构建(new)出来的
// 该例子见第一小节
console.log(xialuo instanceof Student) // true
console.log(xialuo instanceof People) // true
console.log(xialuo instanceof Object) // true
console.log([] instanceof Array) // true
console.log([] instanceof Object) // true
console.log({} instanceof Object) // true
3、原型和原型链
3.1 原型
每个对象拥有一个原型对象,对象以其原型为模板,从原型继承方法和属性,这些属性和方法定义在对象的构造器函数的 prototype 属性上,而非对象实例本身。
原型关系:
- 每个 class 都有显式原型 prototype
- 每个实例都有隐式原型 proto
- 实例的 proto 指向对应 class 的prototype
基于原型的执行规则
- 获取属性或执行方法时,先在自身属性和方法找
- 如果找不到则自动去 proto 中去找
// 该例子见第一小节
// 隐式原型和显式原型
console.log(xialuo.__proto__) // 隐式
console.log(Student.prototype) // 显式
console.log(xialuo.__proto__ === Student.prototype) // true
3.2 原型链
每个对象拥有一个原型对象,通过 proto 指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层层最终指向 null,这就是原型链// 该例子见第一小节
console.log(Student.prototype.__proto__) // 隐式
console.log(People.prototype) // 显式
console.log(Student.prototype.__proto__ === People.prototype) // true
三、作用域和闭包
1、作用域和自由变量
1.1 作用域
作用域代表了一个变量合法的使用范围,包括:
- 全局作用域
- 函数作用域
- 块级作用域
1.2 自由变量
自由变量即一个变量在当前作用域没有定义,但被使用了。它需要遵循下面的规则:
- 向上级作用域,一层一层依次寻找,直至找到为止
- 如果到全局作用域都没找到,则报错 xx is not defined
2、闭包
闭包是作用域应用的特殊情况,有两种表现:
- 函数作为参数被传递
- 函数作为返回值被返回
PS:
- 所有的自由变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方!
- 下面两个例子返回值均为 100
2.1 常用场景
隐藏数据,实现简单的缓存工具
// 闭包隐藏数据,只提供 API
function createCache() {
const data = {} // 闭包中的数据,被隐藏,不被外界访问
return {
set: function (key, val) {
data[key] = val
},
get: function (key) {
return data[key]
}
}
}
const c = createCache()
c.set('a', 100)
console.log(c.get('a'))
3、this
this 取值是在函数执行时确定(谁调用我,我就指向谁),常用场景有:
- 作为普通函数调用:直接返回 window
- 使用 call、apply、bind:改变了 this 的指向
- 作为对象方法被调用:返回对象本身
- 在 class 方法中调用:返回实例本身
- 箭头函数:箭头函数中的 this,是取上级作用域的 this
3.1 手写 call 函数
Function.prototype.myCall = function (context = window, ...args) {
context.fn = this
const result = context.fn(...args)
delete context.fn
return result
}
3.2 手写 apply 函数
Function.prototype.myApply = function (context = window, args) {
context.fn = this
const result = !args ? context.fn() : context.fn(...args)
delete context.fn
return result
}
3.3 手写 bind 函数
Function.prototype.myBind = function (context, ...args) {
const self = this
return function () {
return self.apply(context, args)
}
}
四、异步
JS 是单线程语言,只能同时做一件事,但遇到等待(网络请求,定时任务)时不能卡住,阻塞代码执行,因此需要异步的机制,异步是基于 callback 回调的方式去执行的。
应用场景:网络请求(ajax、图片加载等)、定时任务等
1、Promise
Promise 是异步编程的一个解决方案,比传统的回调函数解决方案更加合理和强大
1.1 基础用法
下面是一个图片加载的例子:
function loadImg(src) {
return new Promise((resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
const err = new Error(`图片加载失败 ${src}`)
reject(err)
}
img.src = src
})
}
const url1 = 'url1'
const url2 = 'url2'
// then、catch 方法均会返回一个Promise对象
loadImg(url1).then(img1 => {
console.log(img1.width)
return img1 // 普通对象
}).then(img1 => {
console.log(img1.height)
return loadImg(url2) // promise 实例
}).then(img2 => {
console.log(img2.width)
return img2
}).then(img2 => {
console.log(img2.height)
}).catch(err => console.error(err))
1.2 进阶知识
promise 有三种状态:
- pending:正在过程中,还没结果;不会触发 then 和 catch
- resolved:已经解决,成功了;会触发后续的 then 回调函数
- rejected:已经拒绝,失败了;会触发后续的 catch 回调函数
状态转换只能由 pending -> resolved 或 pending -> rejected,变化不可逆
then 和 catch 改变状态的方式:
- then 正常返回 resolved,里面有报错则返回 rejected
- catch 正常返回 resolved,里面有报错则返回 rejected ```javascript // 例子一 Promise.resolve().then(() => { console.log(1) }).catch(() => { console.log(2) }).then(() => { console.log(3) }) // 1 3
// 例子二 Promise.resolve().then(() => { console.log(1) throw new Error(‘erro1’) }).catch(() => { console.log(2) }).then(() => { console.log(3) }) // 1 2 3
// 例子三 Promise.resolve().then(() => { console.log(1) throw new Error(‘erro1’) }).catch(() => { console.log(2) }).catch(() => { // 注意这里是 catch console.log(3) }) // 1 2
```
<a name="UGwA7"></a>
### 2、Event Loop(事件循环/事件轮询)
Event Loop 是异步回调的实现原理,如图所示<br />![event loop.png](https://cdn.nlark.com/yuque/0/2021/png/603415/1612297435650-f416d57c-b6b4-48b8-8fb1-21815de0232a.png#crop=0&crop=0&crop=1&crop=1&height=482&id=ZpeEC&margin=%5Bobject%20Object%5D&name=event%20loop.png&originHeight=964&originWidth=1550&originalType=binary&ratio=1&rotation=0&showTitle=false&size=233007&status=done&style=none&title=&width=775)<br />其中
- Call Stack:调用栈,触发执行代码
- Web APIs:触发如定时器、异步等 Web API
- Event Loop:循环机制,若调用栈为空,则轮询将回调队列中的函数放入调用栈中执行
- Callback Queue:回调队列,异步操作结束,会将回调函数推入回调队列中,等待执行
执行过程如下:
1. 同步代码,一行一行放在 Call Stack 执行
2. 遇到异步,会先 “记录” 下,等待时机(定时、网络请求)
3. 时机到了,就移动到 Callback Queue
4. 如果 Call stack 为空(即同步代码执行完)
- 先执行当前微任务
- 再尝试 DOM 渲染
- 最后触发 Event Loop 开始工作
5. 轮询查找 Callback Queue,如有则移动到 Call Stack 执行
6. 然后继续轮询查找
<a name="xq6t5"></a>
### 3、async/await
async/await 是个语法糖,是用同步写法,去编写异步代码
<a name="k7aNr"></a>
#### 3.1 基础用法
下面是一个图片加载的例子:
```javascript
function loadImg(src) {
return new Promise((resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
const err = new Error(`图片加载失败 ${src}`)
reject(err)
}
img.src = src
})
}
const url1 = 'url1'
const url2 = 'url2';
(async function () {
const img1 = await loadImg(url1)
console.log(img1.height, img1.width)
const img2 = await loadImg(url2)
console.log(img2.height, img2.width)
})()
3.2 进阶知识
- 执行 async 函数,返回的是 Promise 对象
- await 相当于 Promise 的 then
- try…catch 可捕获异常,代替了 Promise 的 catch
看一个执行顺序的问题:
async function async1() {
console.log('async1 start') // 2
await async2()
// await 的后面,都可以看作是 callback 里的内容,即异步
console.log('async2 end') // 5
}
async function async2() {
console.log('async2') // 3
}
console.log('script start') // 1
async1()
console.log('script end') // 4
4、宏任务/微任务
- 宏任务(macroTask):在 DOM 渲染后触发,如:setTimeout、setInterval、Ajax、DOM事件
- 微任务(microTask):在 DOM 渲染前触发,如:Promise、async/await
区别:微任务执行时机比宏任务要早(根据 Event Loop 执行机制,各自在 DOM 渲染前后触发),可在控制台输以下代码体验效果
document.children[0].innerHTML = '你好像个憨憨'
Promise.resolve().then(() => {
alert('Promise then 已经执行') // Dom 未渲染
})
setTimeout(() => {
alert('setTimeout 已经执行') // Dom 已渲染
})
原因:
- 宏任务会经过调用 Web APIs 后,再进入回调队列(Callback Queue)
- 微任务不会经过 Web APIs,如 promise,它的回调是进入微任务队列(Micro Task Queue),在 Dom 渲染前执行(因为微任务是 ES6 语法规定,宏任务是由浏览器规定的)
5、异步综合面试题
执行顺序问题
async function async1 () {
console.log('async1 start')
await async2() // 这一句会同步执行,返回 Promise ,其中的 `console.log('async2')` 也会同步执行
console.log('async1 end') // 上面有 await ,下面就变成了“异步”,类似 callback 的功能(微任务)
}
async function async2 () {
console.log('async2')
}
console.log('script start')
setTimeout(function () { // 异步,宏任务
console.log('setTimeout')
}, 0)
async1()
// 初始化 promise 时,传入的函数会立即执行
new Promise (function (resolve) { // 返回 Promise 之后,即同步执行完成,then 是异步代码
console.log('promise1') // Promise 的函数体会立刻执行
resolve()
}).then (function () { // 异步,微任务
console.log('promise2')
})
console.log('script end')
// 同步代码执行完毕(event loop - call stack 被清空)
// 执行微任务
// (尝试触发 Dom 渲染)
// 触发 Event Loop,执行宏任务
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout