写点什么

做了一份前端面试复习计划,保熟~

作者:loveX001
  • 2022-12-07
    浙江
  • 本文字数:15176 字

    阅读完需:约 50 分钟

图片懒加载

实现getBoundClientRect 的实现方式,监听 scroll 事件(建议给监听事件添加节流),图片加载完会从 img 标签组成的 DOM 列表中删除,最后所有的图片加载完毕后需要解绑监听事件。


// scr 加载默认图片,data-src 保存实施懒加载后的图片// <img src="./default.jpg" data-src="https://xxx.jpg" alt="" />let imgs = [...document.querySelectorAll("img")];const len = imgs.length;
let lazyLoad = function() { let count = 0; let deleteImgs = []; // 获取当前可视区的高度 let viewHeight = document.documentElement.clientHeight; // 获取当前滚动条的位置(距离顶部的距离,等价于document.documentElement.scrollTop) let scrollTop = window.pageYOffset; imgs.forEach((img) => { // 获取元素的大小,及其相对于视口的位置,如 bottom 为元素底部到网页顶部的距离 let bound = img.getBoundingClientRect(); // 当前图片距离网页顶部的距离 // let imgOffsetTop = img.offsetTop;
// 判断图片是否在可视区内,如果在就加载(两种判断方式) // if(imgOffsetTop < scrollTop + viewHeight) if (bound.top < viewHeight) { img.src = img.dataset.src; // 替换待加载的图片 src count++; deleteImgs.push(img); // 最后所有的图片加载完毕后需要解绑监听事件 if(count === len) { document.removeEventListener("scroll", imgThrottle); } } }); // 图片加载完会从 `img` 标签组成的 DOM 列表中删除 imgs = imgs.filter((img) => !deleteImgs.includes(img));}
window.onload = function () { lazyLoad();};// 使用 防抖/节流 优化一下滚动事件let imgThrottle = debounce(lazyLoad, 1000);// 监听 `scroll` 事件window.addEventListener("scroll", imgThrottle);
复制代码

li 与 li 之间有看不见的空白间隔是什么原因引起的?如何解决?

浏览器会把 inline 内联元素间的空白字符(空格、换行、Tab 等)渲染成一个空格。为了美观,通常是一个<li>放在一行,这导致<li>换行后产生换行字符,它变成一个空格,占用了一个字符的宽度。


解决办法:


(1)为<li>设置 float:left。不足:有些容器是不能设置浮动,如左右切换的焦点图等。


(2)将所有<li>写在同一行。不足:代码不美观。


(3)将<ul>内的字符尺寸直接设为 0,即 font-size:0。不足:<ul>中的其他字符尺寸也被设为 0,需要额外重新设定其他字符尺寸,且在 Safari 浏览器依然会出现空白间隔。


(4)消除<ul>的字符间隔 letter-spacing:-8px,不足:这也设置了<li>内的字符间隔,因此需要将<li>内的字符间隔设为默认 letter-spacing:normal。

代码输出结果

Promise.resolve().then(() => {    console.log('1');    throw 'Error';}).then(() => {    console.log('2');}).catch(() => {    console.log('3');    throw 'Error';}).then(() => {    console.log('4');}).catch(() => {    console.log('5');}).then(() => {    console.log('6');});
复制代码


执行结果如下:


1 3 5 6
复制代码


在这道题目中,我们需要知道,无论是 thne 还是 catch 中,只要 throw 抛出了错误,就会被 catch 捕获,如果没有 throw 出错误,就被继续执行后面的 then。

代码输出结果

function Foo(){    Foo.a = function(){        console.log(1);    }    this.a = function(){        console.log(2)    }}
Foo.prototype.a = function(){ console.log(3);}
Foo.a = function(){ console.log(4);}
Foo.a();let obj = new Foo();obj.a();Foo.a();
复制代码


输出结果:4 2 1


解析:


  1. Foo.a() 这个是调用 Foo 函数的静态方法 a,虽然 Foo 中有优先级更高的属性方法 a,但 Foo 此时没有被调用,所以此时输出 Foo 的静态方法 a 的结果:4

  2. let obj = new Foo(); 使用了 new 方法调用了函数,返回了函数实例对象,此时 Foo 函数内部的属性方法初始化,原型链建立。

  3. obj.a() ; 调用 obj 实例上的方法 a,该实例上目前有两个 a 方法:一个是内部属性方法,另一个是原型上的方法。当这两者都存在时,首先查找 ownProperty ,如果没有才去原型链上找,所以调用实例上的 a 输出:2

  4. Foo.a() ; 根据第 2 步可知 Foo 函数内部的属性方法已初始化,覆盖了同名的静态方法,所以输出:1

数组去重

使用 indexOf/includes 实现

function unique(arr) {    var res = [];    for(var i = 0; i < arr.length; i++) {        if(res.indexOf(arr[i]) === -1) res.push(arr[i]);        // if(!res.includes(arr[i])) res.push(arr[i]);    }    return res;}
复制代码

使用 filter(forEach) + indexOf/includes 实现

// filterfunction unique(arr) {    var res = arr.filter((value, index) => {        // 只存第一个出现的元素        return arr.indexOf(value) === index;    });    return res;}// forEachfunction unique(arr) {    var res = [];    arr.forEach((value) => {        if(!res.includes(value)) res.push(value);    });    return res;}
复制代码

非 API 版本(原生)实现

function unique(arr) {    var res = [];    for(var i = 0; i < arr.length; i++) {        var flag = false;        for(var j = 0; j < res.length; j++) {            if(arr[i] === res[j]) {                flag = true;                break;            }        }        if(flag === false) res.push(arr[i]);    }    return res;}
复制代码

ES6 使用 Set + 扩展运算符(...)/Array.from() 实现

function unique(arr) {    // return [...new Set(arr)];    return Array.from(new Set(arr));}
复制代码

实现有并行限制的 Promise 调度器

题目描述:JS 实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有两个


 addTask(1000,"1"); addTask(500,"2"); addTask(300,"3"); addTask(400,"4"); 的输出顺序是:2 3 1 4
整个的完整执行流程:
一开始1、2两个任务开始执行500ms时,2任务执行完毕,输出2,任务3开始执行800ms时,3任务执行完毕,输出3,任务4开始执行1000ms时,1任务执行完毕,输出1,此时只剩下4任务在执行1200ms时,4任务执行完毕,输出4

复制代码


实现代码如下:


class Scheduler {  constructor(limit) {    this.queue = [];    this.maxCount = limit;    this.runCounts = 0;  }  add(time, order) {    const promiseCreator = () => {      return new Promise((resolve, reject) => {        setTimeout(() => {          console.log(order);          resolve();        }, time);      });    };    this.queue.push(promiseCreator);  }  taskStart() {    for (let i = 0; i < this.maxCount; i++) {      this.request();    }  }  request() {    if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {      return;    }    this.runCounts++;    this.queue      .shift()()      .then(() => {        this.runCounts--;        this.request();      });  }}const scheduler = new Scheduler(2);const addTask = (time, order) => {  scheduler.add(time, order);};addTask(1000, "1");addTask(500, "2");addTask(300, "3");addTask(400, "4");scheduler.taskStart();
复制代码


参考 前端进阶面试题详细解答

代码输出结果

var obj = {   name : 'cuggz',   fun : function(){     console.log(this.name);   } } obj.fun()     // cuggznew obj.fun() // undefined
复制代码


使用 new 构造函数时,其 this 指向的是全局环境 window。

冒泡排序--时间复杂度 n^2

题目描述:实现一个冒泡排序


实现代码如下:


function bubbleSort(arr) {  // 缓存数组长度  const len = arr.length;  // 外层循环用于控制从头到尾的比较+交换到底有多少轮  for (let i = 0; i < len; i++) {    // 内层循环用于完成每一轮遍历过程中的重复比较+交换    for (let j = 0; j < len - 1; j++) {      // 若相邻元素前面的数比后面的大      if (arr[j] > arr[j + 1]) {        // 交换两者        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];      }    }  }  // 返回数组  return arr;}// console.log(bubbleSort([3, 6, 2, 4, 1]));
复制代码

介绍下 promise 的特性、优缺点,内部是如何实现的,动手实现 Promise

1)Promise 基本特性


  • 1、Promise 有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)

  • 2、Promise 对象接受一个回调函数作为参数, 该回调函数接受两个参数,分别是成功时的回调 resolve 和失败时的回调 reject;另外 resolve 的参数除了正常值以外, 还可能是一个 Promise 对象的实例;reject 的参数通常是一个 Error 对象的实例。

  • 3、then 方法返回一个新的 Promise 实例,并接收两个参数 onResolved(fulfilled 状态的回调);onRejected(rejected 状态的回调,该参数可选)

  • 4、catch 方法返回一个新的 Promise 实例

  • 5、finally 方法不管 Promise 状态如何都会执行,该方法的回调函数不接受任何参数

  • 6、Promise.all()方法将多个多个 Promise 实例,包装成一个新的 Promise 实例,该方法接受一个由 Promise 对象组成的数组作为参数(Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例),注意参数中只要有一个实例触发 catch 方法,都会触发 Promise.all()方法返回的新的实例的 catch 方法,如果参数中的某个实例本身调用了 catch 方法,将不会触发 Promise.all()方法返回的新实例的 catch 方法

  • 7、Promise.race()方法的参数与 Promise.all 方法一样,参数中的实例只要有一个率先改变状态就会将该实例的状态传给 Promise.race()方法,并将返回值作为 Promise.race()方法产生的 Promise 实例的返回值

  • 8、Promise.resolve()将现有对象转为 Promise 对象,如果该方法的参数为一个 Promise 对象,Promise.resolve()将不做任何处理;如果参数 thenable 对象(即具有 then 方法),Promise.resolve()将该对象转为 Promise 对象并立即执行 then 方法;如果参数是一个原始值,或者是一个不具有 then 方法的对象,则 Promise.resolve 方法返回一个新的 Promise 对象,状态为 fulfilled,其参数将会作为 then 方法中 onResolved 回调函数的参数,如果 Promise.resolve 方法不带参数,会直接返回一个 fulfilled 状态的 Promise 对象。需要注意的是,立即 resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。

  • 9、Promise.reject()同样返回一个新的 Promise 对象,状态为 rejected,无论传入任何参数都将作为 reject()的参数


2)Promise 优点


  • ①统一异步 API

  • Promise 的一个重要优点是它将逐渐被用作浏览器的异步 API ,统一现在各种各样的 API ,以及不兼容的模式和手法。

  • ②Promise 与事件对比

  • 和事件相比较, Promise 更适合处理一次性的结果。在结果计算出来之前或之后注册回调函数都是可以的,都可以拿到正确的值。 Promise 的这个优点很自然。但是,不能使用 Promise 处理多次触发的事件。链式处理是 Promise 的又一优点,但是事件却不能这样链式处理。

  • ③Promise 与回调对比

  • 解决了回调地狱的问题,将异步操作以同步操作的流程表达出来。

  • ④Promise 带来的额外好处是包含了更好的错误处理方式(包含了异常处理),并且写起来很轻松(因为可以重用一些同步的工具,比如 Array.prototype.map() )。


3)Promise 缺点


  • 1、无法取消 Promise,一旦新建它就会立即执行,无法中途取消。

  • 2、如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。

  • 3、当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

  • 4、Promise 真正执行回调的时候,定义 Promise 那部分实际上已经走完了,所以 Promise 的报错堆栈上下文不太友好。


4)简单代码实现 最简单的 Promise 实现有 7 个主要属性, state(状态), value(成功返回值), reason(错误信息), resolve 方法, reject 方法, then 方法


class Promise{  constructor(executor) {    this.state = 'pending';    this.value = undefined;    this.reason = undefined;    let resolve = value => {      if (this.state === 'pending') {        this.state = 'fulfilled';        this.value = value;      }    };    let reject = reason => {      if (this.state === 'pending') {        this.state = 'rejected';        this.reason = reason;      }    };    try {      // 立即执行函数      executor(resolve, reject);    } catch (err) {      reject(err);    }  }  then(onFulfilled, onRejected) {    if (this.state === 'fulfilled') {      let x = onFulfilled(this.value);    };    if (this.state === 'rejected') {      let x = onRejected(this.reason);    };  }}
复制代码


5)面试够用版


function myPromise(constructor){ let self=this;  self.status="pending" //定义状态改变前的初始状态   self.value=undefined;//定义状态为resolved的时候的状态   self.reason=undefined;//定义状态为rejected的时候的状态   function resolve(value){    //两个==="pending",保证了了状态的改变是不不可逆的     if(self.status==="pending"){      self.value=value;      self.status="resolved";     }  }  function reject(reason){     //两个==="pending",保证了了状态的改变是不不可逆的     if(self.status==="pending"){        self.reason=reason;        self.status="rejected";       }  }  //捕获构造异常   try{      constructor(resolve,reject);  }catch(e){    reject(e);    } }myPromise.prototype.then=function(onFullfilled,onRejected){   let self=this;  switch(self.status){    case "resolved": onFullfilled(self.value); break;    case "rejected": onRejected(self.reason); break;    default:   }}
// 测试var p=new myPromise(function(resolve,reject){resolve(1)}); p.then(function(x){console.log(x)})//输出1
复制代码


6)大厂专供版


const PENDING = "pending"; const FULFILLED = "fulfilled"; const REJECTED = "rejected";const resolvePromise = (promise, x, resolve, reject) => {  if (x === promise) {    // If promise and x refer to the same object, reject promise with a TypeError as the reason.    reject(new TypeError('循环引用'))  }  // if x is an object or function,  if (x !== null && typeof x === 'object' || typeof x === 'function') {    // If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.    let called    try { // If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.      let then = x.then // Let then be x.then      // If then is a function, call it with x as this      if (typeof then === 'function') {        // If/when resolvePromise is called with a value y, run [[Resolve]](promise, y)        // If/when rejectPromise is called with a reason r, reject promise with r.        then.call(x, y => {          if (called) return          called = true          resolvePromise(promise, y, resolve, reject)        }, r => {          if (called) return          called = true          reject(r)        })      } else {        // If then is not a function, fulfill promise with x.        resolve(x)      }    } catch (e) {      if (called) return      called = true      reject(e)    }  } else {    // If x is not an object or function, fulfill promise with x    resolve(x)  }}function Promise(excutor) {  let that = this; // 缓存当前promise实例例对象  that.status = PENDING; // 初始状态  that.value = undefined; // fulfilled状态时 返回的信息  that.reason = undefined; // rejected状态时 拒绝的原因   that.onFulfilledCallbacks = []; // 存储fulfilled状态对应的onFulfilled函数  that.onRejectedCallbacks = []; // 存储rejected状态对应的onRejected函数  function resolve(value) { // value成功态时接收的终值    if(value instanceof Promise) {      return value.then(resolve, reject);    }    // 实践中要确保 onFulfilled 和 onRejected ⽅方法异步执⾏行行,且应该在 then ⽅方法被调⽤用的那⼀一轮事件循环之后的新执⾏行行栈中执⾏行行。    setTimeout(() => {      // 调⽤用resolve 回调对应onFulfilled函数      if (that.status === PENDING) {        // 只能由pending状态 => fulfilled状态 (避免调⽤用多次resolve reject)        that.status = FULFILLED;        that.value = value;        that.onFulfilledCallbacks.forEach(cb => cb(that.value));      }    });  }  function reject(reason) { // reason失败态时接收的拒因    setTimeout(() => {      // 调⽤用reject 回调对应onRejected函数      if (that.status === PENDING) {        // 只能由pending状态 => rejected状态 (避免调⽤用多次resolve reject)        that.status = REJECTED;        that.reason = reason;        that.onRejectedCallbacks.forEach(cb => cb(that.reason));      }    });  }
// 捕获在excutor执⾏行行器器中抛出的异常 // new Promise((resolve, reject) => { // throw new Error('error in excutor') // }) try { excutor(resolve, reject); } catch (e) { reject(e); }}Promise.prototype.then = function(onFulfilled, onRejected) { const that = this; let newPromise; // 处理理参数默认值 保证参数后续能够继续执⾏行行 onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value; onRejected = typeof onRejected === "function" ? onRejected : reason => { throw reason; }; if (that.status === FULFILLED) { // 成功态 return newPromise = new Promise((resolve, reject) => { setTimeout(() => { try{ let x = onFulfilled(that.value); resolvePromise(newPromise, x, resolve, reject); //新的promise resolve 上⼀一个onFulfilled的返回值 } catch(e) { reject(e); // 捕获前⾯面onFulfilled中抛出的异常then(onFulfilled, onRejected); } }); }) } if (that.status === REJECTED) { // 失败态 return newPromise = new Promise((resolve, reject) => { setTimeout(() => { try { let x = onRejected(that.reason); resolvePromise(newPromise, x, resolve, reject); } catch(e) { reject(e); } }); }); } if (that.status === PENDING) { // 等待态// 当异步调⽤用resolve/rejected时 将onFulfilled/onRejected收集暂存到集合中 return newPromise = new Promise((resolve, reject) => { that.onFulfilledCallbacks.push((value) => { try { let x = onFulfilled(value); resolvePromise(newPromise, x, resolve, reject); } catch(e) { reject(e); } }); that.onRejectedCallbacks.push((reason) => { try { let x = onRejected(reason); resolvePromise(newPromise, x, resolve, reject); } catch(e) { reject(e); } }); }); }};
复制代码

深/浅拷贝

首先判断数据类型是否为对象,如果是对象(数组|对象),则递归(深/浅拷贝),否则直接拷贝。


function isObject(obj) {    return typeof obj === "object" && obj !== null;}
复制代码


这个函数只能判断 obj 是否是对象,无法判断其具体是数组还是对象。

哪些操作会造成内存泄漏?

  • 第一种情况是由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。

  • 第二种情况是设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。

  • 第三种情况是获取一个 DOM 元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回收。

  • 第四种情况是不合理的使用闭包,从而导致某些变量一直被留在内存当中。

偏函数

什么是偏函数?偏函数就是将一个 n 参的函数转换成固定 x 参的函数,剩余参数(n - x)将在下次调用全部传入。举个例子:


function add(a, b, c) {    return a + b + c}let partialAdd = partial(add, 1)partialAdd(2, 3)
复制代码


发现没有,其实偏函数和函数柯里化有点像,所以根据函数柯里化的实现,能够能很快写出偏函数的实现:


function partial(fn, ...args) {    return (...arg) => {        return fn(...args, ...arg)    }}
复制代码


如上这个功能比较简单,现在我们希望偏函数能和柯里化一样能实现占位功能,比如:


function clg(a, b, c) {    console.log(a, b, c)}let partialClg = partial(clg, '_', 2)partialClg(1, 3)  // 依次打印:1, 2, 3
复制代码


_ 占的位其实就是 1 的位置。相当于:partial(clg, 1, 2),然后 partialClg(3)。明白了原理,我们就来写实现:


function partial(fn, ...args) {    return (...arg) => {        args[index] =         return fn(...args, ...arg)    }}
复制代码

Number() 的存储空间是多大?如果后台发送了一个超过最大自己的数字怎么办

Math.pow(2, 53) ,53 为有效数字,会发生截断,等于 JS 能支持的最大数字。

实现数组原型方法

forEach

语法:arr.forEach(callback(currentValue [, index [, array]])[, thisArg])

参数:

callback:为数组中每个元素执行的函数,该函数接受 1-3 个参数currentValue: 数组中正在处理的当前元素index(可选): 数组中正在处理的当前元素的索引array(可选): forEach() 方法正在操作的数组 thisArg(可选): 当执行回调函数 callback 时,用作 this 的值。

返回值:undefined


Array.prototype.forEach1 = function(callback, thisArg) {    if(this == null) {        throw new TypeError('this is null or not defined');    }    if(typeof callback !== "function") {        throw new TypeError(callback + 'is not a function');    }    // 创建一个新的 Object 对象。该对象将会包裹(wrapper)传入的参数 this(当前数组)。    const O = Object(this);    // O.length >>> 0 无符号右移 0 位    // 意义:为了保证转换后的值为正整数。    // 其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型    const len = O.length >>> 0;    let k = 0;    while(k < len) {        if(k in O) {            callback.call(thisArg, O[k], k, O);        }        k++;    }}
复制代码

map

语法: arr.map(callback(currentValue [, index [, array]])[, thisArg])

参数:与 forEach() 方法一样

返回值:一个由原数组每个元素执行回调函数的结果组成的新数组。


Array.prototype.map1 = function(callback, thisArg) {    if(this == null) {        throw new TypeError('this is null or not defined');    }    if(typeof callback !== "function") {        throw new TypeError(callback + 'is not a function');    }    const O = Object(this);     const len = O.length >>> 0;    let newArr = [];  // 返回的新数组    let k = 0;    while(k < len) {        if(k in O) {            newArr[k] = callback.call(thisArg, O[k], k, O);        }        k++;    }    return newArr;}
复制代码

filter

语法:arr.filter(callback(element [, index [, array]])[, thisArg])

参数:

callback: 用来测试数组的每个元素的函数。返回 true 表示该元素通过测试,保留该元素,false 则不保留。它接受以下三个参数:element、index、array,参数的意义与 forEach 一样。

thisArg(可选): 执行 callback 时,用于 this 的值。

返回值:一个新的、由通过测试的元素组成的数组,如果没有任何数组元素通过测试,则返回空数组。


Array.prototype.filter1 = function(callback, thisArg) {    if(this == null) {        throw new TypeError('this is null or not defined');    }    if(typeof callback !== "function") {        throw new TypeError(callback + 'is not a function');    }    const O = Object(this);     const len = O.length >>> 0;    let newArr = [];  // 返回的新数组    let k = 0;    while(k < len) {        if(k in O) {            if(callback.call(thisArg, O[k], k, O)) {                newArr.push(O[k]);            }        }        k++;    }    return newArr;}
复制代码

some

语法:arr.some(callback(element [, index [, array]])[, thisArg])

参数:

callback: 用来测试数组的每个元素的函数。接受以下三个参数:element、index、array,参数的意义与 forEach 一样。

thisArg(可选): 执行 callback 时,用于 this 的值。返回值:数组中有至少一个元素通过回调函数的测试就会返回 true;所有元素都没有通过回调函数的测试返回值才会为 false


Array.prototype.some1 = function(callback, thisArg) {    if(this == null) {        throw new TypeError('this is null or not defined');    }    if(typeof callback !== "function") {        throw new TypeError(callback + 'is not a function');    }    const O = Object(this);     const len = O.length >>> 0;    let k = 0;    while(k < len) {        if(k in O) {            if(callback.call(thisArg, O[k], k, O)) {                return true            }        }        k++;    }    return false;}
复制代码

reduce

语法:arr.reduce(callback(preVal, curVal[, curIndex [, array]])[, initialValue])

参数:

callback: 一个 “reducer” 函数,包含四个参数:

preVal:上一次调用 callback 时的返回值。在第一次调用时,若指定了初始值 initialValue,其值则为 initialValue,否则为数组索引为 0 的元素 array[0]

curVal:数组中正在处理的元素。在第一次调用时,若指定了初始值 initialValue,其值则为数组索引为 0 的元素 array[0],否则为 array[1]

curIndex(可选):数组中正在处理的元素的索引。若指定了初始值 initialValue,则起始索引号为 0,否则从索引 1 起始。

array(可选):用于遍历的数组。initialValue(可选): 作为第一次调用 callback 函数时参数 preVal 的值。若指定了初始值 initialValue,则 curVal 则将使用数组第一个元素;否则 preVal 将使用数组第一个元素,而 curVal 将使用数组第二个元素。返回值:使用 “reducer” 回调函数遍历整个数组后的结果。


Array.prototype.reduce1 = function(callback, initialValue) {    if(this == null) {        throw new TypeError('this is null or not defined');    }    if(typeof callback !== "function") {        throw new TypeError(callback + 'is not a function');    }    const O = Object(this);    const len = O.length >>> 0;    let k = 0;    let accumulator = initialValue;    // 如果第二个参数为undefined的情况下,则数组的第一个有效值(非empty)作为累加器的初始值    if(accumulator === undefined) {        while(k < len && !(k in O)) {            k++;        }        // 如果超出数组界限还没有找到累加器的初始值,则TypeError        if(k >= len) {            throw new TypeError('Reduce of empty array with no initial value');        }        accumulator = O[k++];    }    while(k < len) {        if(k in O) {            accumulator = callback(accumulator, O[k], k, O);        }        k++;    }    return accumulator;}
复制代码

节流

**节流(throttle)**:触发高频事件,且 N 秒内只执行一次。这就好比公交车,10 分钟一趟,10 分钟内有多少人在公交站等我不管,10 分钟一到我就要发车走人!类似 qq 飞车的复位按钮。


核心思想:使用时间戳或标志来实现,立即执行一次,然后每 N 秒执行一次。如果 N 秒内触发则直接返回。


应用:节流常应用于鼠标不断点击触发、监听滚动事件。


实现:


// 版本一:标志实现function throttle(fn, wait){    let flag = true;  // 设置一个标志    return function(...args){        if(!flag) return;        flag = false;        setTimeout(() => {            fn.call(this, ...args);            flag = true;        }, wait);    }}
// 版本二:时间戳实现function throttle(fn, wait) { let pre = 0; return function(...args) { let now = new Date(); if(now - pre < wait) return; pre = now; fn.call(this, ...args); }}
复制代码

分片思想解决大数据量渲染问题

题目描述:渲染百万条结构简单的大数据时 怎么使用分片思想优化渲染


实现代码如下:


let ul = document.getElementById("container");// 插入十万条数据let total = 100000;// 一次插入 20 条let once = 20;//总页数let page = total / once;//每条记录的索引let index = 0;//循环加载数据function loop(curTotal, curIndex) {  if (curTotal <= 0) {    return false;  }  //每页多少条  let pageCount = Math.min(curTotal, once);  window.requestAnimationFrame(function () {    for (let i = 0; i < pageCount; i++) {      let li = document.createElement("li");      li.innerText = curIndex + i + " : " + ~~(Math.random() * total);      ul.appendChild(li);    }    loop(curTotal - pageCount, curIndex + pageCount);  });}loop(total, index);
复制代码


扩展思考:对于大数据量的简单 dom 结构渲染可以用分片思想解决 如果是复杂的 dom 结构渲染如何处理?


这时候就需要使用虚拟列表了 大家自行百度哈 虚拟列表和虚拟表格在日常项目使用还是很频繁的

call apply bind

题目描述:手写 call apply bind 实现


实现代码如下:


Function.prototype.myCall = function (context, ...args) {  if (!context || context === null) {    context = window;  }  // 创造唯一的key值  作为我们构造的context内部方法名  let fn = Symbol();  context[fn] = this; //this指向调用call的函数  // 执行函数并返回结果 相当于把自身作为传入的context的方法进行调用了  return context[fn](...args);};
// apply原理一致 只是第二个参数是传入的数组Function.prototype.myApply = function (context, args) { if (!context || context === null) { context = window; } // 创造唯一的key值 作为我们构造的context内部方法名 let fn = Symbol(); context[fn] = this; // 执行函数并返回结果 return context[fn](...args);};
//bind实现要复杂一点 因为他考虑的情况比较多 还要涉及到参数合并(类似函数柯里化)
Function.prototype.myBind = function (context, ...args) { if (!context || context === null) { context = window; } // 创造唯一的key值 作为我们构造的context内部方法名 let fn = Symbol(); context[fn] = this; let _this = this; // bind情况要复杂一点 const result = function (...innerArgs) { // 第一种情况 :若是将 bind 绑定之后的函数当作构造函数,通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象 // 此时由于new操作符作用 this指向result实例对象 而result又继承自传入的_this 根据原型链知识可得出以下结论 // this.__proto__ === result.prototype //this instanceof result =>true // this.__proto__.__proto__ === result.prototype.__proto__ === _this.prototype; //this instanceof _this =>true if (this instanceof _this === true) { // 此时this指向指向result的实例 这时候不需要改变this指向 this[fn] = _this; this[fn](...[...args, ...innerArgs]); //这里使用es6的方法让bind支持参数合并 } else { // 如果只是作为普通函数调用 那就很简单了 直接改变this指向为传入的context context[fn](...[...args, ...innerArgs]); } }; // 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法 // 实现继承的方式: 使用Object.create result.prototype = Object.create(this.prototype); return result;};
//用法如下
// function Person(name, age) {// console.log(name); //'我是参数传进来的name'// console.log(age); //'我是参数传进来的age'// console.log(this); //构造函数this指向实例对象// }// // 构造函数原型的方法// Person.prototype.say = function() {// console.log(123);// }// let obj = {// objName: '我是obj传进来的name',// objAge: '我是obj传进来的age'// }// // 普通函数// function normalFun(name, age) {// console.log(name); //'我是参数传进来的name'// console.log(age); //'我是参数传进来的age'// console.log(this); //普通函数this指向绑定bind的第一个参数 也就是例子中的obj// console.log(this.objName); //'我是obj传进来的name'// console.log(this.objAge); //'我是obj传进来的age'// }
// 先测试作为构造函数调用// let bindFun = Person.myBind(obj, '我是参数传进来的name')// let a = new bindFun('我是参数传进来的age')// a.say() //123
// 再测试作为普通函数调用// let bindFun = normalFun.myBind(obj, '我是参数传进来的name')// bindFun('我是参数传进来的age')
复制代码

事件循环机制 (Event Loop)

事件循环机制从整体上告诉了我们 JavaScript 代码的执行顺序 Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。


先执行 Script 脚本,然后清空微任务队列,然后开始下一轮事件循环,继续先执行宏任务,再清空微任务队列,如此往复。


  • 宏任务:Script/setTimeout/setInterval/setImmediate/ I/O / UI Rendering

  • 微任务:process.nextTick()/Promise


上诉的 setTimeout 和 setInterval 等都是任务源,真正进入任务队列的是他们分发的任务。


优先级


  • setTimeout = setInterval 一个队列

  • setTimeout > setImmediate 

  • process.nextTick > Promise


for (const macroTask of macroTaskQueue) {    handleMacroTask();      for (const microTask of microTaskQueue) {          handleMicroTask(microTask);    }}
复制代码

发布订阅模式(事件总线)

描述:实现一个发布订阅模式,拥有 on, emit, once, off 方法


class EventEmitter {    constructor() {        // 包含所有监听器函数的容器对象        // 内部结构: {msg1: [listener1, listener2], msg2: [listener3]}        this.cache = {};    }    // 实现订阅    on(name, callback) {        if(this.cache[name]) {            this.cache[name].push(callback);        }        else {            this.cache[name] = [callback];        }    }    // 删除订阅    off(name, callback) {        if(this.cache[name]) {            this.cache[name] = this.cache[name].filter(item => item !== callback);        }        if(this.cache[name].length === 0) delete this.cache[name];    }    // 只执行一次订阅事件    once(name, callback) {        callback();        this.off(name, callback);    }    // 触发事件    emit(name, ...data) {        if(this.cache[name]) {            // 创建副本,如果回调函数内继续注册相同事件,会造成死循环            let tasks = this.cache[name].slice();            for(let fn of tasks) {                fn(...data);            }        }    }}
复制代码

函数中的 arguments 是数组吗?类数组转数组的方法了解一下?

是类数组,是属于鸭子类型的范畴,长得像数组,


  • ... 运算符

  • Array.from

  • Array.prototype.slice.apply(arguments)


用户头像

loveX001

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
做了一份前端面试复习计划,保熟~_JavaScript_loveX001_InfoQ写作社区