new
# new
function myNew(constructor, ...args) {
// 创建空对象并绑定原型
let obj = Object.create(constructor.prototype)
// 执行构造函数并使用apply绑定构造函数的this指向
const result = constructor.apply(obj, args)
// 查看构造函数的返回值。返回值为对象时返回该对象,否则返回第一步创建的空对象
return result instanceof Object ? result : obj
}
# instanceof
function myInstanceof(obj, fun) {
let proto = Object.getPrototypeOf(obj)
while (true) {
if (proto === fun.prototype) return true
if (!proto) return false
proto = Object.getPrototypeOf(proto)
}
}
# Promise
使用Promise实现每隔一秒输出一个数,采用reduce不断给promise添加then:
const arr = [1, 2, 3, 4]
arr.reduce((prev, cur) => {
return prev.then(() => {
return new Promise(r => {
setTimeout(() => r(console.log(x)), 1000)
})
})
}, Promise.resolve())
all和race
function promiseAll(promises) {
let count = 0, res = []
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
promises[i].then((value) => {
count++
res[i] = value
if (count === promises.length) {
resolve(res)
}
}, (reason) => {
reject(reason)
})
}
})
}
function promiseRace(promises) {
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
promises[i].then((value) => {
resolve(value)
}, (reason) => {
reject(reason)
})
}
})
}
Promise实现图片的异步加载:
let loadImage = (url) => {
return new Promise((resolve, reject) => {
let img = new Image()
img.src = url
// 监听Image示例的onload和onerror来决定如何更改Promise的状态
img.onload = () => {
resolve(img)
}
img.onerror = (err) => {
reject(err)
}
})
}
# 防抖节流
防抖:用于鼠标点击、输入框输入等 节流:用于鼠标滚动、页面动画等
// 防抖
function debounce(fun, delay) {
let timer = null
return function () {
let context = this, args = arguments
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fun.apply(context, args)
}, delay)
}
}
// 节流
function throttle(fun, delay) {
let past = 0
return function () {
let context = this, args = arguments, now = Date.now()
if (now - past > delay) {
fun.apply(context, args)
past = now
}
}
}
// 使用定时器方法的节流,同时实现第一次调用立即执行
function throttleTimer(fun, delay) {
let timer = null, immediate = true
return function () {
let args = arguments, context = this
if (immediate) {
fun.apply(context, args)
immediate = false
}
if (timer) return
timer = setTimeout(() => {
fun.apply(context, args)
timer = null
}, delay)
}
}
# call/apply/bind
// call
function myCall(context, ...args) {
context = context || window
context.fn = this
const res = context.fn(...args)
delete context.fn
return res
}
// apply
function muApply(context, args) {
context = context || window
context.fn = this
const res = context.fn(...args)
delete context.fn
return res
}
// bind
function myBind(context) {
let self = this
let arr1 = Array.prototype.slice.call(arguments, 1)
return function Fn() {
let arr2 = Array.prototype.slice.call(arguments)
let arrSum = arr1.concat(arr2)
// 区别以构造函数形式调用bind返回的函数时this的指向
if (this instanceof Fn) {
return self.apply(this, arrSum)
} else {
return self.apply(context, arrSum)
}
}
}
# 函数柯里化
柯里化是一种把拥有多个参数的函数转换为一系列接受一个参数的函数的过程。
function curry(fn, ...args) {
// 判断函数的参数是否和传入的参数数量一致
if (fn.length === args.length) {
return fn(...args)
}
return (...rest) => {
curry(fn, ...args, ...rest)
}
}
# 使用原生和Promise方法封装Ajax请求
// 原生
const xhr = new XMLHttpRequest()
xhr.open('GET', 'xxx', true)
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status <= 300) || xhr.status === 304) {
alert(xhr.responseText)
} else {
alert('Request Unsuccessful')
}
}
}
// 即使请求不发送任何载体,也需要传入null作为send的参数
xhr.send(null)
// 使用Promise封装的时候,请求成功就是fulfilled,失败则是rejected
# 深拷贝和浅拷贝
深拷贝和浅拷贝的区别仅在对对象内部的引用数据类型的拷贝方式,是添加新的引用(浅拷贝)还是重新开辟内存空间复制(深拷贝)
function shallowClone(target) {
const clone = Array.isArray(target) ? [] : {}
for (const key in target) {
if (target.hasOwnProperty(key)) {
clone[key] = target[key]
}
}
return clone
}
function deepClone(target, map=new WeakMap()) {
if (typeof target !== 'object' || target === null) return target
// 支持ES6新增的特殊类型
let reg = /^(Function|Date|RegExp)$/i
if (reg.test(target.constructor.name)) {
return new target.constructor(target)
}
// map解决循环引用问题
if (map.has(target)) return map.get(target)
let clone = Array.isArray(target) ? [] : {}
map.set(target, clone)
for (let key in target) {
if (target.hasOwnProperty(key)) {
clone[key] = deepClone(target[key], map)
}
}
return clone
}
# 数组
数组去重:
// Set
const uniqueArray = (arr) => [...new Set(arr)]
// map哈希表
function uniqueArr(arr) {
let map = new Map(), res = []
for (let i = 0; i < arr.length; i++) {
if (!map.has(arr[i])) {
map.set(arr[i], 1)
res.push(arr[i])
}
}
return res
}
// 双指针
function fun(arr) {
let i = 0
for (let j = 0; j < arr.length; j++) {
if (arr[j] !== arr[i]) {
i++
arr[i] = arr[j]
}
}
return arr.slice(0, i + 1)
}
实现数组的原生方法:
// map
Array.prototype._map = function (fn) {
let res = []
for (let i = 0; i < this.length; i++) {
res.push(fn(this[i]))
}
return res
}
// filter
Array.prototype._filter = function (fn) {
let res = []
for (let i = 0; i < this.length; i++) {
if (fn(this[i])) {
res.push(this[i])
}
}
return res
}
// reduce
Array.prototype._reduce = function (fn ,init) {
let pre = init || this[0]
let index = init ? 0 : 1
for (; index < this.length; index++) {
pre = fn(pre, this[index], index, this)
}
return pre
}
数组扁平化
// 递归
function flat(nums) {
let res = []
for (let i = 0; i < nums.length; i++) {
if (Array.isArray(nums[i])) {
res = res.concat(flat(nums[i]))
} else {
res.push(nums[i])
}
}
return res
}
// some
function flat1(nums) {
while (nums.some((item) => Array.isArray(item))) {
nums = [].concat(...nums)
}
}
// reduce
function flat2(nums) {
return nums.reduce((pre, cur) => {
return pre = pre.concat(Array.isArray(cur) ? flat2(cur) : cur)
}, [])
}
# JS中的各种继承
原型链继承,父类的实例作为子类的原型。在构造多个实例时,原型上的属性修改会反映到所有实例上。
function Parent(name){
this.name = name
}
Parent.prototype.getName = function (){
console.log(this.name)
}
function Child(){
}
Child.prototype = new Parent('Jack')
let person = new Child()
console.log(person.name) //Jack
盗用构造函数继承,无法访问父类原型中定义的方法。
function P(){
this.name = ['Dante']
}
P.prototype.get = function (){
console.log(this.name)
}
function C(){
P.call(this)
}
let a = new C() // console.log(a.get())
a.get() // Uncaught TypeError: a.get is not a function
组合继承:将原型链继承和盗用构造函数继承的方式组合起来的继承,缺点是父类在子类构造实例时被调用了两次。
原型式继承,创建临时构造函数来创建新对象,在ES6中被规范化为新的API:Object.create()
function object(o) {
function F() {}
F.prototype = o
return new F()
}
寄生式继承:创建以目标对象为原型的对象,并向新创建的对象中添加新的属性和方法(继承)
function createDog(animal) {
// 创建一个新对象,继承自animal
const dog = Object.create(animal);
// 增强新对象
dog.breed = 'Labrador';
dog.bark = function() {
console.log('Woof Woof');
};
return dog; // 返回增强后的对象
}
寄生组合继承。
function Parent1(name){
this.name = name
this.weapon = 'power'
}
Parent1.prototype.sayName = function (){
console.log(`this is ${this.weapon}`)
}
function Child(name, age){
this.name = name
// 使用构造函数继承,原型上的属性修改不会反映到每个实例了
Parent1.call(this, name)
this.age = age
}
// Object.create实现原型链继承,可以访问父类原型上定义的方法
Child.prototype = Object.create(Parent.prototype)
// 原型的构造器本身还得是自己
Child.prototype.constructor = Child
let c2 = new Child(20)
console.log(c2.name, c2.weapon, c2.age)
c2.sayName()
# 发布订阅和观察者模式
发布订阅模式
class EventBus {
constructor() {
this.events = {}
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = []
}
this.events[eventName].push(callback)
}
emit(eventName, ...args) {
if (this.events[eventName]) {
const callbacks = this.events[eventName].slice()
callbacks.forEach((callback) => {
callback(...args)
})
}
}
off(eventName, callback) {
const callbacks = this.events[eventName]
const index = callbacks.indexOf(callback)
if (index !== -1) {
callback.splice(index, 1)
}
}
once(eventName, callback) {
const wrapper = (...args) => {
callback(...args)
this.off(eventName, wrapper)
}
this.on(eventName, wrapper)
}
}
观察者模式
class Publisher {
constructor() {
this.observers = []
}
addSub(observer) {
this.observers.push(observer)
}
notify() {
this.observers.forEach((observer) => {
observer.update()
})
}
}
class Sub {
constructor() {
}
update() {
}
}
# 场景手写题
# 前端实现并发请求控制
有大量的请求需要发送但是要求同一时间只能进行一定数量的请求。
async function asyncPool(urls, poolLimit) {
const res = [], pool = []
for (let i = 0; i < urls.length; i++) {
let url = urls[i]
if (pool.length >= poolLimit) {
await Promise.race(pool)
}
let request = fetch(url)
request.
then(result => {
res[i] = result
})
.finally(() => {
pool.splice(pool.indexOf(result), 1)
})
pool.push(request)
}
await Promise.all(pool)
return res
}
# 图片懒加载
在图片到达可视区域之前不加载。
const img = document.querySelector('.img')
if (img.getBoundingClientRect().top <= window.innerHeight) {
img.src = img.getAttribute('data-src')
}
另外一种方法是使用intersectionObserver
API,可以判断元素是否位于可视区域。
# 虚拟列表
渲染长列表时为了节省性能,只渲染可视区域附近的列表项。核心实现原理是找到需要渲染的列表项的索引,startIndex和endIndex。
// 开始渲染的索引,等于列表已经滚动过的高度除以单个列表项的高度
let startIndex = Math.floor(scrollTop / itemHeight)
// 结束索引等于可视区域高度除以单个列表项高度的值 + 开始索引
let endIndex = startIndex + Math.ceil(window.innerHeight / itemHeight)
// 根据两个索引去原始列表中截取需要渲染的子列表
let visibleList = list.slice(startIndex, Math.min(endIndex, list.length - 1))
// 监听页面滚动事件,不断更新startIndex和endIndex的值以更新需要渲染的列表项