筑基
变量类型
typeof 和 instanceof 的区别?
typeof 对于string
,boolean
,number
,undefined
,function
,symbol
等类型可正确判断
对于null
,array
,object
判断结果均为 object
特殊的对于 null
,null
不是一个对象,尽管 typeof null
输出的是 object
,这是一个历史遗留问题,JS
最初为了性能使用低位存储变量的 类型信息 ,000
开头代表是对象,null
表示为全零,所以将它错误的判断为 object
instanceof 代码形式为object instanceof constructor
(object 是否是 constructor 的实例),该操作符用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上(可用于判断引用类型)
何时使用 === 何时使用 ==
除了 == null 之外 其它地方一律用===
const obj = { a: 2 }
if (obj.b == null) {
// 相当于 if(obj.b===null||obj.b===undefined)
console.log('b')
}
原始值和引用值的区别
内存的分配不同
- 原始值存储在栈中
- 引用值存储在堆中,栈中存储的变量,是指向堆中的引用地址
访问机制不同
- 原始值是按值访问
- 引用值按引用访问,JavaScript 不允许直接访问保存在堆内存中的对象,在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象的值
复制变量时不同
- 原始值:a=b;是将 b 中保存的原始值的副本赋值给新变量 a,a 和 b 完全独立,互不影响
- 引用值:a=b;将 b 保存的对象内存的引用地址赋值给了新变量 a;a 和 b 指向了同一个堆内存地址,其中一个值发生了改变,另一个也会改变
比较变量时不同
原始值:==比较值是否相等(先进行类型转换再确定操作数是否相等---引自 js 高级程序设计(第四版) P71),===不仅比较值是否相等,还会比较数据类型是否相同
引用数据类型:不管是 == 还是 === ,都是比较内存地址是否相同,即比较是否都指向同一个对象
参数传递的不同
函数传参都是按值传递(栈中的存储的内容):原始值,拷贝的是值;引用值,拷贝的是引用地址
手写深拷贝
仅仅是解决了深复制的关键问题,还需要针对不同的数据类型进行完善,
lodash
的深拷贝针对不同的数据类型进行了处理,见深入浅出 loadash 深拷贝源码
` copyObjs.push({ target, copyTarget: obj })` 这一步为什么要放在递归遍历之前进行
这一步需要放在递归遍历之前进行,是因为在递归遍历中可能会遇到循环引用的情况,如果不先将新对象或数组保存在 copyObjs
中,那么在处理循环引用时就无法判断当前对象是否已经被拷贝过了,从而可能会陷入无限递归的情况。
举个例子,假设有一个对象 A,其中包含一个属性是指向自己的引用,即 A 的某个属性的值是 A 自身。如果不先将 A 拷贝后的副本保存在 copyObjs
中,那么在递归遍历 A 时会陷入无限递归的情况,因为递归到指向自身的属性时会一直循环下去。
而如果先将 A 拷贝后的副本保存在 copyObjs
中,那么在递归遍历 A 时就可以判断当前对象是否已经被拷贝过了,从而避免陷入无限递归的情况。因此,这一步需要放在递归遍历之前进行。
function deepClone(obj = {}) {
if (typeof obj !== 'object' || obj == null) {
return obj
}
// 初始化返回结果
let result = Array.isArray(target) ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 保证key不是原型的属性
result[key] = deepClone(obj[key])
}
}
// 返回结果
return result
}
function deepClone(target) {
// 此数组存放已经递归到的对象
let copyObjs = []
function _deepClone(target) {
// 不是对象直接返回
if (typeof target !== 'object') return target
// 如果目标对象和copyobjs保存的对象相等 那么不对他进行递归(解决相同引用问题)
for (let i = 0; i < copyObjs.length; i++) {
if (target === copyObjs[i].target) return copyObjs[i].copyTarget
}
let obj = {}
// 处理目标对象是数组的情况
if (Array.isArray(target)) obj = []
// 在递归之前保存已经递归到的目标对象(解决循环引用问题)
copyObjs.push({ target, copyTarget: obj })
Object.keys(target).forEach((key) => {
obj[key] = _deepClone(target[key])
})
return obj
}
return _deepClone(target)
}
JSON.sringify 和 JSON.parse 方法拷贝的缺陷
这是 JS
实现深拷贝最简单的方法了,原理就是先将对象转换为字符串,再通过 JSON.parse
重新建立一个对象。 但是这种方法的局限也很多:
- 不能复制
function
、正则、Symbol
- 循环引用(当对象 1 中的某个属性指向对象 2,对象 2 中的某个属性指向对象 1 就会出现循环引用)报错
- 相同的引用会被重复拷贝
let obj = { asd: 'asd' }
let obj2 = { name: 'aaaaa' }
obj.ttt1 = obj2
obj.ttt2 = obj2
let cp = JSON.parse(JSON.stringify(obj))
obj.ttt1.name = 'change'
cp.ttt1.name = 'change'
console.log(obj, cp)
对于上面的代码,原对象改变 ttt1.name
也会改变 ttt2.name
,因为他们指向相同的对象。但是,复制的对象中,ttt1
和 ttt2
分别指向了两个对象。拷贝的对象没有保持和原对象一样的结构。因此,JSON
实现深拷贝不能处理指向相同引用的情况,相同的引用会被重复拷贝
原型原型链
如何用 class 实现继承
利用es6
的extends
实现继承没难度,可参考《JS
高级程序设计》es5
实现继承的方式
如何理解 JS 原型(隐式原型和显式原型)和原型链
原型和实例的关系:每个构造函数都有一个原型对象,原型有 一个属性指回构造函数,而实例有一个内部指针指向原型
《
JS
高级程序设计》P238
在JavaScript
中是使用构造函数来新建一个对象的(或者使用 class 类实例化),每一个构造函数的内部都有一个 prototype
属性,这个就是显式原型
,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个叫做__proto__
的属性,这个属性指向构造函数的 prototype
属性对应的值,这个就是隐式原型__proto__

ES5
中新增了一个 Object.getPrototypeOf()
方法,可以通过这个方法来获取对象的原型
原型链查找
当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。所以这就是新建的对象为什么能够使用 toString()
等方法的原因
原型链的终点是null
,因为 Object.prototype.__proto__
指向 null
手写一个简易的 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++) {
fn(this[i])
}
}
on(type, fn) {
this.each((elem) => {
elem.addEventListener(type, fn, false)
})
}
}
const $ = new Jquery('p')
console.log($.get(1))
$.on('click', () => {
alert('123')
})
Jquery.prototype.addClass = function (index, className) {
this.get(index).classList.add(className)
}
$.addClass(2, 'wocainima')
// -----------------------------
class JqueryPlus extends Jquery {
constructor(selector) {
super(selector)
}
changeStyle(index, key, value) {
this.get(index).style[key] = value
}
}
const _ = new JqueryPlus('p')
_.changeStyle(0, 'fontSize', '20px')
_.changeStyle(0, 'color', 'red')
简述一下 new 的过程
1)在内存中创建一个新对象
2)将新对象与构造函数通过原型链连接起来
3)将构造函数中的this 绑定到新对象上
4)执行构造函数内部的代码
5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
function Fun(a) {
this.a = a
return { [a]: 1 }
}
const obj = new Fun(2)
console.log(obj) // { '2': 1 }而不是 { a: 2 }
作用域闭包
什么是作用域?什么是自由变量?
全局作用域
声明在任何函数之外的顶层作用域的变量就是全局变量,这样的变量拥有全局作用域
所有未定义直接赋值的变量拥有全局作用域
所有 window 对象的属性拥有全局作用域
全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突
函数作用域
在函数内部定义的变量称为局部变量,拥有函数作用域
只有函数被调用的时候才会形成函数作用域
内层作用域可以访问外层作用域,反之不行
块级作用域
使用 ES6 中新增的 let 和 const 指令可以声明块级作用域
块作用域内的变量只要出了自己被定义的那个代码块,那么就无法访问了。
在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部
自由变量
一个变量在当前作用域没有定义但被使用了
自由变量的查找规则:自由变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方,因为 js 是词法作用域
如何理解闭包
作用域应用的特殊情况,有两种情况:
- 函数作为参数被传递
function print(fn) {
let a = 200
fn()
}
let a = 100
function fn() {
console.log(a)
}
print(fn)
- 函数作为返回值被返回
function create() {
let a = 100
return function () {
console.log(a)
}
}
let fn = create()
let a = 200
fn()
加餐: 词法作用域和动态作用域
- 词法(静态)作用域: 在代码书写的时候完成划分,作用域链沿着它定义的位置往外延伸
- 动态作用域: 在代码运行时完成划分,作用域链沿着它的调用栈往外延伸
var name = 'fan'
function showName() {
console.log(name)
}
function changeName() {
var name = 'hang'
showName()
}
changeName() // 词法: fan 动态:hang
闭包代码输出问题
常见的循环体输出
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, 1000)
}
console.log(i) //5 5 5 5 5
如何让它按 1,2,3,4,5 输出?
- setTimeout 从第三个入参位置开始往后,是可以传入无数个参数的。这些参数会作为回调函数的附加参数存在。
for (var i = 0; i < 5; i++) {
setTimeout(
function (j) {
console.log(j)
},
1000,
i
)
}
- 在 setTimeout 外面再套一层函数,利用这个外部函数的入参来缓存每一个循环中的 i 值:
var output = function (i) {
setTimeout(function () {
console.log(i)
}, 1000)
}
for (var i = 0; i < 5; i++) {
// 这里的 i 被赋值给了 output 作用域内的变量 i
output(i)
}
- 在 setTimeout 外面再套一层函数,只不过这个函数是一个立即执行函数。利用立即执行函数的入参来缓存每一个循环中的 i 值:
for (var i = 0; i < 5; i++) {
// 这里的 i 被赋值给了立即执行函数作用域内的变量 j
;(function (j) {
setTimeout(function () {
console.log(j)
}, 1000)
})(i)
}
讲一下闭包的使用场景
待扩展
模拟私有变量的实现
创建 10 个 a,点击弹出对应的序号
<div id="root"></div>
const root = document.getElementById('root')
for (let i = 1; i <= 10; i++) {
const a = document.createElement('a')
a.innerText = i
a.style.display = 'block'
a.onclick = function (e) {
e.preventDefault()
alert(i)
}
root.appendChild(a)
}
手写 bind 函数
提示
bind()
方法创建一个新的函数(这个包装了目标函数),在 bind()
被调用时,这个新函数的 this
被指定为 bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
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)
- 手写
bind
// 剩余参数是ES6的新特性,一般说来,不支持bind的情况一般也不支持剩余参数,所以,不推荐这种写法
// 不能使用箭头函数 箭头函数不支持arguments
Function.prototype.bind1 = function () {
// 将参数拆解为数组
// call支持最低版本chorme-1
const args = Array.prototype.slice.call(arguments)
// 获取this,数组第一项
const _this = args.shift()
// 返回一个函数
return () => {
// apply支持最低版本chorme-1
// bind支持最低版本chorme-7
this.apply(_this, args)
}
}
// ----------------------------------------
const obj = {
a: 2,
b() {
console.log(this.a)
},
}
const _obj = {
a: 3,
b() {
console.log(this.a)
},
}
obj.b.bind1(_obj)()
this 指向问题
this
取什么值函数执行时确认的,不是在函数定义时确认的
- 作为普通函数使用,指向
window
call()
、apply()
、bind()
都是用来重定义this
这个对象的
bind
返回的是一个新的函数,你必须调用它才会被执行 ,其余两个都是立即执行的
bind
和call
参数形式一致,是直接放进去的,第二第三第 n 个参数全都用逗号分隔;apply
需要把参数写到数组里
call
apply
bind
- 作为对象方法被调用,指向当前对象
- 在
class
方法中调用,指向当前对象 - 箭头函数的
this
永远取它上层作用域的this
异步
异步和同步的区别?
基于 JS 是单线程语言,异步不会阻塞代码执行,同步会阻塞代码执行
手写 promise 加载图片
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>promise加载图片</title>
</head>
<body>
<div id="root"></div>
<script type="text/javascript">
const URL = 'https://zfhblog.top/titlelogo.png'
const root = document.getElementById('root')
function loadImg(url) {
return new Promise((resolve, reject) => {
const img = document.createElement('img')
// 加载完
img.onload = () => {
resolve(img)
}
// 加载失败
img.onerror = () => {
const err = new Error(`图片加载失败了,图片地址为${url}`)
reject(err)
}
img.src = url
})
}
loadImg(URL)
.then((img) => {
root.appendChild(img)
console.log('我第二个执行then')
})
.catch((err) => {
console.log(err)
})
console.log('我第一个执行,同步代码')
setTimeout(() => {
console.log('我第三个执行setTimeout')
}, 0)
</script>
</body>
</html>
setTimeout 结果输出题
console.log(1)
setTimeout(function () {
console.log(2)
}, 1000)
console.log(3)
setTimeout(function () {
console.log(4)
}, 0)
console.log(5)
输出结果
: 1 , 3 , 5 , 4 , 2
描述事件循环机制
二个理解层次
(一)只考虑同步异步:
- 同步代码,一行一行放在
Call Stack
执行 - 遇到异步,会先“记录”下,等待时机(如:定时、网络请求)
- 时机到了,就移动到
Callback Queue
- 如
Call Stack
为空(即同步代码执行完)Event-loop
开始工作 - 轮询查找
Callback Queue
,如有则移动到Call Stack
执行 - 然后继续轮询查找(永动机一样)
(二)考虑微任务宏任务:
call Stack
清空- 执行当前的微任务
- 尝试
DOM
渲染 - 触发
event-loop
什么是宏任务什么是微任务,两者区别?
- 宏任务:setTimeout, setinterval , Ajax , DOM 事件(浏览器规定的)
- 微任务:Promise(then),async/await(ES6 语法规定的)
- 微任务的执行时机比宏任务早
Promise 的三种状态,如何变化?
略
手写 Promise
- 初始化 & 异步调用
- 实例方法
then
catch
链式调用 - 静态方法:
resolve
reject
all
race
any
allSettled
EasyPromise
class EasyPromise {
// promise 的 状态 pending fulfilled rejected
PromiseState = 'pending'
// promise 的 结果
PromiseResult = null
// pending 状态下保存的回调函数任务栈
callBacks = []
constructor(actuator) {
// reslove处理函数
const reslove = (result) => {
if (this.PromiseState !== 'pending') return
this.PromiseResult = result
this.PromiseState = 'fulfilled'
this.callBacks.forEach((fn) => {
fn.onResloved()
})
}
// reject处理函数
const reject = (error) => {
if (this.PromiseState !== 'pending') return
this.PromiseResult = error
this.PromiseState = 'rejected'
this.callBacks.forEach((fn) => {
fn.onRejected()
})
}
try {
actuator(reslove, reject)
} catch (err) {
reject(err)
}
}
then(resloveCallback, rejectCallBack) {
resloveCallback =
typeof resloveCallback === 'function' ? resloveCallback : (v) => v
rejectCallBack =
typeof rejectCallBack === 'function'
? rejectCallBack
: (e) => {
throw e
}
return new EasyPromise((reslove, reject) => {
// 判断返回值的类型
const judgeReturnValueType = (callBack) => {
try {
const result = callBack(this.PromiseResult)
if (result instanceof EasyPromise) {
// 由返回的Promise的状态决定
result.then(
(v) => {
reslove(v)
},
(e) => {
reject(e)
}
)
} else {
reslove(result)
}
} catch (err) {
reject(err)
}
}
switch (this.PromiseState) {
case 'fulfilled':
// 一个微任务
queueMicrotask(() => {
judgeReturnValueType(resloveCallback)
})
break
case 'rejected':
queueMicrotask(() => {
judgeReturnValueType(rejectCallBack)
})
break
default:
this.callBacks.push({
onResloved() {
judgeReturnValueType(resloveCallback)
},
onRejected() {
judgeReturnValueType(rejectCallBack)
},
})
}
})
}
catch(rejectCallBack) {
return this.then(null, rejectCallBack)
}
static all(promiseList = []) {
return new EasyPromise((reslove, reject) => {
const result = [] // 存储PromiseList结果的数组
const length = promiseList.length
let resloveCount = 0 // 结果为成功的个数
promiseList.forEach((p) => {
p.then((data) => {
result.push(data)
resloveCount++
if (resloveCount === length) reslove(result)
}).catch((err) => {
reject(err)
})
})
})
}
static any(promiseList = []) {
return new EasyPromise((reslove, reject) => {
let rejectCount = 0 // 结果为失败的个数
const length = promiseList.length
promiseList.forEach((p) => {
p.then((data) => {
reslove(data)
}).catch(() => {
rejectCount++
if (rejectCount === length) {
reject('All promises were rejected')
}
})
})
})
}
static race(promiseList = []) {
return new EasyPromise((reslove, reject) => {
promiseList.forEach((p) => {
p.then((data) => {
reslove(data)
}).catch((err) => {
reject(err)
})
})
})
}
static allSettled(promiseList = []) {
return new EasyPromise((reslove, reject) => {
let result = []
promiseList.forEach((p) => {
p.then((data) => {
result.push({ status: 'fulfilled', value: data })
}).catch((err) => {
result.push({ status: 'rejected', reason: err })
})
})
reslove(result)
})
}
static reslove(value) {
return new EasyPromise((reslove, reject) => {
reslove(value)
})
}
static reject(reason) {
return new EasyPromise((reslove, reject) => {
reject(reason)
})
}
}
JS-WEB-API
property 和 attribute 的区别
property
:修改对象属性,不会提现到html
结构中attribute
:修改html
属性,会改变html
结构- 两者都有可能引起
DOM
的重新渲染
如何优化 DOM 操作的性能
注意
涉及前端性能优化,此处答案过于简单,后期扩展
- DOM 查询做缓存
// 不缓存DOM查询结果
for (let i = 0; i < document.getElementsByTagName('p').length; i++) {
// 每次循环 都会计算length 频繁进行DOM操作
}
// 缓存DOM查询结果
const pList = document.getElementsByTagName('p')
const length = pList.length
for (let i = 0; i < document.getElementsByTagName('p').length; i++) {
// 缓存length 只进行一次DOM查询
}
- 将频繁操作改为一次操作
const list = document.querySelector('.list')
// 文档切片
const frag = document.createDocumentFragment()
for (let i = 0; i < 10000; i++) {
const title = document.createElement('h1')
title.innerText = `list item ${i}`
frag.appendChild(title)
}
list.appendChild(frag)
编写一个通用的事件监听函数
const bindEvent = (elem, type, selector, fn) => {
if (fn == null) {
fn = selector
selector = null
}
elem.addEventListener(type, (event) => {
const target = event.target
if (selector) {
// 代理
if (target.matches(selector)) {
fn.call(target, event)
}
} else {
// 普通
fn.call(target, event)
}
})
}
无限下拉的图片列表,如何监听每个图片的点击?
事件代理
用
event.target
获取触发元素用``matches`来判断是否是触发元素