三、JS基础
1. 变量类型和计算
- 值类型vs引用类型(区别)
let a = 100
let b = a
console.log(b) // 100
let a = {age: 20}
let b = a
b.age = 21
console.log(a.age) // 21
- typeof能判断哪些类型?
- typeof能识别所有的值类型、函数,能判断是否是引用类型(不可再细分)
- 何时使用===,何时使用==?
- 除了==null之外,其它都一律用===
- 手写深拷贝
let result;
const obj1 = {
age: 20,
name: 'xxx',
address: {
city: 'beijing'
}
}
const obj2 = deepClone(obj1)
obj2.address.city = 'shanghai'
console.log(obj1.address.city) // beijing
function deepClone(obj = {}) {
if(typeof obj !== 'object' || obj ===null) {
return obj
}
if(obj instanceof Array) {
result = []
} else {
result = {}
}
for(let key in obj) {
if(obj.hasOwnProperty(key)) {
// 保证key不是原型的属性
if(obj.hasOwn) {
// 递归
result[key] = deepClone(obj[key])
}
}
}
return result;
}
- 字符串拼接
const b = 100 + '10' // '10010'
const c = true + '10' // 'true10'
- ==运算符(除了==null之外,其它都一律用===)
100 == '100' // true
0 == '' // true
0 == false // true
false = '' // true
null == undefined // true
if(obj.a == null) { } // 相当于if(obj.a === null || obj.a === undefined) { }
- truly变量和falsely变量(双重否定表肯定)
- truly变量: !!a === true 的变量(这不是废话嘛,真诚脸)
- falsely变量:!!a === false 的变量
2. 原型和原型链
- 如何判断一个变量是不是数组?
- xx instanceof Array // true,则xx是数组
- 手写一个简易的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
}
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
const $p = new jQuery('p')
$p.get(1) //<p>一段文字</p>
考虑插件:
jQuery.prototype.dialog = function(info) {
alert(info)
}
// 应用
$p.dialog('abc')
复写机制(造轮子):
class myJQuery extends jQuery {
constructor(selector) {
super(selector)
}
// 扩展自己的方法
addClass(className) {}
style(data) {}
}
- class的原型本质,怎么理解?
- 原型和原型链的图示
- 原型关系
- 每个class都有显示原型prototype
- 每个实例都有隐式__proto__
- 实例的__proto__指向对应class的prototype
- 基于原型的执行规则
- 想要获取属性xialuo.name或执行方法xialuo.sayhi()时,先在自身属性和方法寻找,如果找不到则自动去__proto__找
3. 作用域和闭包
- 作用域
- 作用域表示某个变量的合法的使用范围
- 全局作用域
- 函数作用域
- 块级作用域(if, for等的大括号里面的区域)
- 自由变量
- 定义:一个变量在当前作用域没有定义,但被使用了
- 应该向上级作用域,一层一层依次寻找,直到找到为止
- 如果到全局作用域都没找到,则报错xx is not defined
- 闭包
- 定义:作用域应用的特殊情况,有两种表现:函数作为参数被传递,或函数作为返回值被返回。
- 闭包中自由变量的查找是在函数定义的地方向上级作用域查找,不是在执行的地方查找
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
- this的不同应用场景,如何取值?
- 使用场景:
-
- 作为普通函数:this返回window
- 使用call apply bind:传入什么,this就返回什么
- 作为对象方法被调用:this返回对象
- 在class方法中调用:this返回实例本身
- 箭头函数:this取值取上级作用域的值
- this取什么样的值是在函数执行的时候确定的,不是在函数定义的时候确定的
function fn1() {
console.log(this)
}
fn1() // window
fn1.call({ x: 100 }) // { x: 100 }
const fn2 = fn1.bind({ x: 200 })
fn2() // { x: 200 }
- 手写bind函数
function fn1(a, b, c) {
console.log('this',this)
console.log(a,b,c)
return 'this is fn1'
}
const fn2 = fn1.bind({x: 100}, 10, 20, 30)
const res = fn2()
console.log(res)
// 打印结果
// this {x:100}
// 10 20 30
// this is fn1
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 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')) // 100
- 创建10个,点击弹出序号
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)
}
4. 异步
- 单线程和异步
- JS是单线程语言,本质上只能同时做一件事
- 浏览器和nodejs已支持JS启动进程,如Web Worker
- JS和DOM渲染共用同一个线程,因为JS可修改DOM结构
- 遇到等待(网络请求,定时任务)不能卡住,所以需要异步
- 异步是基于callback回调函数形式
- 同步和异步的区别是什么?
- 异步不会阻塞代码执行
- 同步会阻塞代码执行
- 前端使用异步的场景有哪些?
- 网络请求,如ajax图片加载
- 定时任务,如setTimeout
- 手写promise,用promise加载一张图片
const url1 = 'xxx'
const url2 = 'xxxx'
function loadImg(src) {
return new Promise(
(resolv, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
const err = new Error(`图片加载失败 ${src}`)
reject(err)
}
img.src = src
}
)
return p
}
loadImg(url1).then(img1 => {
console.log(img1.width)
return img1
}).then(img1 => {
console.log(img1.height)
return loadImg(url2)
}).then(img2 => {
console.log(img2.width)
return img2
}).then(img2 => {
console.log(img2.height)
}).catch(ex=> console.error(ex))
5. 异步进阶
- 请描述event loop(事件循环/事件轮询)的机制,可画图
- event loop是异步回调的实现原理和过程
-
- 同步代码,一行一行放在Call Stack执行;遇到异步,会先“记录”下,等待时机(定时、网络请求等);时机到了,就移动到Callback Queue里面
- 如果Call Stack为空(即同步代码执行完),Event Loop开始工作;轮询查找Callback Queue,如有则移动到Call Stack执行;然后继续轮询查找(永动机一样)
-
DOM事件和event loop
- DOM事件使用回调,是基于event loop来实现的
- 什么是宏任务和微任务,两者有什么区别?
- Promise有哪三种状态,如何变化
- pending: 过程中,不会触发then和catch
- resolved: 成功,会触发后续的then回调函数
- rejected: 失败,会触发后续的catch回调函数
- 变化:pending->resolved; pending -> rejected
- then和catch改变状态
-
- then正常返回resolved,里面有报错则返回rejected
- catch正常返回resolved,里面有报错则返回rejected
- then和catch的链式调用(promise 面试题示例)
第一题
Promise.resolve().then(()=>{ // resolved,不进.catch
console.log(1)
}).catch(()=>{ // 不执行
console.log(2)
}).then(()=>{
console.log(3)
})
// 结果返回1 3
第二题
Promise.resolve().then(()=>{
console.log(1)
throw new Error('error1') // rejected,进.catch
}).catch(()=>{ // resolved,进.then
console.log(2)
}).then(()=>{ // resolved
console.log(3)
})
// 结果返回1 2 3
第三题
Promise.resolve().then(()=>{
console.log(1)
throw new Error('error1') // rejected,进.catch
}).catch(()=>{
console.log(2) // resolved,不进.catch
}).catch(()=>{
console.log(3)
})
// 结果返回1 2
- async/await语法
- 背景:异步 回调 callback hell
- Promise then catch 的链式调用,可以解决异步问题,但也是基于回调函数
- async/await是同步语法,彻底代替(消灭)回调函数,但和Promise并不互斥
- async/await和Promise的关系
- 执行async函数,返回的是Promise对象
- await相当于Promise的then
- try...catch可捕获异常,代替了Promise的catch
- 异步的本质
- for...of
- for...in(以及forEach,for)是常规的同步遍历
- for...of常用于异步的遍历
- 宏任务macroTask和微任务microTask
- 宏任务:setTimeout, setInterval, Ajax, DOM事件
- 微任务:Promise, async/await
- 微任务执行时机比宏任务要早(因为宏任务是在DOM渲染后触发,微任务是在DOM渲染前触发)
- 微任务是ES6语法规定的,宏任务是浏览器规定的
- 在Call Stack清空后,先执行当前的微任务,然后尝试DOM渲染, 再触发Event Loop,执行宏任务
- event loop 和 DOM 渲染
- 每次Call Stack清空(即每次轮询结束),即同步任务执行完
- 都是DOM重新渲染的机会,DOM结构如有改变则重新渲染
- 然后再去触发下一次Event Loop