前言
在js中有许多常用方法,但是往往大部分人只知道用法但不知道其原理,这样会造成很容易记混或者忘记。而自己去实现这些方法可以巩固我们的记忆,同时也能加深我们对javascript这门编程语言的认识。
new
面试中经常会问到在我们使用new关键字的时候,具体发生了什么。自己实现一个其实就一目了然。
function myNew(func,...args) { //传入构造函数及参数
const instance = {}; //生成一个空对象
if(func.prototype) { //为指定空对象的原型为构造函数的prototype
Object.setPrototypeOf(instance,func.prototype); //为指定空对象的原型为构造函数的prototype
}
const res = func.apply(instance,args); //指定构造函数的this为新生成的对象调用构造函数
//返回新的对象
if(typeof res == "function" || (typeof res == "object" && res !== null)) {
return res;
}
return instance;
}
//测试代码
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function (){
console.log(`My name is ${this.name}`);
}
const me = myNew(Person,"Lance");
me.sayName();
完成上面的代码,我们再来回答new发生了什么,是不是就so easy了!
①生成一个新的对象
②设置新对象的原型为构造函数的prototype
③指定构造函数的上下文(this)为新对象并执行构造函数
④返回新对象
call,bind,apply
这三个我想一起讲,因为它们常在面试中被一起问到。
问:怎么修改函数的上下文(this)
答:使用call,apply或者bind都可以修改函数的上下文。
再问:它们三者有什么区别呢?
答:emmm…
接下来我们就自己实现一下这三个方法,看一看他们到底有什么区别!
Function.prototype.myCall = function (context = globalThis) {
let args = [...arguments].slice(1); //保存参数
//利用Symbol不会重复的特效生成一个key,防止覆盖传入对象的属性
let key = Symbol("key");
context[key] = this; //将函数作为要指定this的一个属性
let res = context[key](...args);//执行上一步保存的方法
delete context[key];//删除该属性
return res;//返回函数执行结果
}
Function.prototype.myApply = function (context = globalThis) {
let args = [...arguments][1];//保存参数
//利用Symbol不会重复的特效生成一个key,防止覆盖传入对象的属性
let key = Symbol("key");
context[key] = this; //将函数作为要指定this的一个属性
let res;
if(args){ //如果没有传参数,可能会取到undefined
res = context[key](...args);
}
else {
res = context[key]();
}
delete context[key]; //删除该属性
return res; //返回函数执行结果
}
Function.prototype.myBind = function (){
var _this = this; //保存函数
var context = [].shift.call(arguments); //获取要绑定的上下文
var args = [].slice.call(arguments);//获取参数
const newFunc = function (){
//兼容处理new的方式调用
if(this instanceof newFunc){ //如果使用new的方式调用,需要继承原函数
newFunc.prototype = Object.create(_this.prototype);
return _this.apply(this,[].concat.call(args,[].slice.call(arguments)));
}
return _this.apply(context,[].concat.call(args,[].slice.call(arguments)));
}
return newFunc;
}
下面我们来总结一下他们的异同
①三个方法的第一个参数均为要指定的上下文对象,call和bind后续可以接收一系列参数,而bind第二个参数为一个数组。
②call和apply是指定上下文执行函数,返回的是一个执行结果,而bind返回的是一个待执行函数。
节流
什么是函数节流?
限制一个函数在一定时间内只能执行一次。
为什么需要节流?
开发过程中,有一些事件或者函数,会被频繁地触发(onresize,scroll,mousemove ,mousehover),不做限制的话,会造成不必要的性能浪费。
节流的整体实现思路为添加一个锁,然后延迟执行函数,一次执行完以后才会解锁重新起定时器执行下一次。
代码实现:
function throttle(func,ms=1000) {
let canRun = true;
return function (...args){
if(!canRun){
return;
}
canRun = false
setTimeout(()=>{
func.apply(this,args);
canRun = true;
})
}
}
防抖
什么是函数防抖?
触发事件后,在延迟时间内函数只能执行一次,如果触发事件后在延迟时间内又触发了事件,则会重新计算函数延执行时间。等延迟时间计时完毕,则执行目标代码。
为什么需要防抖?
一个查询输入框,每次输入都会进行查询,如果输入速度很快,前一次输入查询还没结果有需要进行新的查询,而实际需要的是最后一次输入结束后的结果,前面的查询就造成浪费。
代码实现:
function debounce(func,ms=1000) {
let timer;
return function (...args) {
if(timer){
clearTimeout(timer)
}
timer = setTimeout(()=>{
func.apply(this,args);
},ms)
}
}
instanceof
instance用于引用类型的类型判断,原理是判断左边是否在右边的原型链上。
function myInstanceof(left,right) {
if(left === null){return false;}
if(left.__proto__ === null) {
return false;
}
if(left.__proto__ === right.prototype) {
return true;
}
return myInstanceof(left.__proto__,right);
}
deepCopy
function deepCopy(obj,cache = new WeakMap()) {
if(! obj instanceof Object) {
return obj;
}
if(cache.get(obj)){
return cache.get(obj);
}
if(obj instanceof Function) { //兼容function
return function () {
return obj.call(this,...arguments);
}
}
if(obj instanceof Date) { //兼容date
return new Date(obj);
}
if(obj instanceof RegExp) { //兼容正则
return new RegExp(obj);
}
//创建一个空对象,使用原型上的构造函数直接new,会自动创建空数组或者对象
const res = new obj.constructor;
cache.set(obj,res); //处理对象循环引用的情况
Object.keys(obj).forEach((key)=>{
if(obj[key] instanceof Object) {
res[key] = deepCopy(obj[key],cache);
}else {
res[key] = obj[key];
}
})
return res;
}
// 测试
const source = {
name: 'Lance',
meta: {
age: 12,
birth: new Date('1995-09-18'),
ary: [1, 2, { a: 1 }],
say() {
console.log('Hello');
}
}
}
source.source = source
const newObj = deepCopy(source)
console.log(newObj.meta.ary[2] === source.meta.ary[2]); // false
console.log(newObj.meta.birth === source.meta.birth); // false
柯理化
什么叫函数柯理化?
只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
function curry(func) {
let argArr = [];
return function curried(...args){
argArr = argArr.concat(args)
if(argArr.length == func.length){ //如果参数等于func的形参数量,则执行函数
return func.apply(this,argArr);
}
return curried;
}
}
function sum(a,b,c) {
return a+b+c;
}
const currySum = curry(sum);
console.log(currySum(1)(2)(3));
基于柯理化还会有一道很经典的算法题。
实现一个add方法,具有以下效果
console.log(add(1)); // 1
console.log(add(1)(2)); // 3
console.log(add(1)(2)(3)); // 6
console.log(add(1)(2, 3)); // 6
console.log(add(1, 2)(3)); // 6
console.log(add(1, 2, 3)); // 6
function add() {
var arg = [...arguments];
var func = function(){
arg = arg.concat([...arguments]);//闭包保存参数
return func;
}
func.toString = function () { //重写toString方法 实现打印。
return arg.reduce(function (pre,cur) {
return pre+cur;
},0)
}
return func;
}
console.log(add(1)); // 1
console.log(add(1)(2)); // 3
console.log(add(1)(2)(3)); // 6
console.log(add(1)(2, 3)); // 6
console.log(add(1, 2)(3)); // 6
console.log(add(1, 2, 3)); // 6
事件总线:发布订阅模式
class EventEmitter {
constructor() {
this.cache = {};//管理所有订阅者
}
//订阅方法
on(name,fn) {
if(this.cache[name]) {
this.cache[name].push(fn);
}else {
this.cache[name] = [fn];
}
}
//取消订阅方法
off(name,fn){
const tasks = this.cache[name];
if(tasks) {
const index = tasks.findIndex(item => {
return item===fn || item.callback === fn;
})
if(index>=0) {
tasks.splice(index,1);
}
}
}
//发布
emit(name, once = false){
if(this.cache[name]){
//拷贝一份任务列表,不然任务里注册相同name的任务会造成死循环
const tasks = this.cache[name].slice();
for(let fn of tasks) {
fn();
}
if(once) {//如果是一次性任务
delete this.cache[name];
}
}
}
}
// 测试
const eventBus = new EventEmitter()
const task1 = () => { console.log('task1'); }
const task2 = () => { console.log('task2'); }
eventBus.on('task', task1)
eventBus.on('task', task2)
setTimeout(() => {
eventBus.emit('task')
}, 1000)
es5实现继承
方式有很多,这里就不一一实现了,可以参考我的另一篇文章。
深入了解原型链,es5应该如何实现继承
异步任务并发数限制
/**
* 关键点
* 1. new promise 一经创建,立即执行
* 2. 使用 Promise.resolve().then 可以把任务加到微任务队列,防止立即执行迭代方法
* 3. 微任务处理过程中,产生的新的微任务,会在同一事件循环内,追加到微任务队列里
* 4. 使用 race 在某个任务完成时,继续添加任务,保持任务按照最大并发数进行执行
* 5. 任务完成后,需要从 doingTasks 中移出
*/
function limit(count, array, iterateFunc) {
const tasks = [];//任务队列
const doingTasks = [];//执行队列
let i = 0
const enqueue = () => {
if (i === array.length) { //如果长度满了,执行全部任务
return Promise.resolve()
}
const task = Promise.resolve().then(() => iterateFunc(array[i++]));//创建一个待执行任务
tasks.push(task);
const doing = task.then(() => doingTasks.splice(doingTasks.indexOf(doing), 1));//执行异步任务的任务
doingTasks.push(doing)
const res = doingTasks.length >= count ? Promise.race(doingTasks) : Promise.resolve()
return res.then(enqueue)
};
return enqueue().then(() => Promise.all(tasks))
}
// test
const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i));//异步任务
limit(2, [1000, 1000, 1000, 1000], timeout).then((res) => {
console.log(res);
})
异步串行||异步并行
function asyncAdd(a, b, callback) {
setTimeout(function () {
callback(null, a + b);
}, 500);
}
// 解决方案
// 1. promisify
const promiseAdd = (a, b) => new Promise((resolve, reject) => {
asyncAdd(a, b, (err, res) => {
if (err) {
reject(err)
} else {
resolve(res)
}
})
})
// 2. 串行处理
async function serialSum(...args) {
return args.reduce((task, now) => task.then(res => promiseAdd(res, now)), Promise.resolve(0))
}
// 3. 并行处理
async function parallelSum(...args) {
if (args.length === 1) return args[0]
const tasks = []
for (let i = 0; i < args.length; i += 2) {
tasks.push(promiseAdd(args[i], args[i + 1] || 0))
}
const results = await Promise.all(tasks)
return parallelSum(...results)
}
// 测试
(async () => {
console.log('Running...');
const res1 = await serialSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
console.log(res1)
const res2 = await parallelSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
console.log(res2)
console.log('Done');
})()
promise
class MyPromise {
constructor(func) {
this.status="pending"; //状态,初始未pending
this.value = null; //用于管理传递的参数
this.resolvedTasks = []; //then链注册的resolved任务
this.rejectedTasks = []; //then链注册的rejected任务
this._resolve = this._resolve.bind(this); //保证调用的时候上下文是对应的promise实例
this._reject = this._reject.bind(this); //保证调用的时候上下文是对应的promise实例
try{
func(this._resolve,this._reject);
}catch (err) {
this._reject(err);
}
}
_resolve(value){ //resolve方法
setTimeout(()=>{
this.status = "fulfilled"; //修改状态为fulfilled
this.value = value; //保存传递的参数
this.resolvedTasks.forEach((task)=>{ //执行then注册的onFulfilled任务
task(value);
})
})
}
_reject(reason){
setTimeout(()=>{
this.status = "rejected"; //修改状态为fulfilled
this.value = reason; //保存传递的参数
this.rejectedTasks.forEach((task)=>{ //执行then注册的onRejected任务
task(reason);
})
})
}
then(onFulfilled,onRejected){
return new MyPromise((resolve,reject)=>{
this.resolvedTasks.push((value)=>{
try{
const res = onFulfilled(value);//执行then注册的onFulfilled任务
if(res instanceof MyPromise){ //如果返回的是一个promise
//用then注册任务,传入本该执行resolve和reject 链回原来的路径
res.then(resolve,reject);
}else {
resolve(value);//执行下一个promise的resolve
}
}
catch (err) {
reject(err);
}
});
this.rejectedTasks.push((value)=>{
try{
const res = onRejected(value);//执行then注册的onRejected任务
if(res instanceof MyPromise){ //如果返回的是一个promise
//用then注册任务,传入本该执行resolve和reject 链回原来的路径
res.then(resolve,reject);
}else {
resolve(value);//执行下一个promise的resolve
}
}
catch(err){
reject(err);
}
})
})
}
catch(onRejected) {
return this.then(null, onRejected);
}
}
// 测试
new MyPromise((resolve) => {
setTimeout(() => {
resolve(1);
}, 500);
}).then((res) => {
return new MyPromise((resolve) => {
setTimeout(() => {
resolve(2);
}, 500);
});
}).then((res) => {
console.log(res);
}).then(res=>{
console.log(res);
}).catch(res=>console.log(res))