JS
变量类型和计算
-
typeof
能判断那些类型 - 何时使用 === 何时使用 ==
- 值类型和引用类型的区别
- 手写深拷贝
值类型和引用类型
值类型存储在栈内存中,引用类型存储在堆内存中
// 值类型
let a = 100
let b = a
a = 200
console.log(b) // 100
// 引用类型
let a = { age: 20 }
let b = a
b.age = 21
console.log(a.age) // 21
常见值类型和引用类型
// 常见值类型
let u // undefined
const s = 'abc'
const n = 100
const b = true
const s = Symbol('s')
// 常见引用类型
const obj = { x: 100 }
const arr = ['a', 'b', 'c']
const n = null // 特殊引用类型,指针指向为空地址
// 特殊的引用类型,但不用于存储数据,所以没有拷贝、复制函数这一说
function fun() {}
类型判断
typeof
运算符
- 识别所以值类型
- 识别函数
- 判断是否是引用类型(不可再细分)
深克隆
function deepClone(obj = {}) {
if (typeof obj !== 'object' || obj === null) {
// obj 是 null,或是不是对象和数组,直接返回
return obj
}
// 初始化返回结果
let result
if (obj instanceof Array) {
result = []
} else {
result = {}
}
for (let key in obj) {
// 保证 key 不是原型的属性
if (obj.hasOwnProperty(key)) {
// 递归
result[key] = deepClone(obj[key])
}
}
return result
}
类型转换
- 字符串拼接
- ==
- if 语句逻辑运算
const a = 100 + 10 // 110
const b = 100 + '10' // 10010
const c = true + '10' // true10
const d = 100 + parseInt('10') // 110
==
100 == '100' // true
0 == '0' // true
0 == false // true
false == '' // true
null == undefined // true
NaN == NaN // false
// 除了 == null 之外,其他都一律用 === ,例如:
const obj = { x: 100 }
if (obj.a == null) {}
// 相当于
if (obj.a === null || obj.a === undefined) {}
逻辑运算
if 语句和逻辑运算
- truly 变量:!!a === true 的变量
- falsely 变量:!!a === false 的变量
原型和原型链
- 如何判断一个变量是不是数组?
- 手写一个简易的 jQuery,考虑插件和扩展性
- class 的原型本质,怎么理解?
class
class Student {
constructor(name, number) {
this.name = name
this.number = number
}
greeting() {
console.log(`Hello, My name is ${this.name}, number is ${this.number}`)
}
}
// 实例化
const zhangsan = new Student('zhangsan', 1)
zhangsan.greeting() // Hello, My name is zhangsan, number is 1
继承
// 父类
class Person {
constructor(name) {
this.name = name
}
eat() {
console.log(`${this.name} eat something`)
}
}
// 子类
class Student extends Person {
constructor(name, number) {
super(name)
this.number = number
}
greeting() {
console.log(`Hello, My name is ${this.name}, number is ${this.number}`)
}
}
// 子类
class Teacher extends Person {
constructor(name, subject) {
super(name)
this.subject = subject
}
teach() {
console.log(`My name is ${this.name}, and I am teaching ${this.subject}`)
}
}
// 实例化
const zhangsan = new Student('zhangsan', 1)
zhangsan.greeting() // Hello, My name is zhangsan, number is 1
zhangsan.eat() // zhangsan eat something
const mrWang = new Teacher('wang', 'Math')
mrWang.eat() // wang eat something
mrWang.teach() // My name is wang, and I am teaching Math
// 类型判断
console.log(zhangsan instanceof Student) // true
console.log(zhangsan instanceof Person) // true
console.log(zhangsan instanceof Object) // true
// true
console.log([] instanceof Array)
console.log([] instanceof Object)
console.log({} instanceof Object)
原型
// class 实际上是函数,语法糖而已
typeof Person // function
typeof Student // function
// 隐式原型和显示原型
console.log(zhangsan.__proto__)
console.log(Student.prototype)
console.log(zhangsan.__proto__ === Student.prototype) // true
原型关系
- 每个 class 都有显示原型 prototype
- 每个实例都有隐式原型 __ proto__
- 实例的 __ proto__指向对应的 class 的 prototype
原型链
console.log(Student.prototype.__proto__)
console.log(Person.prototype)
console.log(Person.prototype === Student.prototype.__proto__) // true
instanceof
手动实现 instanceof
function myInstanceof(left, right) {
// 获取类的原型
let prototype = right.prototype
// 获取对象的原型
left = left.__proto__
// 判断对象的类型是否等于类型的原型
while (true) {
if (left === null)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
手写一个简易的 jQuery,考虑插件和扩展性
class jQuery {
constructor(selector) {
const result = document.querySelectorAll(selector)
const length = result.length
for (let i = 0; i < length; i++) {
this[i] = result[i]
}
this.length = length
this.selector = selector
}
get(index) {
return this[index]
}
each(fn) {
for (let i = 0; i < this.length; i ++) {
const elem = this[i]
fn(elem)
}
}
on(type, fn) {
return this.each(elem => {
elem.addEventListener(type, fn, false)
})
}
}
// 插件
jQuery.prototype.dialog = function (info) {
alert(info)
}
// “造轮子”
class myJQuery extends jQuery {
constructor(selector) {
super(selector)
}
// 扩展自己的方法
addClass(className) {
// ...
}
style(data) {
// ...
}
}
作用域和闭包
- this 的不同应用场景,如何取值?
- 手写 bind 函数
- 实际开发中闭包的应用场景,举例说明
- 创建 10 个
<a>
标签点击的时候弹出对应的序号
作用域
let a = 0
function fn1() {
let a1 = 100
function fn2() {
let a2 = 200
function fn3() {
let a3 = 300
return a + a1 + a2 + a3
}
fn3()
}
fn2()
}
fn1()
- 全局作用域
- 函数作用域
- 块级作用域(ES6新增)
自由变量
- 一个变量在当前作用域没有定义,但被使用了
- 向上级作用域,一层一层依次寻找,直到找到了为止
- 如果在全局作用域都没有找到,则报错 xxx is not defined
闭包
- 作用域应用的特殊情况,有两种表现
- 函数作为参数被传递
- 函数作为值被返回
// 函数作为返回值
function create() {
let a = 100
return function () {
console.log(a)
}
}
let fn = create()
let a = 200
fn() // 100
// 函数作为参数
function print(fn) {
let a = 200
fn()
}
let a = 100
function fn() {
console.log(a)
}
print(fn) // 100
闭包:自由变量的查找,是在函数定义的地方,向上级作用域查找
不是在执行的地方!!!
this
- 作为普通函数
- 使用 call apply bind
- 作为对象方法被调用
- 在 class 方法中调用
- 箭头函数
this 取什么值是在函数执行的时候确定的,不是在定义的时候
function fn1() {
console.log(this)
}
fn1() // window
fn1.call({x: 100}) // {x: 100}
const fn2 = fn1.bind({x: 200})
fn2() // {x: 200}
箭头函数
const zhangsan = {
name: 'zhangsan',
greeting() {
// this 即当前对象
console.log(this)
},
wait() {
setTimeout(function() {
// this === window
console.log(this)
})
}
}
// 箭头函数的 this 永远取上级作用域的 this
const zhangsan = {
name: 'zhangsan',
// this 即当前对象
greeting() {
console.log(this)
},
wait() {
setTimeout(() => {
// this 即当前对象
console.log(this)
})
}
}
创建 10 个 <a>
标签点击的时候弹出对应的序号
let a
for (let i = 0; i < 10; i++) {
a = document.createElement('a')
a.innerHTML = i + '<br>'
a.addEventListener('click', function(e) {
e.preventDefault()
alert(i)
})
document.body.appendChild(a)
}
手写 bind 函数
Function.prototype.myBind = function() {
// 将参数拆解为数组
const args = Array.prototype.slice.call(arguments)
// 获取 this(数组第一项)
const that = args.shift()
// fn.bind(...) 中的 fn
const self = this
// 返回一个函数
return function() {
return self.apply(that, args)
}
}
实际开发中闭包的应用
隐藏数据
如做一个简单的 cache 工具
function createCache() { const data = {} // 闭包中的数据,被隐藏,不被外界访问 return { set(key, value) { data[key] = value }, get(key) { return data[key] } } } const c = createCache() c.set('a', 100) console.log(c.get('a'))
异步
同步和异步得区别是什么?
手写 Promise 加载一张图片
前端使用异步的场景
请描述 event loop (事件循环/事件轮询)的机制,可画图
什么是宏认为和微任务,两者有什么区别?
Promise 有哪三种状态?如何变化?
Promise 的 then 和 catch 的连接问题
async/await 语法
Promise 和 setTimeout 的顺序问题
单线程
- JS 是单线程语言,只能同时做一件事
- 浏览器和 nodejs 已支持 JS 启动进程,如 Web Worker
- JS 和 DOM 渲染共用同一个线程,因为 JS 可修改 DOM 结构
- 异步基于 回调 callback 函数形式
callback
// 异步
console.log(100)
setTimeout(function() {
console.log(200)
}, 1000)
console.log(300)
// 同步
console.log(100)
alert(200)
console.log(300)
异步不会阻塞代码执行
同步会阻塞代码执行
应用场景
- 网络请求,如 ajax 图片加载
- 定时任务,如 setTimeout
promise
基本使用
// 加载图片
function loadImg(src) {
const p = 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
}
)
return p
}
const url = 'www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png'
loadImg(url).then(img => {
console.log(img.width)
return img
}).then(img => {
console.log(img.height)
}).catch(ex => console.error(ex))
状态
- 三种状态
- pending
- resolved
- rejected
- 变化: pending => resolved 或 pending => rejected
- 变化是不可逆的
- 状态的表现和变化
- pending 状态,不会触发 then 和 catch
- resolved 状态,会触发后续的 then 回调函数
- rejected 状态,会触发后续的 catch 回调函数
const p1 = new Promise((resolve, reject) => {})
console.log(p1) // pending
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
})
})
console.log(p2) // pending 一开始打印时
setTimeout(() => console.log(p2)) // resolved
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject()
})
})
console.log(p3) // pending 一开始打印时
setTimeout(() => console.log(p3)) // rejected
// 直接获取一个 resolved 状态的 Promise
const resolved = Promise.resolve(100)
console.log(resolved) // resolved
// 直接获取一个 rejected 状态的 Promise
const resolved = Promise.reject('err')
console.log(resolved) // rejected
- then 和 catch 对状态的影响
- then 正常返回 resolved ,里面有报错则返回 rejected
- catch正常返回 resolved ,里面有报错则返回 rejected
// then() 一般正常返回 resolved 状态的 promise
Promise.resolve().then(() => {
return 100
})
// then() 里抛出错误,会返回 rejected 状态的 promise
Promise.resolve().then(() => {
throw new Error('err')
})
// catch() 不抛出错误,会返回 resolved 状态的 promise
Promise.reject().catch(() => {
console.error('catch some error')
})
// catch() 抛出错误,会返回 rejected 状态的 promise
Promise.reject().catch(() => {
console.error('catch some error')
throw new Error('err')
})
题
// 第一题
Promise.resolve().then(() => {
console.log(1)
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 第二题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
console.log(1)
throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
console.log(2)
}).then(() => {
console.log(3)
})
// 第三题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
console.log(1)
throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
console.log(2)
}).catch(() => {
console.log(3)
})
event-loop
- JS 是单线程运行的
- 异步要基于回调来实现
- event loop 就是异步回调的实现原理
JS 如何执行的
- 从前到后,一行一行执行
- 如果某一行执行报错,则停止下面代码的执行
- 先把同步代码执行完,再执行异步
event loop 执行过程
- 同步代码,一行一行放在 Call Stack 执行
- 遇到异步,会先 “记录” 下,等待时机(定时,网络请求)
- 时机到了,就移动到 Callback Queue
- 如果 Call Stack 为空(即同步代码执行完)Event Loop 开始工作
- 轮询查找 Callback Queue ,如果有则移动到 Call Stack 执行
- 然后继续轮询查找(永动机一样)
DOM 事件和 event loop
- 异步(setTimeout,ajax 等)使用回调,基于 event loop
- DOM 事件也使用回调,基于 event loop
async/await
- 异步回调 callback hell
- Promise 基于 then catch 链式调用,但也是基于回调函数
- async/await 是同步语法,彻底消灭回调函数
有很多 async 的面试题,例如
- async 直接返回,是什么
- async 直接返回 promise
- await 后面不加 promise
基本语法
function loadImg(src) {
const promise = new Promise((resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
reject(new Error(`图片加载失败 ${src}`))
}
img.src = src
})
return promise
}
async function loadImg1() {
const src1 = 'www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png'
const img1 = await loadImg(src1)
return img1
}
async function loadImg2() {
const src2 = 'https://avatars3.githubusercontent.com/u/9583120'
const img2 = await loadImg(src2)
return img2
}
(async function () {
// 注意:await 必须放在 async 函数中,否则会报错
try {
// 加载第一张图片
const img1 = await loadImg1()
console.log(img1)
// 加载第二张图片
const img2 = await loadImg2()
console.log(img2)
} catch (ex) {
console.error(ex)
}
})()
async/await 和 promise 的关系
- async/await 是消灭异步回调的终极武器
- 但和 promise 并不互斥
- 两者反而相辅相成
- await 相当于 promise 的 then
- try...catch 可捕获异常,代替了 promise 的 catch
- async 函数返回结果都是 Promise 对象(如果函数内没返回 Promise ,则自动封装一下)
async function fn2() {
return new Promise(() => {})
}
console.log( fn2() )
async function fn1() {
return 100
}
console.log( fn1() ) // 相当于 Promise.resolve(100)
- await 后面跟 Promise 对象:会阻断后续代码,等待状态变为 resolved ,才获取结果并继续执行
- await 后续跟非 Promise 对象:会直接返回
(async function () {
const p1 = new Promise(() => {})
await p1
console.log('p1') // 不会执行
})()
(async function () {
const p2 = Promise.resolve(100)
const res = await p2
console.log(res) // 100
})()
(async function () {
const res = await 100
console.log(res) // 100
})()
(async function () {
const p3 = Promise.reject('some err')
const res = await p3
console.log(res) // 不会执行
})()
- try...catch 捕获 rejected 状态
(async function () {
const p4 = Promise.reject('some err')
try {
const res = await p4
console.log(res)
} catch (ex) {
console.error(ex)
}
})()
总结来看:
- async 封装 Promise
- await 处理 Promise 成功
- try...catch 处理 Promise 失败
异步本质
await 是同步写法,但本质还是异步调用。
async function async1 () {
console.log('async1 start')
await async2()
console.log('async1 end') // 关键在这一步,它相当于放在 callback 中,最后执行
}
async function async2 () {
console.log('async2')
}
console.log('script start')
async1()
console.log('script end')
即,只要遇到了 await
,后面的代码都相当于放在 callback 里。
for...of
- for ... in (以及 forEach for)是常规的同步遍历
- for ... of 常用与异步的循环
// 定时算乘法
function multi(num) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(num * num)
}, 1000)
})
}
// // 使用 forEach ,是 1s 之后打印出所有结果,即 3 个值是一起被计算出来的
// function test1 () {
// const nums = [1, 2, 3];
// nums.forEach(async x => {
// const res = await multi(x);
// console.log(res);
// })
// }
// test1();
// 使用 for...of ,可以让计算挨个串行执行
async function test2 () {
const nums = [1, 2, 3];
for (let x of nums) {
// 在 for...of 循环体的内部,遇到 await 会挨个串行计算
const res = await multi(x)
console.log(res)
}
}
test2()
微任务/宏任务
- 宏任务:setTimeout setInterval DOM 事件
- 微任务:Promise(对于前端来说)
- 微任务比宏任务执行的更早
console.log(100)
setTimeout(() => {
console.log(200)
})
Promise.resolve().then(() => {
console.log(300)
})
console.log(400)
// 100 400 300 200
event loop 和 DOM 渲染
- 每一次 call stack 结束,都会触发 DOM 渲染(不一定非得渲染,就是给一次 DOM 渲染的机会!)
- 然后再进行 event loop
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
.append($p1)
.append($p2)
.append($p3)
console.log('length', $('#container').children().length )
alert('本次 call stack 结束,DOM 结构已更新,但尚未触发渲染')
// (alert 会阻断 js 执行,也会阻断 DOM 渲染,便于查看效果)
// 到此,即本次 call stack 结束后(同步任务都执行完了),浏览器会自动触发渲染,不用代码干预
// 另外,按照 event loop 触发 DOM 渲染时机,setTimeout 时 alert ,就能看到 DOM 渲染后的结果了
setTimeout(function () {
alert('setTimeout 是在下一次 Call Stack ,就能看到 DOM 渲染出来的结果了')
})
宏任务和微任务的区别
- 宏任务:DOM 渲染后再触发
- 微任务:DOM 渲染前会触发
// 修改 DOM
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
.append($p1)
.append($p2)
.append($p3)
// // 微任务:渲染之前执行(DOM 结构已更新)
// Promise.resolve().then(() => {
// const length = $('#container').children().length
// alert(`micro task ${length}`)
// })
// 宏任务:渲染之后执行(DOM 结构已更新)
setTimeout(() => {
const length = $('#container').children().length
alert(`macro task ${length}`)
})
再深入思考一下:为何两者会有以上区别,一个在渲染前,一个在渲染后?
- 微任务:ES 语法标准之内,JS 引擎来统一处理。即,不用浏览器有任何关于,即可一次性处理完,更快更及时。
- 宏任务:ES 语法没有,JS 引擎不处理,浏览器(或 nodejs)干预处理。
经典面试题
async function async1 () {
console.log('async1 start')
await async2() // 这一句会同步执行,返回 Promise ,其中的 `console.log('async2')` 也会同步执行
console.log('async1 end') // 上面有 await ,下面就变成了“异步”,类似 cakkback 的功能(微任务)
}
async function async2 () {
console.log('async2')
}
console.log('script start')
setTimeout(function () { // 异步,宏任务
console.log('setTimeout')
}, 0)
async1()
new Promise (function (resolve) { // 返回 Promise 之后,即同步执行完成,then 是异步代码
console.log('promise1') // Promise 的函数体会立刻执行
resolve()
}).then (function () { // 异步,微任务
console.log('promise2')
})
console.log('script end')
// 同步代码执行完之后,屡一下现有的异步未执行的,按照顺序
// 1. async1 函数中 await 后面的内容 —— 微任务
// 2. setTimeout —— 宏任务
// 3. then —— 微任务
模块化
ES6 Module
略