Object.getPrototypeOf是一个JavaScript内置函数,用于获取指定对象的原型
Object.create()
是 JavaScript 中用来创建一个新对象,并且可以将其设置为继承自另一个对象的原型对象
1.实现防抖函数(debounce)
防抖函数原理:把触发非常频繁的事件合并成一次去执行 在指定时间内只执行一次回调函数,如果在指定的时间内又触发了该事件,则回调函数的执行时间会基于此刻重新开始计算。
eg. 像百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。
手写简化版:
// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
// 缓存一个定时器id
let timer = 0
// 这里返回的函数是每次用户实际调用的防抖函数
// 如果已经设定过定时器了就清空上一次的定时器
// 开始一个新的定时器,延迟执行用户传入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
适用场景:
- 文本输入的验证,连续输入文字后发送 AJAX 请求进行验证,验证一次就好。
- 按钮提交场景:防止多次提交按钮,只执行最后提交的一次。
- 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似。
2.实现节流函数(throttle)
节流函数原理:指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。总结起来就是:事件,按照一段时间的间隔来进行触发。
像dom的拖拽,如果用防抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多
手写简版
使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行。
时间戳方式:
const throttle = (func, wait = 50) => {
let lastTime = 0;
return function (...args) {
let now = new Date();
if (now - lastTime > wait) {
lastTime = now;
func.apply(this,args)
}
}
}
setInterval(
throttle(()=>{
console.log('1')
},1000)
,1)
定时器方式:
使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数
const throttle1 =(func, wait = 50) => {
let timer = null ;
return function(...args){
if(!timer){
timer = setTimeout(()=>{
func.apply(this,args);
timer = null
},wait)
}
}
}
setInterval(
throttle1(()=>{
console.log('1')
},1000)
,1)
适用场景:
DOM
元素的拖拽功能实现(mousemove
)- 搜索联想(
keyup
) - 计算鼠标移动的距离(
mousemove
) Canvas
模拟画板功能(mousemove
)- 监听滚动事件判断是否到页面底部自动加载更多
- 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
- 缩放场景:监控浏览器
resize
- 动画场景:避免短时间内多次触发动画引起性能问题
总结
- 函数防抖:将几次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
- 函数节流:使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。
3 实现instanceOf
- 步骤1:先取得当前类的原型,当前实例对象的原型链
- 步骤2:一直循环(执行原型链的查找机制)
- 取得当前实例对象原型链的原型链(
proto = proto.__proto__
,沿着原型链一直向上查找)。 - 如果 当前实例的原型链
__proto__
上找到了当前类的原型prototype
,则返回true。
- 如果 一直找到
Object.prototype.__proto__ == null
,Object
的基类(null
)上面都没找到,则返回false。
- 取得当前实例对象原型链的原型链(
// 实例.__proto__ = 类.prototype
function _instanceof(example, classFunc) {
// 基本数据类型直接返回false
if (typeof example !== 'object' || example === null) return false;
let proto = Object.getPrototypeOf(example);
while (true) {
if (proto == null) return false;
if (proto == classFunc.prototype); return true
proto = Object.getPrototypeOf(proto)
}
}
console.log('test', _instanceof(null, Array)) // false
console.log('test', _instanceof([], Array)) // true
console.log('test', _instanceof('', Array)) // false
console.log('test', _instanceof({}, Object)) // true
4.实现new的过程
new操作符做了这些事:
- 创建一个全新的对象
- 这个对象的
__proto__
要指向构造函数的原型prototype - 执行构造函数,使用
call/apply
改变 this 的指向 - 返回值为
object
类型则作为new
方法的返回值返回,否则返回上述全新对象
function myNew(fn, ...args) {
// 基于原型链 创建一个新对象
let newObj = Object.create(fn.prototype);
// 添加属性到新对象上 并获取obj函数的结果
let res = fn.apply(newObj, args); // 改变this指向
// 如果执行结果有返回值并且是一个对象, 返回执行的结果, 否则, 返回新创建的对象
return typeof res === 'object' ? res: newObj;
}
// 用法
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function() {
console.log(this.age);
};
let p1 = myNew(Person, "poety", 18);
console.log(p1.name);
console.log(p1);
p1.say();
5.实现call方法
call做了什么:
- 将函数设为对象的属性
- 执行和删除这个函数
- 指定
this
到函数并传入给定参数执行函数 - 如果不传入参数,默认指向为
window
Function.prototype.myCall = function (context = window, ...args) {
if (typeof this !== 'function') {
throw new Error('type error')
}
let key = Symbol('key');
// this就是fn
// this() fn()
context[key] = this;
// 利用context进行调用
let result = context[key](...args);
delete context[key];
return result
}
//用法:f.call(obj,arg1)
function f(a, b) {
console.log(a + b)
console.log(this.name)
}
let obj = {
name: 1
}
f.myCall(obj, 1, 2) //否则this指向window
6.实现apply方法
思路: 利用this
的上下文特性。apply
其实就是改一下参数的问题
Function.prototype.myApply = function (context = window, args) {
if (typeof this !== 'function') {
throw new Error('type Error')
}
let key = Symbol('key')
context[key] = this;
let result = context[key](...args)
delete context[key]
return result
}
// 使用
function f(a, b) {
console.log(a, b)
console.log(this.name)
}
let obj = {
name: '张三'
}
f.myApply(obj, [1, 2]) //arguments[1]
7.实现bind方法
bind
的实现对比其他两个函数略微地复杂了一点,涉及到参数合并(类似函数柯里化),因为bind
需要返回一个函数,需要判断一些边界问题,以下是bind
的实现
Function.prototype.myBind = function (context, ...args1) {
let fn = this;
return function (...args2) {
return fn.apply(context, [...args1, ...args2]);
};
};
8.实现深拷贝
简单版:
const newObj = JSON.parse(JSON.stringify(oldObj));
局限性:
- 他无法实现对函数 、RegExp等特殊对象的克隆,因为在JSON中,函数和RegExp等特殊对象的值会被转化为
null
。 - 会抛弃对象的
constructo
r,所有的构造函数会指向Object,
由于 JSON.stringify() 会把对象转化为 JSON 字符串,再通过 JSON.parse() 解析成对象,这个过程中会丢失对象的 constructor 属性,导致所有对象的 constructor 都指向 Object。 - 对象有循环引用,会报错,当被克隆的对象中存在循环引用时,JSON.stringify() 会抛出异常,因为 JSON 格式不支持循环引用。例如,如果对象A中引用了对象B,而对象B又引用了对象A,那么 JSON.stringify(A) 会抛出异常。
面试简版
function deepClone(obj) {
// 如果是 值类型 或 null,则直接return
if(typeof obj !== 'object' || obj === null) {
return obj
}
// 定义结果对象
let copy = {}
// 如果对象是数组,则定义结果数组
if(obj.constructor === Array) {
copy = []
}
// 遍历对象的key
for(let key in obj) {
// 如果key是对象的自有属性
if(obj.hasOwnProperty(key)) {
// 递归调用深拷贝方法
copy[key] = deepClone(obj[key])
}
}
return copy
}
调用深拷贝方法,若属性为值类型,则直接返回;若属性为引用类型,则递归遍历。这就是我们在解这一类题时的核心的方法。
进阶版
- 解决拷贝循环引用问题
- 解决拷贝对应原型问题
function deepClone(value, hash = new WeakMap) {
// 如果是值类型或者null ,则直接return
if (value == null) return value
if (value instanceof RegExp) return new RegExp(value);
if (value instanceof Date) return new Date(value);
if (typeof value !== 'object') {
return value
}
let obj = new value.constructor();
// 说明是一个对象类型
if (hash.get(value)) {
return hash.get(value)
}
hash.set(value, obj);
// 遍历对象的key
for (let key in value) {
// 如果key是对象自有属性
if (value.hasOwnProperty(key)) {
obj[key] = deepClone(value[key], hash)
}
}
return obj
}
var o = {};
o.x = o;
var o1 = deepClone(o); // 如果这个对象拷贝过了 就返回那个拷贝的结果就可以了
console.log(o1);
9.实现类的继承
1. 寄生组合继承
function Parent(name) {
this.name = name;
}
Parent.prototype.say = function () {
console.log(this.name + ` say`);
}
Parent.prototype.play = function () {
console.log(this.name + ` play`);
}
function Child(name, parent) {
//
Parent.call(this, parent);
this.name = name;
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.say = Parent.prototype.play = function () {
console.log(this.name + ` play`);
}
// 注意记得把子类的构造指向子类本身
Child.prototype.constructor = Child;
var parent = new Parent('parent');
parent.say()
var child = new Child('child');
child.say()
child.play(); // 继承父类的方法
10.实现Promise相关方法
1.实现Promise相关方法
实现 resolve 静态方法有三个要点:
- 传参为一个
Promise
, 则直接返回它。
const promise1 = Promise.resolve('resolved');
const resultPromise = Promise.resolve(promise1);
console.log(resultPromise === promise1); // true
在这个例子中,
promise1
是一个已经 resolved 的 Promise 对象,调用Promise.resolve
方法时传入了promise1
对象,因此返回的 Promise 对象直接就是promise1
对象本身,而不是新创建的一个 Promise 对象。因此,resultPromise
和promise1
引用同一个对象,它们完全相等
- 传参为一个
thenable
对象,返回的Promise
会跟随这个对象,采用它的最终状态作为自己的状态。
const thenable = {
then(resolve, reject) {
setTimeout(() => {
resolve('resolved from thenable');
}, 1000);
}
};
const resultPromise = Promise.resolve(thenable);
resultPromise.then(value => {
console.log(value); // resolved from thenable
});
在这个例子中,
thenable
对象具有 then 方法,因此可以认为它是一个 thenable 对象。调用Promise.resolve
方法时传入了thenable
对象,因此返回的 Promise 对象会跟随thenable
对象,等待它的状态发生变化。在thenable
对象的then
方法中,我们使用了setTimeout
来模拟异步操作,1 秒后将其状态设置为 resolved,这时候返回的 Promise 对象的状态也会相应地变为 resolved,成功回调函数会被调用,并输出resolved from thenable
。
- 其他情况,直接返回以该值为成功状态的
promise
对象。
const resultPromise = Promise.resolve('resolved');
resultPromise.then(value => {
console.log(value); // resolved
});
调用
Promise.resolve
方法时传入了一个普通的字符串'resolved'
,因此返回的 Promise 对象的状态会直接被设置为 resolved,成功回调函数会被调用,并输出'resolved'
。
Promise.reject = (param) => {
// 如果 param 是一个 Promise 对象,则直接返回该对象,不需要再创建新的 Promise 对象。
if (param instanceof Promise) return param;
// 如果 param 是一个 thenable 对象,则返回一个 Promise 对象,该对象的状态会跟随 param 对象,采用它的最终状态作为自己的状态。
if(param && param.then && typeof param.then==='function'){
// param 状态变为成功会调用resolve,将新 Promise 的状态变为成功,反之亦然
param.then(resolve,reject)
}else{
// 如果 param 不是 Promise 对象也不是 thenable 对象,则返回一个以 param 为成功状态的 Promise 对象。
resolve(param)
}
}
2 实现 Promise.reject
Promise.reject 中传入的参数会作为一个 reason 原封不动地往下传, 实现如下:
Promise.reject = function (reason) {
return new Promise((resolve, reject) => {
reject(reason);
});
}
3 实现 Promise.prototype.finally
- 无论 Promise 的状态是成功还是失败,finally 方法中的回调函数都会被执行。
如果 finally 回调函数中返回的 Promise 对象被 reject,且前面没有捕获该错误的处理函数,那么该错误会传递到后面的
then
的err
处理函数中。如果 finally 回调函数中返回的 Promise 对象被 resolve,那么它将不会影响原来 Promise 对象的状态和值,也不会改变后面的
then
的状态和值。- 如果 finally 方法中有异步操作,如 Promise,它会等待异步操作完成后再将原 Promise 对象的状态传递给下一个 then 方法。也就是说,如果 finally 方法中包含异步操作,后面的 then 方法会等待它们全部执行完毕,再将原 Promise 对象的状态传递给下一个 then 方法。
Promise.prototype.finally = function (callback) {
//返回一个新的promise对象
return this.then((data) => {
return Promise.resolve(callback()).then(() => data)
}, err => {
return Promise.resolve(callback()).then(() => {
throw err
})
})
}
4 实现 Promise.all
1.如果传入参数为一个空的可迭代对象,则直接进行 resolve。
如果传入的可迭代对象是空的,即没有任何元素,Promise.all 方法会立即返回一个已完成的 Promise 对象,其值为一个空数组。例如:
Promise.all([]).then((result) => {
console.log(result); // []
});
2.如果参数中有一个 promise 失败,那么 Promise.all 返回的 promise 对象失败。
Promise.all([
Promise.resolve(1),
Promise.reject(new Error("Error")),
Promise.resolve(3)
]).catch((error) => {
console.log(error); // Error: Error
});
3.在任何情况下,Promise.all 返回的 promise 的完成状态的结果都是一个数组。
Promise.all([
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
]).then((result) => {
console.log(result); // [1, 2, 3]
});
Promise.all = function (promises) {
return new Promise((resolve, reject) => {
// 存储每一个 promise 对象的解决结果
let result = [];
// 计数器 index 来记录已解决的 promise 数量。
let index = 0;
let len = promises.length;
if (len === 0) {
resolve(result)
return;
}
for (let i = 0; i < len; i++) {
Promise.resolve(promises[i]).then(data => {
result[i] = data;
index++
if (index === len) resolve(result)
}).catch(err => {
reject(err)
})
}
})
}
5 实现promise.allsettle
Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的promise`结果
假设有三个异步函数
fetchData1()
,fetchData2()
,fetchData3()
分别用来从服务器获取数据,这些函数都返回一个 Promise 对象。我们想要同时获取这三个异步操作的结果,并根据结果采取不同的操作,我们可以使用Promise.allSettled()
方法来实现。
const promises = [fetchData1(), fetchData2(), fetchData3()];
Promise.allSettled(promises)
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log(`获取数据成功: ${result.value}`);
} else {
console.log(`获取数据失败: ${result.reason}`);
}
});
});
在这个例子中,我们创建了一个 Promise 对象数组
promises
,其中包含了三个异步函数的返回值。然后,我们调用Promise.allSettled(promises)
方法来等待这三个异步操作都完成。当所有的异步操作完成后,Promise.allSettled()
方法返回一个 Promise 对象,该对象带有一个结果数组,其中每个结果表示对应的 Promise 对象的执行结果。在
.then()
方法中,我们遍历了结果数组,并根据每个结果的status
属性来判断对应的 Promise 对象是否成功执行。如果成功执行,则打印出获取数据成功的信息,否则打印出获取数据失败的信息。
function isPromise (val) {
return typeof val.then === 'function'; // (123).then => undefined
}
Promise.allSettled = function(promises) {
return new Promise((resolve, reject) => {
let arr = [];
let times = 0;
const setData = (index, data) => {
arr[index] = data;
if (++times === promises.length) {
resolve(arr);
}
console.log('times', times)
}
for (let i = 0; i < promises.length; i++) {
let current = promises[i];
if (isPromise(current)) {
current.then((data) => {
setData(i, { status: 'fulfilled', value: data });
}, err => {
setData(i, { status: 'rejected', value: err })
})
} else {
setData(i, { status: 'fulfilled', value: current })
}
}
})
}
6 实现 Promise.race
race 的实现相比之下就简单一些,只要有一个 promise 执行完,直接 resolve 并停止执行
Promise.race = function(promises) {
return new Promise((resolve, reject) => {
let len = promises.length;
if(len === 0) return;
for(let i = 0; i < len; i++) {
Promise.resolve(promise[i]).then(data => {
resolve(data);
return;
}).catch(err => {
reject(err);
return;
})
}
})
}
7.实现一个Promise
11 实现发布订阅模式
发布订阅者模式,一种对象间一对多的依赖关系,但一个对象的状态发生改变时,所依赖它的对象都将得到状态改变的通知。
主要的作用(优点):
- 广泛应用于异步编程中(替代了传递回调函数)
- 对象之间松散耦合的编写代码
缺点:
- 创建订阅者本身要消耗一定的时间和内存
- 多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护
实现的思路:
- 创建一个对象(缓存列表)
on
方法用来把回调函数fn
都加到缓存列表中emit
根据key
值去执行对应缓存列表中的函数off
方法可以根据key
值取消订阅
class EventEmiter {
constructor() {
this._events = {}
}
// 订阅事件的方法
on(eventName, callback) {
if (!this._events) {
this._events = {}
}
// 合并之前订阅的cb
this._events[eventName] = [...(this._events[eventName] || []), callback]
}
// 触发事件的方法
emit(eventName, ...args) {
if (!this._events[eventName]) {
return
}
// 遍历执行所有订阅的事件
this._events[eventName].forEach(fn => {
fn(...args)
});
}
off(eventName, cb) {
if (!this._events[eventName]) {
return
}
// 遍历执行所有订阅的事件
this._events[eventName] = this._events[eventName].filter(fn => fn != cb && fn.l != cb)
}
once(eventName, callback) {
const one = (...args) => {
// 等callback执行完毕在删除
callback(args)
this.off(eventName, one)
}
one.l = callback // 自定义属性
this.on(eventName, one)
}
}
let event = new EventEmiter();
let login1 = function (...args) {
console.log('login success1', args)
}
let login2 = function (...args) {
console.log('login success2', args)
}
event.on('login', login1)
//
event.once('login', login2)
event.off('login', login1) // 解除订阅
event.emit('login', 1, 2, 3, 4, 5)
event.emit('login', 6, 7, 8, 9)
event.emit('login', 10, 11, 12)
发布订阅者模式和观察者模式的区别?
- 发布/订阅模式是观察者模式的一种变形,两者区别在于,发布/订阅模式在观察者模式的基础上,在目标和观察者之间增加一个调度中心。
- 观察者模式是由具体目标调度,比如当事件触发,
Subject
就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。 - 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。
12.实现观察者模式
观察者模式(基于发布订阅模式) 有观察者,也有被观察者
观察者需要放到被观察者中,被观察者的状态变化需要通知观察者 我变化了 内部也是基于发布订阅模式,收集观察者,状态变化后要主动通知观察者
class Subject { // 被观察者 学生
constructor() {
this.state = 'happy'
this.observers = []; // 存储所有的观察者
}
// 收集所有的观察者
attach(o) {
this.observers.push(o)
}
// 更新被观察者 状态的方法
setState(newState) {
this.state = newState; // 更新状态
// this 指被观察者 学生
this.observers.forEach(o => o.update(this)) // 通知观察者 更新它们的状态
}
}
class Observer { // 观察者 父母和老师
constructor(name) {
this.name = name;
}
update(student) {
console.log('当前' + this.name + '被通知了', '当前学生的状态是' + student.state)
}
}
let student = new Subject('学生');
let parent = new Observer('父母');
let teacher = new Observer('老师');
// 被观察者存储观察者的前提,需要先接纳观察者
student.attach(parent);
student.attach(teacher);
student.setState('被欺负了')
13.实现单例模式
核心要点: 用闭包和
Proxy
属性拦截使用闭包和 Proxy 属性拦截可以很好地实现单例模式。具体来说,我们可以通过闭包来确保只创建一个实例,然后使用 Proxy 属性拦截来防止对该实例进行不必要的操作或修改
在实现中,我们可以先将类的构造函数定义为私有属性,并通过闭包来创建一个实例。然后,我们可以使用 Proxy 对象来拦截对该实例的属性读取、赋值和删除操作,以确保只有一个实例并且不被修改。最后,我们可以将实例作为单例对象的公共属性,使其可以被全局访问。
const Singleton = (function () {
let instance = null;
const SingletonClass = function () {
// ...私有属性和方法
}
// construct 方法用于拦截 SingletonClass 的构造函数,
// 它检查是否已经创建了实例,如果没有则创建实例,否则返回已经存在的实例。
return new Proxy(SingletonClass, {
construct(target, args) {
if (!instance) {
instance = new target(...args);
}
return instance;
},
get(target, prop) {
return instance[prop];
},
set(target, prop, value) {
// 防止修改属性
throw new Error('Cannot modify singleton instance');
},
deleteProperty(target, prop) {
// 防止删除属性
throw new Error('Cannot delete singleton instance property');
}
})
})()
// 使用示例
const s1 = new Singleton(); // 创建单例实例
const s2 = new Singleton(); // 获取已有单例实例
console.log(s1 === s2); // true,两个实例是同一个对象
s1.foo = 'bar'; // 不能修改属性,会抛出错误
console.log(s1.foo); // undefined
delete s1.foo; // 不能删除属性,会抛出错误
14 实现Ajax
步骤
- 创建
XMLHttpRequest
实例 - 发出 HTTP 请求
- 服务器返回 XML 格式的字符串
- JS 解析 XML,并更新局部页面
- 不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON。
function ajax() {
let xhr = new XMLHttpRequest();
xhr.open('get', 'http://127.0.0.1')
xhr.onreadystatechange = (request, resonese) => {
console.log(xhr.readyState)
if (xhr.readyState === 4) {
console.log(xhr.status)
if (xhr.status >= 200 && xhr.status < 300) {
console.log('成功')
let string = xhr.responseText
//JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象
let object = JSON.parse(string)
}
}
}
//参数2,url。参数三:异步
xhr.send() //用于实际发出 HTTP 请求。不带参数为GET请求
}
Promise实现
基于Promise
封装Ajax
- 返回一个新的
Promise
实例 - 创建
HMLHttpRequest
异步对象 - 调用
open
方法,打开url
,与服务器建立链接(发送前的一些处理) - 监听
Ajax
状态信息 - 如果
xhr.readyState == 4
(表示服务器响应完成,可以获取使用服务器的响应了)xhr.status == 200
,返回resolve
状态xhr.status == 404
,返回reject
状态
xhr.readyState !== 4
,把请求主体的信息基于send
发送给服务器
function ajax(url) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('get', url)
xhr.onreadystatechange = () => {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status <= 300) {
resolve(JSON.parse(xhr.responseText))
} else {
reject('请求出错')
}
}
}
xhr.send() //发送hppt请求
})
}
let url = '/data.json'
ajax(url).then(res => console.log(res))
.catch(reason => console.log(reason))
15 实现JSONP方法
利用
<script>
标签不受跨域限制的特点,缺点是只能支持get
请求
- 创建
script
标签 - 设置
script
标签的src
属性,以问号传递参数,设置好回调函数callback
名称 - 插入到
html
文本中 - 调用回调函数,
res
参数就是获取的数据
function jsonp({
url,
params,
callback
}) {
return new Promise((reslove, reject) => {
let script = document.createElement('script');
// 只有当服务器返回数据时,才会执行该函数。
window[callback] = function (data) {
reslove(data);
document.body.removeChild(script)
}
let arr = [];
for (let key in params) {
arr.push(`${key}=${params[key]}`)
}
script.type = 'text/javascript';
script.src = `${url}?callback=${callback}&${arr.join('&')}`
document.body.appendChild(script)
})
}
// 测试用例
jsonp({
url: 'http://suggest.taobao.com/sug',
callback: 'getData',
params: {
q: 'iphone手机',
code: 'utf-8'
},
}).then(data => {
console.log(data)
})
16.实现async/await
17 基于Generator函数实现async/await原理
18.实现ES6的const
enumerable
:表示该属性是否可枚举。如果设置为false
,则该属性不会出现在for...in
循环中,也不会被Object.keys()
、Object.values()
、Object.entries()
等方法返回。
configurable
:表示该属性是否可配置。如果设置为false
,则该属性不可以使用delete
运算符删除,也不可以重新定义属性描述符。
var __const = function __const (data, value) {
window.data = value // 把要定义的data挂载到window下,并赋值value
Object.defineProperty(window, data, { // 利用Object.defineProperty的能力劫持当前对象,并修改其属性描述符
enumerable: false,
configurable: false,
get: function () {
return value
},
set: function (data) {
if (data !== value) { // 当要对当前属性进行赋值时,则抛出错误!
throw new TypeError('Assignment to constant variable.')
} else {
return value
}
}
})
}
__const('a', 10)
console.log(a)
delete a
console.log(a)
for (let item in window) { // 因为const定义的属性在global下也是不存在的,所以用到了enumerable: false来模拟这一功能
if (item === 'a') { // 因为不可枚举,所以不执行
console.log(window[item])
}
}
a = 20 // 报错
19 实现一个迭代器生成函数
JS原生的集合类型数据结构,只有Array
(数组)和Object
(对象);而ES6
中,又新增了Map
和Set
。四种数据结构各自有着自己特别的内部实现,但我们仍期待以同样的一套规则去遍历它们,所以ES6
在推出新数据结构的同时也推出了一套统一的接口机制——迭代器(Iterator
)。
ES6
约定,任何数据结构只要具备Symbol.iterator
属性(这个属性就是Iterator
的具体实现,它本质上是当前数据结构默认的迭代器生成函数),就可以被遍历——准确地说,是被for...of...
循环和迭代器的next方法遍历。 事实上,for...of...
的背后正是对next
方法的反复调用。
在ES6中,针对Array
、Map
、Set
、String
、TypedArray
、函数的 arguments
对象、NodeList
对象这些原生的数据结构都可以通过for...of...
进行遍历。原理都是一样的,此处我们拿最简单的数组进行举例,当我们用for...of...
遍历数组时:
const arr = [1, 2, 3]
const len = arr.length
for(item of arr) {
console.log(`当前元素是${item}`)
}
之所以能够按顺序一次一次地拿到数组里的每一个成员,是因为我们借助数组的Symbol.iterator
生成了它对应的迭代器对象,通过反复调用迭代器对象的next
方法访问了数组成员,像这样:
const arr = [1, 2, 3]
// 通过调用iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()
// 对迭代器对象执行next,就能逐个访问集合的成员
iterator.next()
iterator.next()
iterator.next()
而for...of...
做的事情,基本等价于下面这通操作:
// 通过调用iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()
// 初始化一个迭代结果
let now = { done: false }
// 循环往外迭代成员
while(!now.done) {
now = iterator.next()
if(!now.done) {
console.log(`现在遍历到了${now.value}`)
}
}
可以看出,for...of...
其实就是iterator
循环调用换了种写法。在ES6中我们之所以能够开心地用for...of...
遍历各种各种的集合,全靠迭代器模式在背后给力。
2 实现迭代器生成函数
我们说迭代器对象全凭迭代器生成函数帮我们生成。在ES6
中,实现一个迭代器生成函数并不是什么难事儿,因为ES6早帮我们考虑好了全套的解决方案,内置了贴心的生成器(Generator
)供我们使用:
// 编写一个迭代器生成函数
function *iteratorGenerator() {
yield '1号选手'
yield '2号选手'
yield '3号选手'
}
const iterator = iteratorGenerator()
iterator.next()
iterator.next()
iterator.next()
用
ES5
去写一个能够生成迭代器对象的迭代器生成函数(解析在注释里):
// 定义生成器函数,入参是任意集合
function iteratorGenerator(list) {
// idx记录当前访问的索引
var idx = 0
// len记录传入集合的长度
var len = list.length
return {
// 自定义next方法
next: function() {
// 如果索引还没有超出集合长度,done为false
var done = idx >= len
// 如果done为false,则可以继续取值
var value = !done ? list[idx++] : undefined
// 将当前值与遍历是否完毕(done)返回
return {
done: done,
value: value
}
}
}
}
var iterator = iteratorGenerator(['1号选手', '2号选手', '3号选手'])
iterator.next()
iterator.next()
iterator.next()
20 实现ES6的extends
function extend(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
Object.setPrototypeOf(subClass, superClass);
}
- 通过
Object.create(superClass.prototype)
创建一个空对象,将其作为subClass
的原型对象,以实现原型链继承。 - 将
subClass.prototype.constructor
设置为subClass
,因为通过第一步的操作,subClass.prototype
的构造函数已经被设置为superClass
,需要将其修正回来。 - 使用
Object.setPrototypeOf(subClass, superClass)
将subClass
的原型对象指向superClass
,以实现静态方法和属性的继承。
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(`Hello, ${this.name}!`);
}
}
class Student extends Person {
constructor(name, grade) {
super(name);
this.grade = grade;
}
study() {
console.log(`${this.name} is studying in grade ${this.grade}.`);
}
}
extend(Student, Person);
const john = new Student('John', 5);
john.sayHello(); // "Hello, John!"
john.study(); // "John is studying in grade 5."
21 实现Object.create
Object.create()
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
// 模拟 Object.create
function create(proto) {
function F() {}
F.prototype = proto;
return new F();
}
22 实现Object.freeze
Object.freeze
冻结一个对象,让其不能再添加/删除属性,也不能修改该对象已有属性的可枚举性、可配置可写性,也不能修改已有属性的值和它的原型属性,最后返回一个和传入参数相同的对象
function myFreeze(obj) {
if (obj instanceof Object) {
Object.seal(obj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
Object.defineProperty(obj, key, {
writable: false
})
myFreeze(obj[key]);
}
}
}
return obj;
}
23 实现Object.is
Object.is
不会转换被比较的两个值的类型,这点和===
更为相似,他们之间也存在一些区别
NaN
在===
中是不相等的,而在Object.is
中是相等的+0
和-
0在===
中是相等的,而在Object.is
中是不相等的
Object.is = function (x, y) {
if (x === y) {
// 当前情况下,只有一种情况是特殊的,即 +0 -0
// 如果 x !== 0,则返回true
// 如果 x === 0,则需要判断+0和-0,则可以直接使用 1/+0 === Infinity 和 1/-0 === -Infinity来进行判断
return x !== 0 || 1 / x === 1 / y;
}
// x !== y 的情况下,只需要判断是否为NaN,如果x!==x,则说明x是NaN,同理y也一样
// x和y同时为NaN时,返回true
return x !== x && y !== y;
}
24.实现一个compose函数
组合多个函数,从右到左,比如:
compose(f, g, h)
最终得到这个结果(...args) => f(g(h(...args))).
这种组合方式从右向左执行函数,即先执行h函数,再执行g函数,最后执行f函数。最终返回的是一个函数,这个函数可以接受任意参数,将这些参数依次传入第一个函数f,然后依次执行后面的函数,直到最后一个函数执行完毕并返回结果。
题目描述:实现一个 compose
函数,这个意思是将多个函数合并成一个函数,这个合并后的函数会依次执行传入的每个函数,并将前一个函数的执行结果作为后一个函数的输入参数,直到最后一个函数执行完毕并返回结果
// 用法如下:
function fn1(x) {
return x + 1;
}
function fn2(x) {
return x + 2;
}
function fn3(x) {
return x + 3;
}
function fn4(x) {
return x + 4;
}
const a = compose(fn1, fn2, fn3, fn4);
console.log(a(1)); // 1+4+3+2+1=11
function compose(...funcs) {
if (!funcs.length) return (v) => v;
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => {
return (...args) => a(b(...args)))
}
}
compose
创建了一个从右向左执行的数据流。如果要实现从左到右的数据流,可以直接更改compose
的部分代码即可实现
- 更换
Api
接口:把reduce
改为reduceRight
- 交互包裹位置:把
a(b(...args))
改为b(a(...args))
25 setTimeout与setInterval实现
1 setTimeout 模拟实现 setInterval
题目描述: setInterval
用来实现循环定时调用 可能会存在一定的问题 能用 setTimeout
解决吗
function mySetInterval(fn, t) {
let timerId = null;
function interval() {
fn();
timerId = setTimeout(interval, t); // 递归调用
}
timerId = setTimeout(interval, t); // 首次调用
return {
// 利用闭包的特性 保存timerId
cancel:() => {
clearTimeout(timerId)
}
}
}
// 测试
var a = mySetInterval(()=>{
console.log(111);
},1000)
var b = mySetInterval(() => {
console.log(222)
}, 1000)
// 终止定时器
a.cancel()
b.cancel()
为什么要用
setTimeout
模拟实现setInterval
?setInterval
的缺陷是什么?
setInterval(fn(), N);
上面这句代码的意思其实是
fn()
将会在N
秒之后被推入任务队列。在setInterval
被推入任务队列时,如果在它前面有很多任务或者某个任务等待时间较长比如网络请求等,那么这个定时器的执行时间和我们预定它执行的时间可能并不一致。
// 最常见的出现的就是,当我们需要使用 ajax 轮询服务器是否有新数据时,必定会有一些人会使用 setInterval,然而无论网络状况如何,它都会去一遍又一遍的发送请求,最后的间隔时间可能和原定的时间有很大的出入
// 做一个网络轮询,每一秒查询一次数据。
let startTime = new Date().getTime();
let count = 0;
setInterval(() => {
let i = 0;
while (i++ < 10000000); // 假设的网络延迟
count++;
console.log(
"与原设定的间隔时差了:",
new Date().getTime() - (startTime + count * 1000),
"毫秒"
);
}, 1000)
// 输出:
// 与原设定的间隔时差了: 567 毫秒
// 与原设定的间隔时差了: 552 毫秒
// 与原设定的间隔时差了: 563 毫秒
// 与原设定的间隔时差了: 554 毫秒(2次)
// 与原设定的间隔时差了: 564 毫秒
// 与原设定的间隔时差了: 602 毫秒
// 与原设定的间隔时差了: 573 毫秒
// 与原设定的间隔时差了: 633 毫秒
再次强调,定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行
setInterval有两个缺点
- 使用
setInterval
时,某些间隔会被跳过 - 可能多个定时器会连续执行
可以这么理解:每个setTimeout
产生的任务会直接push
到任务队列中;而setInterval
在每次把任务push
到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中)。因而我们一般用setTimeout
模拟setInterval
,来规避掉上面的缺点。
2 setInterval 模拟实现 setTimeout
const mySetTimeout = (fn, t) => {
const timer = setInterval(() => {
clearInterval(timer);
fn();
}, t);
};
// 测试
// mySetTimeout(()=>{
// console.log(1);
// },1000)
26 实现Node的require方法
require 基本原理
27 实现LRU淘汰算法
假如我们有一块内存,专门用来缓存我们最近发访问的网页,访问一个新网页,我们就会往内存中添加一个网页地址,随着网页的不断增加,内存存满了,这个时候我们就需要考虑删除一些网页了。这个时候我们找到内存中最早访问的那个网页地址,然后把它删掉。这一整个过程就可以称之为
LRU
算法
梳理实现 LRU 思路
- 特点分析:
- 我们需要一块有限的存储空间,因为无限的化就没必要使用
LRU
算发删除数据了。 - 我们这块存储空间里面存储的数据需要是有序的,因为我们必须要顺序来删除数据,所以可以考虑使用
Array
、Map
数据结构来存储,不能使用Object
,因为它是无序的。 - 我们能够删除或者添加以及获取到这块存储空间中的指定数据。
- 存储空间存满之后,在添加数据时,会自动删除时间最久远的那条数据。
- 我们需要一块有限的存储空间,因为无限的化就没必要使用
- 实现需求:
- 实现一个
LRUCache
类型,用来充当存储空间 - 采用
Map
数据结构存储数据,因为它的存取时间复杂度为O(1)
,数组为O(n)
- 实现
get
和set
方法,用来获取和添加数据 - 我们的存储空间有长度限制,所以无需提供删除方法,存储满之后,自动删除最久远的那条数据
- 当使用
get
获取数据后,该条数据需要更新到最前面
- 实现一个
class LRUCache {
constructor(length) {
this.length = length; // 存储长度
this.data = new Map(); // 存储数据
}
// 存储数据,通过键值对的方式
set(key, value) {
const data = this.data;
if (data.has(key)) {
data.delete(key)
}
data.set(key, value);
// 如果超出了容量,则需要删除最久的数据
if (data.size > this.length) {
const delKey = data.keys().next().value;
data.delete(delKey);
}
}
// 获取数据
get(key) {
const data = this.data;
// 未找到
if (!data.has(key)) {
return null;
}
const value = data.get(key); // 获取元素
data.delete(key); // 删除元素
data.set(key, value); // 重新插入元素
return value // 返回获取的值
}
}
var lruCache = new LRUCache(5);
set 方法
:往map
里面添加新数据,如果添加的数据存在了,则先删除该条数据,然后再添加。如果添加数据后超长了,则需要删除最久远的一条数据。data.keys().next().value
便是获取最后一条数据的意思。get 方法
:首先从map
对象中拿出该条数据,然后删除该条数据,最后再重新插入该条数据,确保将该条数据移动到最前面
// 测试
// 存储数据 set:
lruCache.set('name', 'test');
lruCache.set('age', 10);
lruCache.set('sex', '男');
lruCache.set('height', 180);
lruCache.set('weight', '120');
console.log(lruCache);
继续插入数据,此时会超长,代码如下:
lruCache.set('grade', '100');
console.log(lruCache);
我们使用 get
获取数据,代码如下:
我们发现此时 sex
字段已经跑到最前面去了
总结
LRU
算法其实逻辑非常的简单,明白了原理之后实现起来非常的简单。最主要的是我们需要使用什么数据结构来存储数据,因为map
的存取非常快,所以我们采用了它,当然数组其实也可以实现的。还有一些小伙伴使用链表来实现LRU
,这当然也是可以的。