一、JS基础
变量类型和计算
常考题
1.值类型和引用类型区别
值类型
引用类型
题目
2.typeof
能判断哪些类型
- 识别所有值类型:number,boolean,string,function(函数),object(NULL,数组,对象),undefined
- 识别函数(function)
- 判断是否是引用类型(object 不可再细分)
3.手写深拷贝
- 注意判断值类型和引用类型
- 注意判断是数组还是对象
- 递归
/**
* 深拷贝
* @param {Object} obj 要拷贝的对象
*/
function deepClone(obj = {}) {
if (typeof obj !== 'object' || obj == null) {
return obj
}
// 初始返回结果
let result
if (Array.isArray(obj)) {
result = []
} else {
result = {}
}
// 遍历对象或数组(一个个拷贝值)
for (let key in obj) {
// 保证 key 不是原型的属性
if (obj.hasOwnProperty(key)) {
// 递归调用!!!
result[key] = deepClone(obj[key])
}
}
return result
}
结果验证
const obj1 = {
age: 20,
name: 'xxx',
address: {
city: 'beijing'
},
arr: ['a', 'b', 'c']
}
const obj2 = deepClone(obj1)
obj2.address.city = 'shanghai'
console.log(obj1.address.city) // 'beijing'
console.log(obj2.address.city) // 'shanghai'
4. 何时使用 === 何时使用 ==
5. truly和falsely变量
truly变量: !!a === true的变量
falsely变量: !!a === false的变量
- 注意: !![]=true !!{}=true
- 拓展:如何判断对象或数组是否为空呢?
- 数组:判断arr.length == 0
- 对象:Object.keys(obj) – 返回对象中属性名组成的数组,再判断这个数组的长度
var data = {}; var arr = Object.keys(data); console.log(arr.length == 0); //true 为空, false 不为空
原型和原型链
知识点补充
1.继承(extends、super、扩展或重写方法)
2.类型判断- instanceof
3.原型
原型关系
- 每个class都有显示原型prototype
- 每个实例都有隐式原型_proto__
- 实例的__proto__指向对应class的prototype
基于原型的执行规则
- 获取属性xialuo.name或执行方法xialuo.sayhi()时
- 先在自身属性和方法寻找
- 如果找不到则自动去__proto__中查找
4.原型链
常考题
1.如何准确判断一个变量是不是数组
- a instanceof Array
- Array.isArray(a)
2.class的原型本质
- 原型和原型链的图示
- 属性和方法的执行规则
3.手写简易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)
})
}
// 扩展很多 DOM API
}
// 插件
jQuery.prototype.dialog = function (info) {
alert(info)
}
// “造轮子”
class myJQuery extends jQuery {
constructor(selector) {
super(selector)
}
// 扩展自己的方法
addClass(className) {
}
style(data) {
}
}
// const $p = new jQuery('p')
// $p.get(1)
// $p.each((elem) => console.log(elem.nodeName))
// $p.on('click', () => alert('clicked'))
作用域和闭包
知识点补充
1.作用域和自由变量
作用域
- 全局作用域
- 函数作用域
- 块级作用域(if、for、while…)
自由变量
- 一个变量在当前作用域没有定义,但被使用了
- 向上级作用域,一层一层依次寻找,直至找到为止
- 如果到全局作用域都没找到,则报错xx is not defined
2.闭包
闭包就是能够读取其他函数内部变量的函数,是将函数内部和函数外部连接起来的一座桥梁。
2.1、作用域应用的特殊情况,有两种表现∶
- 函数作为返回值被返回
// 函数作为返回值
function create() {
const a = 100
return function () {
console.log(a); // 在定义的作用域中向上级寻找
}
}
const fn = create()
const a = 200
fn() // 100
- 函数作为参数被传递
// 函数作为参数
function print(fn) {
const a = 200
fn()
}
const a = 100
function fn() {
console.log(a); // 在定义的作用域中向上级寻找
}
print(fn) //100
- 结论
- 所有的自由变量的查找,是在
函数定义
的地方,向上级作用域查找 不是在执行的地方!!
- 所有的自由变量的查找,是在
2.2、优缺点
闭包的特性
①封闭性:外界无法访问闭包内部的数据,如果在闭包内声明变量,外界是无法访问的,除非闭包主动向外 界提供访问接口;(隐藏变量,避免全局污染; 可以读取其它函数内部的变量)
②持久性:一般的函数,调用完毕之后,系统自动注销函数,而对于闭包来说,在外部函数被调 用之后,闭包结构依然保存在;
负面影响:
使用闭包会占有内存资源,过多的使用闭包会导致内存溢出等。(上级作用域内变量的生命周期,因为在下级作用域内被引用,而没有被释放。)
内存泄漏:是指程序中已动态分配的堆内存,由于某种原因,程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
闭包是一个非常强大的特性,但是人们对其也有诸多误解。一种耸人听闻的说法是闭包会造成内存泄漏,所以要尽量减少闭包的作用。
局部变量本来应该在函数退出的时候被解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就能一直生存下去。
从这个意义上看,闭包的确会使一些数据无法被及时销毁,使用闭包的一部分原因室我们选择主动把一些变量封闭在闭包中,因为可能在以后还需要使用这些变量,把这些变量放在闭包中和放在全局作用域,对内存方面的影响是一致的,这里并不能说成是内存泄漏。
如果在将来需要回收这些变量,我们可以手动把这些变量设为null,将变量设置为null意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。
2.3、例子
需求:实现变量a 自增
1、通过全局变量,可以实现,但会污染其他程序
var a = 10;
function Add(){
a++;
console.log(a);
}
Add();
Add();
Add();
2、定义一个局部变量,不污染全局,但是实现不了递增
var a = 10;
function Add2(){
var a = 10;
a++;
console.log(a);
}
Add2();
Add2();
Add2();
console.log(a);
3、通过闭包,可以使函数内部局部变量递增,不会影响全部变量,完美!!
var a = 10;
function Add3(){
var a = 10;
return function(){
a++;
return a;
};
};
var cc = Add3();
console.log(cc());
console.log(cc());
console.log(cc());
console.log(a);
常考题
1.this的不同应用场景,如何取值?
this取什么值,是在函数执行的时候确认,不是在函数定义的时候确认
- 作为普通函数—指向window
// 普通函数
function fn1() {
console.log(this);
}
fn1() // window
- 作为对象方法被调用 – 指向当前对象
- setTimeout(function (){})-- 指向window
setTimeout(()=>{})-- 取它上级作用域的this
// 作为对象方法被调用
const zhangsan = {
name: '张三',
sayHi() {
// this指向当前对象
// {name: "张三", sayHi: ƒ, wait: ƒ}
console.log(this);
},
wait() {
setTimeout(function () {
// this指向window
console.log(this);
})
}
}
- 在class方法中调用 – 指向它的实例对象
class People {
constructor(name) {
this.name = name
this.age = 20
}
sayHi() {
console.log(this);
}
}
const zhangsan = new People('张三')
zhangsan.sayHi() // this指向zhnagsan对象
- 箭头函数 – 永远是取它上级作用域的this
// 箭头函数
const zhangsan = {
name: '张三',
sayHi() {
// this指向当前对象
// {name: "张三", sayHi: ƒ, wait: ƒ}
console.log(this);
},
wait() {
setTimeout(() => {
// this指向当前对象
// {name: "张三", sayHi: ƒ, wait: ƒ}
console.log(this);
})
}
}
- 使用call apply bind(改变this指向)
// 普通函数
function fn1() {
console.log(this);
}
fn1() // window
// 使用call
fn1.call({ x: 100 }) // {x: 100}
// 使用apply
fn1.apply({ x: 100 }) // {x: 100}
// 使用bind
const fn2 = fn1.bind({ x: 200 })
fn2() // {x: 200}
call 、apply、bind 这三个函数的第一个参数是 this 的指向对象,后面的参数都为传递给原函数的参数
原函数是否需要传参\调用方法 | call | apply | bind | 总结 |
---|---|---|---|---|
无参数function fn1() {} | fn1.call({ x: 100 }) | fn1.apply({ x: 100 }) | fn1.bind ({ x: 100 })() | bind 返回的是一个新的函数,必须调用 ,它才会被执行 |
有参数function fn1(a,b) {} | fn1.call({ x: 100 },‘成都’,‘上海’) | fn1.apply({ x: 100 },[‘成都’,‘上海’]) | fn1.bind ({ x: 100 },‘成都’,‘上海’)() | apply 的所有参数都必须放在一个数组 里面传进去 |
2.创建10个a标签,点击的时候弹出对应的序号
问题:点击每个a标签,弹出来的都是10
原因:i是全局作用域
let i, a
for (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)
}
解决方式:把i放到块级作用域
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)
}
3.手写bind函数
// 模拟bind
Function.prototype.bind1 = function () {
// 将参数拆解为数组
const args = Array.prototype.slice.call(arguments)
// 获取 this (数组第一项)
const t = args.shift()
// fn1.bind(...)中的fn1
const self = this
// 返回一个函数
return function () {
return self.apply(t, args)
}
}
function fn1(a, b, c) {
console.log('this', this);
console.log(a, b, c);
return 'this is fn1'
}
const fn2 = fn1.bind1({ x: 100 }, 10, 20, 30)
const res = fn2()
console.log(res);
4.实际开发中闭包的应用场景,举例说明
- 隐藏数据
如做一个简单的cache工具
// 闭包隐藏数据,只提供 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') )
异步
知识点补充
1.单线程和异步
异步产生
- JS是单线程语言
异步的设计主要是因为js是一个单线程的语言,单线程只允许同时做一件事情,如果同时需要做多个,那其他的需要去旁边排队去 - 浏览器和nodejs已支持JS启动
进程
,如Web Worker - JS和DOM渲染共用同一个线程,因为JS可修改DOM结构
- 遇到等待(网络请求,定时任务)不能卡住
异步特点
- 回调callback函数形式
- 基于JS是单线程语言
- 异步不会阻塞代码执行(callback)
- 同步会阻塞代码执行
2.异步应用场景
- 网络请求,如ajax图片加载
- 定时任务,如setTimeout
3.Promise解决callback hell
嵌套
链式
常考题
1.同步和异步的区别是什么?
- 基于JS是单线程语言
- 异步不会阻塞代码执行
- 同步会阻塞代码执行
2.手写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 url1 = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
const url2 = 'https://img3.mukewang.com/5a9fc8070001a82402060220-100-100.jpg'
loadImg(url1).then(img1 => { // 接收resolve传递的参数
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(ex => console.error(ex))
3.前端异步的使用场景有哪些?
- 网络请求,如ajax图片加载
- 定时任务,如setTimeout
4.setTimeout笔试题
答案:1 3 5 4 2
二、JS异步进阶
知识补充
1.event loop(事件循环/事件轮询)
①JS是单线程运行的,异步要基于回调来实现,event loop就是异步回调的实现原理
②JS如何执行
- 从前到后,一行一行执行
- 如果某一行执行报错,则停止下面代码的执行
- 先把同步代码执行完,再执行异步
③event loop过程
- 同步代码,一行一行放在Call Stack(调用栈) 执行
- 遇到异步,会先放在一边,等待时机(定时、网络请求等)
- 时机到了,就移动到Callback Queue(回调函数队列)
- 当Call Stack为空(即同步代码执行完),开始启动Event Loop(事件轮询)机制
- 轮询查找Callback Queue中有没有函数 ,如有则移动到Call Stack 执行
- 然后继续轮询查找(永动机一样)
由于主线程不断的重复获得任务、执行任务、再获取任务、再执行,所以这种机制被称为事件循环( event loop)。
2.promise进阶
三种状态
- pending resolved rejected
- pending —> resolved(成功) 或 pending —> rejected(失败)
- 变化不可逆
状态的表现和变化
- pending 状态,不会触发then和catch
- resolved状态,会触发后续的then回调函数
- rejected 状态,会触发后续的catch回调函数
then和catch 对状态的影响
- then 正常返回resolved,里面有报错则返回rejected
- catch 正常返回resolved ,里面有报错则返回rejected
3.async/await
背景
- 异步回调callback hell
- Promise then catch 链式调用,但也是基于回调函数
- async/await是同步语法,彻底消灭回调函数
async/await和 Promise的关系!!!
- async/await是消灭异步回调的终极武器,但和Promise并不互斥,反而,两者相辅相成
- 执行async函数,返回的是 Promise对象
- await 相当于Promise的then
- try…catch 可捕获异常,代替了Promise的catch
异步的本质
- await 是同步写法,但本质还是异步调用
- JS还是单线程,还得是有异步,还得是基于event loop
- async/await只是一个语法糖
async function async1 () {
console.log('async1 start') // 2
await async2()
// await的后面,都可以看做是 callback 里的内容,即异步
console.log('async1 end') // 5
await async3()
// 下面一行是异步回调的内容
console.log('async1 end2') // 7
}
async function async2 () {
console.log('async2') // 3
}
async function async3 () {
console.log('async3') // 6
}
console.log('script start') // 1
async1()
console.log('script end') // 4
// 同步代码执行完 event loop
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()
4.宏任务macroTask / 微任务microTask
什么是宏任务,什么是微任务
- 宏任务: setTimeout , setInterval , Ajax ,DOM事件
- 微任务:Promise async/await
- 微任务执行时机比宏任务要早
event loop和 DOM渲染
- JS是单线程的,而且和DOM渲染共用一个线程
- JS执行的时候,得留一些时机供DOM渲染
- DOM渲染时机
- 每次Call Stack清空(即同步任务执行完/每次轮询结束)
- 尝试DOM渲染,DOM结构如有改变则重新渲染
- 然后再去触发下一次Event Loop
微任务和宏任务的区别
-
微任务:DOM渲染前触发,如Promise
-
宏任务:DOM渲染后触发,如setTimeout
-
微任务是ES6语法规定的,宏任务是由浏览器规定的
-
微任务放 micro task queue
-
执行顺序
常考题
1.描述event loop机制(可画图)
三方面
- event loop 的基础过程
- 和DOM渲染的关系
- 微任务和宏任务在event loop过程中的不同处理
2.什么是宏任务和微任务,两者区别
- 宏任务: setTimeout , setInterval , Ajax,DOM事件
- 微任务: Promise , async/await
- 区别:微任务执行时机比宏任务要早
3.Promise的三种状态,如何变化
- pending resolved rejected
- pending —> resolved 或 pending -> rejected
- resolved会触发then后面的回调,rejected会触发catch后面的回调
- 变化不可逆
4.场景题- promise then和catch的连接
// 第一题
Promise.resolve().then(() => {
console.log(1)
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 输出:1 3
// 第二题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
console.log(1)
throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
console.log(2)
}).then(() => {
console.log(3)
})
// 输出:1 2 3
// 第三题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
console.log(1)
throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
console.log(2)
}).catch(() => {
console.log(3)
})
// 输出:1 2
5.场景题- async/await语法
题1
// 执行完毕,会打印出哪些内容
async function fn() {
return 100
}
(async function () {
const a = fn()
console.log('a', a)
const b = await fn()
console.log('b', b)
})()
打印结果:
题2
// 执行完毕,会打印出哪些内容
(async function () {
console.log('start')
const a = await 100
console.log('a', a)
const b = await Promise.resolve(200)
console.log('b', b)
const c = await Promise.reject(300)
console.log('c', c)
console.log('end')
})()
打印结果:
6.场景题- promise和 setTimeout的顺序
console.log(100)
setTimeout(() => {
console.log(200)
})
Promise.resolve().then(() => {
console.log(300)
})
console.log(400)
打印结果:100 400 300 200
7.场景题–外加async/await的顺序问题
async function async1() {
console.log('async1 start') // 2
await async2()
// await后面的都作为回调内容 —— 微任务
console.log('async1 end') // 6
}
async function async2() {
console.log('async2') // 3
}
console.log('script start') // 1
setTimeout(function () { // 宏任务 setTimeout
console.log('setTimeout') // 8
}, 0)
async1()
// 初始化 Promise 时,传入的函数会立刻被执行
new Promise(function (resolve) {
console.log('promise1') // 4
resolve()
}).then(function () { // 微任务
console.log('promise2') // 7
})
console.log('script end') // 5
// 同步代码执行完毕(event loop - call stack 被清空)
// 执行微任务
// 尝试触发 DOM 渲染
// 触发 Event Loop,执行宏任务
打印结果: