2020-11-06 JS面试题总结-持续更新

464 阅读20分钟

1、原型与原型链

一句话概括:

JS原型:指的是为其他对象提供共享属性访问的对象,在创建对象时,每个对象都有一个隐式引用指向它的原型对象或者null

原型链:原型也是对象,因此它也有自己的原型,这样就构成一个原型链

2、谈一谈你对this的了解

  • this的指向不是在定义时确定的,而是在执行时确定的;同时this不同的指向在于遵循了一定的规则
  • 首先,在默认情况下,this是指向全局对象的,比如在浏览器中就是windows
  • 其次,如果函数被调用的位置存在上下文对象时,那么函数的this是被隐式绑定的
  • 再次,显示改变this指向,常见的方法就是call、apply、bind
  • 最后,也是优先级最高的绑定new绑定
  • 箭头函数不同于普通函数,它没有属于自己的this,把定义时所在的外层作用域中的this作为自己的this

绑定优先级:new绑定>显示绑定>隐式绑定>默认绑定

3、class中super关键字

super关键字既可以当函数用,也可以当对象使用

第一种情况,super作为函数调用时,代表父类的构造函数,子类的构造函数必须执行一次super函数,注意:super虽然代表了父类的构造函数,但是返回的是子类的实例,相当于A.prototype.constructor.call(this)(A指的是父类),而且作为函数时只能用在子类的构造函数中,用在其他地方就会报错

第二种情况,super作为对象时,在普通方法中,指向父类的原型对象,如A.prototype(A指的是父类);在静态方法中,指向父类

4、Object.is()与比较操作符'==='、'=='的区别

  • 两等号判断,会在比较时进行类型转换
  • 三等号判断(判断严格),比较时不进行隐式类型转换
+0 === -0  // true
NaN === NaN // false
  • Object.is在三等号判断的基础上特别处理了NaN-0+0Object.is应被认为有其特殊的用途,而不能认为它比其他的相等更宽松或更严格
Object.is(NaN, NaN) // true
Object.is(-0, +0) // false

object.is的实现

Object.is = function(x, y){
    if(x===y){
        // 处理+0和-0
        return x!==0 || 1/x === 1/y
    }else {
        // 处理NaN
        return x!==x && y!==y
    }
}

5、ES6模块与CommonJS模块的差异

  • CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用
  • CommonJS模块是运行时加载,ES6模块是编译时输出接口
  • CommonJS模块的require()是同步加载,ES6模块的import命令是异步加载,有一个独立的模块依赖的解析阶段

6、其他类型转换为数字时的规则

  1. undefined类型的值转换为NaN

  2. null类型的转换为0

  3. boolean类型的值,true转换为1,false转换为0

  4. string类型的值转换为如同使用Number()函数转换,如果包含非数字值则转换为NaN,空字符串为0

  5. symbol类型的值不能转换为数字,会报错

  6. 对象(包括数组)会首先被转换为相应的基本类型,如果返回的是非数字的基本类型值,则在遵循以上规则将其强制转换为数字

    注意:将对象转换为基本类型,首先会检查是否有valueOf()方法,如果有并且返回基本类型值,就是用该值进行强制类型转换,没有就使用toString()方法的返回值(如果存在)来进行强制类型转换,如果valueOf()和toString()均不返回基本类型值,会产生TypeError错误

7、JS有几种方法判断变量的类型

  • 使用typeof运算符,当需要判断变量是否是number、string、boolean、function、undefined、symbol、bigInt时,可以用此方法,主要用来判断基本类型
  • 使用instanceof运算符,instanceof与typeof相似,用于识别对象类型,且明确要求对象为某种特定类型,例如A instanceof B 返回布尔值,A是否是B的实例,指的是B.prototype是否在A的原型链上,主要用来判断属于哪一类引用类型
// 使用递归
function instanceOf1(L, R) {
  if (L === null) return false;
  // 判断B.prototype是否在A的原型链上
  let left = Object.getPrototypeOf(L);
  if (left) {
    const right = R.prototype;
    if (left === right) {
      return true;
    }
    return instanceOf1(left, R);
  } else {
    return false;
  }
}

// 使用迭代循环
function instanceOf2(L, R) {
  if (L === null) return false;
  let left = Object.getPrototypeOf(L);
  const right = R.prototype;
  while (true) {
    if (left === null) return false;
    if (left === right) return true;
    left = Object.getPrototypeOf(left);
  }
}
  • 使用constructor属性检测,constructor本来是原型对象上的属性,指向构造函数。但是根据实例对象寻找属性的顺序,若实例对象上没有就去原型链上去寻找,因此,实例对象也是能使用constructor属性的
  • 使用Object.prototype.toString.call(value)方法,返回 [object type] 字符串,其中第二个参数type指的就是值的类型

具体关于JS类型和值判断的请参考我的另一篇文章

8、JS数组去重方法

(1)使用ES6 Set方法,[...new Set(array)]

(2)利用新数组indexOf()方法查找

function indexOf(arr) {
  const newArr = [];
  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    const index = newArr.indexOf(item);
    if (index === -1) {
      newArr.push(item);
    }
  }
  return newArr;
}

(3)for双重循环,通过判断第二层循环,去重的数组中是否有该元素,如果有就退出第二层循环,如果没有j === result.length就相等,然后push元素到去重数组

function doubleLoop(arr) {
  let result = [];
  for (var i = 0; i < arr.length; i++) {
    for (var j = 0; j < result.length; j++) {
      if (arr[i] === result[j]) {
        break;
      }
    }
    if (j === result.length) {
      result.push(arr[i]);
    }
  }
  return result;
}

(4)利用for嵌套for,然后splice去重

function arrSplice(arr) {
  const result = arr;
  for (let i = 0; i < result.length; i++) {
    for (let j = i + 1; j < result.length; j++) {
      if (result[i] === result[j]) {
        result.splice(j, 1);
        j--;
      }
    }
  }
  return result;
}

(5)利用filter方法

function arrFliter(arr) {
  return arr.filter((item, index) => {
    return arr.indexOf(item) === index;
  });
}

(6)利用map数据结构去重

function arrMap(arr) {
  const map = new Map();
  return arr.filter(item => {
    return !map.has(item) && map.set(item, 1);
  });
}

9、call、apply、bind以及new的实现

call:使用一个指定的this值和单独给出的一个或多个参数来调用一个函数

/**
 * call函数的实现
 * 1.将函数设置为对象的属性
 * 2.执行该函数
 * 3.删除该属性
 */

Function.prototype.myCall = function (context) {
  // 1、判断调用对象是否是函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  // 2、判断context是否传入,没有则设置为window,并且取出参数
  context = context || window;
  const args = [...arguments].slice(1);
  // 3、将调用函数设置为传入对象context的属性
  const fn = Symbol();
  context[fn] = this;
  // 4、调用函数
  const result = context[fn](...args);
  // 5、删除函数属性并且返回result
  delete context[fn];
  return result;
};

apply:调用一个具有给定this值的函数,以及一个数组(或类数组对象)的形式提供的函数

注意:call()方法的作用和apply()方法类似,区别就是call()方法接收的是参数列表,而apply()方法接收的是一个参数数组

/**
 * apply函数的实现
 * 和call差不多,不过参数不一样是数组
 */

Function.prototype.myApply = function (context) {
  // 1.判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }

  // 2.判断context是否存在,不存在则传入为window
  context = context || window;

  // 3.将函数设置为context的方法
  const fn = Symbol();
  context[fn] = this;

  // 4.调用方法
  let result;
  const args = arguments[1]; // 类数组或数组
  if (args.length > 0) {
    result = context[fn](...args);
  } else {
    result = context[fn]();
  }

  // 5.删除属性,返回结果
  delete context[fn];

  return result;
};

bind:创建一个新的函数,这个新函数的this被指定为其第一个参数,其余参数作为新函数的参数,供调用

/**
 * bind函数的实现:
 * 返回一个新的函数
 * 参数形式与call一样
 */

Function.prototype.myBind = function (context) {
  // 1.判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }

  // 2.获取参数
  const args = [...arguments].slice(1);

  // 3.返回一个新的函数,考虑new操作符
  const self = this;
  const fNOP = function () {};
  const fBond = function () {
    return self.apply(
      this instanceof fNOP ? this : context,
      args.concat(...arguments)
    );
  };

  fNOP.prototype = this.prototype;
  fBond.prototype = new fNOP();
  return fBond;
};

new:创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例

/**
 * new操作符
 * 1、创建一个空对象,作为将要返回的实例对象
 * 2、将这个空对象的原型指向构造函数的prototype属性
 * 3、将这个空对象赋值给构造函数内部的this关键字
 * 4、开始执行构造函数内部代码
 */

function myNew() {
  const obj = Object.create(null);
  const Con = [].shift.call(arguments);
  Object.setPrototypeOf(obj, Con.prototype);
  let result = Con.apply(obj, arguments);
  return result instanceof Object ? result : obj;
}

10、事件委托是什么

事件委托本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件代理,使用事件代理可以减少内存上的消耗

11、编程题:红绿灯

红灯3秒亮一次,绿灯2秒亮一次,黄灯1秒亮一次,如何让三个灯不断重复的亮灯

function red() {
  console.log("red");
}

function green() {
  console.log("green");
}

function yellow() {
  console.log("yellow");
}

const light = function (timer, cb) {
  return new Promise(resolve => {
    setTimeout(() => {
      cb();
      resolve();
    }, timer);
  });
};

const step = function () {
  Promise.resolve()
    .then(() => {
      return light(3000, red);
    })
    .then(() => {
      return light(2000, green);
    })
    .then(() => {
      return light(1000, yellow);
    })
    .then(() => {
      step();
    });
};

step();

12、编程题:实现Promise.retry

实现Promise.retry,成功后resolve结果,失败后重试,尝试超过一定次数后才真正的reject

普通版本

Promise.retry1 = function (promiseFn, times) {
  return new Promise(async (resolve, reject) => {
    while (times--) {
      try {
        const ret = await promiseFn();
        resolve(ret);
        break;
      } catch (error) {
        if (times === 0) reject(error);
      }
    }
  });
};

变种版本-增加delay延时重试

Promise.retry2 = function (promiseFn, times, delay) {
  return new Promise((resolve, reject) => {
    const retry = function () {
      promiseFn()
        .then(resolve)
        .catch(err => {
          if (times === 0) {
            reject(err);
          } else {
            times--;
            setTimeout(retry, delay);
          }
        });
    };
    retry();
  });
};

13、原型如何实现继承?Class如何实现继承?Class的本质是什么

原型继承

function Parent(value) {
  this.value = value;
}

Parent.prototype.getValue = function () {
  console.log(this.value);
};

function Child(value) {
  Parent.call(this, value);
}

Child.prototype = new Parent();

const child = new Child("child");
child.getValue();  // child
child instanceof Parent; // true

Class继承

class Parent {
  constructor(value) {
    this.value = value;
  }
  getValue() {
    console.log(this.value);
  }
}

class Child extends Parent {
  constructor(value) {
    super(value);
    this.value = value;
  }
}

const child = new Child("child");
child.getValue(); // child
child instanceof Parent;  // true

Class的本质是function,它可以看做是一个语法糖,让对象原型的写法更加清晰、更加面向对象编程的写法

14、promise的一些API实现

Promise.prototype.finally

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value => P.resolve(callback()).then(() => value),
    reason =>
      P.resolve(callback()).then(() => {
        throw reason;
      })
  );
};

Promise.resolve

Promise.resolve = function (value) {
  return new Promise(resolve => resolve(value));
};

Promise.reject

Promise.reject = function (error) {
  return new Promise((resolve, reject) => reject(error));
};

Promise.allpromises中有一个失败reject,全部成功resolve

Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    const result = [];
    let finish = 0;
    for (const [i, p] of promises.entries()) {
      Promise.resolve(p)
        .then(res => {
          result[i] = res;
          finish++;
          if (finish === promises.length) resolve(result);
        })
        .catch(err => reject(err));
    }
  });
};

Promise.racepromises中最先结束状态的成功resolve,失败reject

Promise.race = function (promises) {
  return new Promise((resolve, reject) => {
    for (const promise of promises) {
      Promise.resolve(promise)
        .then(res => resolve(res))
        .catch(err => reject(err));
    }
  });
};

Promise.allSettledpromises中全部结束状态后resolve

Promise.allSettled = function (promises) {
  return new Promise(resolve => {
    const result = [];
    let count = 0;
    for (const [i, p] of promises.entries()) {
      Promise.resolve(p)
        .then(value => (result[i] = { status: "fulfilled", value }))
        .catch(reason => (result[i] = { status: "rejected", reason }))
        .finally(() => {
          count++;
          if (count === promises.length) resolve(result);
        });
    }
  });
};

15、实现一个带并发限制的异步调度器Scheduler

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

class Scheduler {
  constructor() {
    this.taskQueue = [];
    this.counter = 0;
    this.limit = 2;
  }
  add(promiseCreator) {
    return new Promise(resolve => {
      promiseCreator.resolve = resolve;
      if (this.counter < this.limit) {
        this.run(promiseCreator);
      } else {
        this.taskQueue.push(promiseCreator);
      }
    });
  }
  run(promiseCreator) {
    this.counter++;
    promiseCreator().then(() => {
      promiseCreator.resolve();
      this.counter--;
      if (this.taskQueue.length > 0) {
        this.run(this.taskQueue.shift());
      }
    });
  }
}

const timeout = time => new Promise(resolve => setTimeout(resolve, time));
const scheduler = new Scheduler();
const addTask = (time, order) =>
  scheduler.add(() => timeout(time)).then(() => console.log(order));

// test
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");

/**
 * output: 2 3 1 4
 * 一开始,1、2两个任务进队列
 * 500ms时,2完成,输出2
 * 任务3进队列
 * 800ms时,3完成,输出3
 * 任务4进队列
 * 1000ms时,1完成,输出1
 * 1200ms时,4完成,输出4
 */

16、实现对象的 Map 函数类似 Array.prototype.map

// 对象的map, fn形如(key, value)=>void
Object.prototype.map = function (fn) {
  if (typeof fn !== "function") throw new TypeError(fn + " is not function");
  const obj = this;
  const result = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = fn(key, obj[key]);
    }
  }
  return result;
};

17、请分别简述下函数节流与防抖

节流 规定在一个单位时间内,只能触发一次函数。如果在这个单位时间内多次触发函数,只有一次生效,稀释了高频事件的触发频率

应用场景:

  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
  • 缩放场景:监控浏览器resize
  • 动画场景:避免短时间内多次触发动画引起性能问题
function throttle(fn, delay = 500) {
  // 第一次执行了
  let last = 0;
  return (...args) => {
    let now = Date.now();
    if (now - last > delay) {
      fn.apply(this, args);
      last = now;
    }
  };
}

防抖 在事件被触发n秒后在执行回调,如果在这n秒内又被触发,则重新计时

应用场景:

  • 按钮提交场景:防止多次提交表单,只执行最后提交的一次
  • 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似
function debounce(fn, delay = 500) {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

18、var、let和const的区别是什么

  1. var声明的变量会挂载在window上,而let和const不会
  2. var声明变量存在变量提升,let和const不存在变量提升
  3. let和const声明形成块作用域
  4. 同一作用域下let和const不能声明同名变量,而var可以
  5. 暂时性死区
  6. const一旦声明必须赋值,不能再修改,如果是复合型数据,可以修改其属性

19、如何在JS中创建对象

  1. 使用对象字面量
let obj = {
  name: "xiaoxu"
};
console.log(obj.name) // xiaoxu
  1. 使用构造函数
let obj = new Object();
obj.name = "xiaoxu";
console.log(obj.name) // xiaoxu
  1. 使用Object.create方法
let obj = Object.create({
  name: "xiaoxu"
});
console.log(obj.name);  // xiaoxu

20、简单说下webpack的原理

webpack是从指定的入口文件(entry)开始,经过加工处理,递归调用依赖模块,最终按照output设定输出固定内容的bundle;而这个加工处理的过程,用到了loader和plugin两个工具:loader是源代码的处理器,plugin解决的是loader处理不了的问题如压缩代码体积,优化编译速度等

21、简单说下Set、Map、WeakSet和WeakMap的区别

  • Set是一种叫集合的数据结构,允许存储任何类型的唯一值,不论是原始值还是对象引用,不会添加重复的值,内部判断相等方法类似于严格相等运算符,但判断NaN相等,键名和键值一样,可以遍历

  • WeakSet和Set有两个区别,一是其内部成员只能是对象;二是内部成员对象都是弱引用,可以被垃圾回收机制回收,所以不能遍历

  • Map是一种叫字典的数据结构,是以key-value形式存储元素,键名可以是任何类型,判断键名相同是采用同Set的算法,NaN视为相同的键

    MapObject的区别:
    - Object的key只能是数值、字符串或Symbol符号,Map的键无要求
    - Object不能使用迭代方法,如for...of之类,而Map内部实现[Symbol.iterator]属性,可进行迭代操作
    
  • WeakMap和Map也有两个区别,一是键名只能是对象(null除外);二是键名所指的对象是弱引用,不计入垃圾回收机制,所以也不能遍历

22、说下闭包和作用域

闭包当函数可以记住并访问所在的词法作用域时,就产生了闭包;即使函数在当前的词法作用域之外执行

作用域分为全局作用域和局部作用域,局部作用域如函数作用域和ES6中使用let和const的块级作用域

23、TypeScript中interface与type的区别以及说下对泛型的理解

  • type是类型别名,可以定义string、number等所有类型;interface只能定义对象,而且是定义对象的形状shape
  • type和interface都可以使用继承,但是使用方式不同,interface用关键字,而type则使用&符号
  • interface可以定义多个,会合并
  • 泛型指的是在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候在指定类型的一种方式

24、为什么函数是一等公民

因为函数复合一等公民的三个要素:可以当参数使用,可以作为返回值,还可以作为赋值给变量

25、数组与链表的区别

  • 数组是基于索引的数据结构,其中每个元素与索引相关,添加和删除元素比较麻烦,但是获取值简单
  • 链表是依赖引用指针的数据结构,每个元素由值和下一个引用指针指向组成,插入和删除节点简单,获取指定的节点麻烦
  • 数组的大小是固定的,链表可以扩容,大小不固定

26、如何判断数组

  • 使用ES6中数组的方法Array.isArray()
  • 使用instanceof
  • 使用Object.prototype.toString.call()

27、JS中的基本类型和复杂类型是存储在哪里的

基本类型存储在栈中,但是一旦被闭包引用则成为常驻内存,会存储在内存堆中

复杂类型存储在内存堆中

28、Babel的原理是什么

babel的转译过程分为三个阶段如下:

  • 解析Parse:将代码解析生成抽象语法树(AST)
  • 转换Transform:对于AST进行变换一系列的操作,babel接受得到AST并通过babel-traverse对其进行遍历,在此过程中进行添加、更新以及移除等操作
  • 生成Generate:将变换后的AST再转换为JS代码,使用到的模块是babel-generator

29、编程题:实现一个发布订阅设计模式EventEmitter

EventEmitter类有以下几个方法:

  • on():订阅事件
  • emit():触发事件
  • remove():移除事件
  • once():只执行一次事件
class EventEmitter {
  constructor() {
    // 事件对象,存放订阅的名字和事件
    this.events = {};
  }

  // 订阅事件的方法
  on(eventName, callback) {
    if (!this.events[eventName]) {
      // 一个名字可以订阅多个事件函数
      this.events[eventName] = [callback];
    } else {
      // 存在则push保存数组
      this.events[eventName].push(callback);
    }
  }

  // 触发事件的方法
  emit(eventName, ...args) {
    // 遍历执行所有订阅的事件
    this.events[eventName] &&
      this.events[eventName].forEach(fn => fn.apply(this, args));
  }

  // 移除订阅事件
  remove(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName] = this.events[eventName].filter(
        fn => fn !== callback
      );
    }
  }

  // 只执行一次订阅事件,然后移除
  once(eventName, callback) {
    const fn = (...args) => {
      callback.apply(this, args);
      this.remove(eventName, fn);
    };
    this.on(eventName, fn);
  }
}

30、实现一个单例设计模式

要求:一个类只能有一个实例,实现一个全局方法即可

class SingleMode {
  constructor(name) {
    this.name = name;
  }
  getName() {
    console.log(this.name);
  }
}

const ProxyMode = (function () {
  let instance = null;
  return function (name) {
    if (!instance) {
      instance = new SingleMode(name);
    }
    return instance;
  };
})();

31、实现一个深克隆,要求处理循环引用和Symbol符号

JSON.parse(JSON.stringify(obj))有三个缺陷,不能处理函数会把其当做undefined处理、不能处理Symbol、不能处理循环引用

function cloneDeep(source, hash = new WeakMap()) {
  if (!isObject(source)) return source;
  // 循环引用
  if (hash.has(source)) return hash.get(source);

  const target = Array.isArray(source) ? [] : {};
  hash.set(source, target);

  Reflect.ownKeys(source).forEach(key => {
    if (isObject(source[key])) {
      target[key] = cloneDeep(source[key], hash);
    } else {
      target[key] = source[key];
    }
  });

  return target;
}

// 判断是否是对象
function isObject(source) {
  return Object.prototype.toString.call(source) === "[object Object]";
}

32、使用reduce分别实现数组的map和filter方法

  • reduce的参数:reduce(function(accumulator, currentValue, index, array){}, initialValue)
  • map的参数:map(function(currentValue, index, array){}, thisArg)
  • filter的参数:filter(function(element, index, array){}, thisArg)
// 使用reduce实现数组的map
if (!Array.prototype.mapUsingReduce) {
  Array.prototype.mapUsingReduce = function (callback, thisArg) {
    return this.reduce(function (mappedArray, currentValue, index, array) {
      mappedArray[index] = callback.call(thisArg, currentValue, index, array);
      return mappedArray;
    }, []);
  };
}

// 使用reduce实现数组的filter
if (!Array.prototype.filterUsingReduce) {
  Array.prototype.filterUsingReduce = function (callback, thisArg) {
    return this.reduce(function (filteredArray, element, index, array) {
      const result = callback.call(thisArg, element, index, array);
      if (result) filteredArray.push(element);
      return filteredArray;
    }, []);
  };
}

33、实现一个批量请求函数multiRequest(urls, maxNum)

要求如下:

  1. 要求最大并发数maxNum
  2. 每当有一个请求返回,就留下一个空位,可以增加新的请求
  3. 所有请求完成后,结果按照urls里面的顺序依次打出
  4. 请求函数可以用fetch模拟
function multiRequest(urls, maxNum) {
  // 确定给定的maxNum是否小于urls数组的长度
  const min = Math.min(urls.length, maxNum);
  return new Promise(resolve => {
    let index = 0,
      start = 0,
      finish = 0;
    const result = [];
    const run = function () {
      if (finish === urls.length) resolve(result);
      while (start < min && index < urls.length) {
        start++;
        fetch(urls[index]).then(value => {
          start--;
          finish++;
          result[index++] = value;
          run();
        });
      }
    };

    run();
  });
}

34、webpack中loader和plugin的区别

  • loader的主要作用是让webpack识别更多的文件类型
  • plugin的职责是让其可以控制构建流程,从而执行一些特殊的任务,扩展功能和做些输出优化

35、写出以下代码段的promise形式

setTimeout(() => {
  const data1 = 1;
  console.log(data1);
  setTimeout(() => {
    const data2 = data1 + 1;
    console.log(data2);
    setTimeout(() => {
      const data3 = data2 + 1;
      console.log(data3);
    }, 1000);
  }, 1000);
}, 1000);

其promise封装如下:

const p = data => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(data);
    }, 1000);
  });
};

p(1)
  .then(data1 => {
    console.log(data1);
    return p(data1 + 1);
  })
  .then(data2 => {
    console.log(data2);
    return p(data2 + 1);
  })
  .then(data3 => {
    console.log(data3);
  });

36、说下对TypeScript的看法

  • TypeScript是加了类型系统的Javascript,适用于任何规模的项目
  • TypeScript是一门静态类型,弱类型的语言
  • TypeScript完全兼容Javascript,不会修改Javascript运行时的特性
  • TypeScript可以编译为Javascript,然后运行在浏览器、NodeJS等环境中
  • TypeScript有很多编译选项,类型检查的严格程度由自己控制
  • TypeScript可以和Javascript共存,这意味者Javascript项目可以渐进式的迁移到TypeScript
  • TypeScript增强了编辑器的功能,提供代码补全、接口提示、跳转到定义、代码重构等功能

37、虚拟滚动的原理

虚拟列表是按需显示的一种技术,即根据滚动容器的可视区域来显示长列表数据中的某一部分,有三个重要的概念:

  • 滚动容器元素:一般指的是window对象,如果某个元素产生了纵向或横向滚动,则是滚动容器元素
  • 可滚动区域:滚动容器内部内容区域
  • 可视区域:滚动容器元素的视觉可见区域

38、前端性能优化

  • 减少HTTP请求,将多个小文件合并成一个大文件
  • 使用HTTP2多路复用、首部压缩
  • 服务端渲染,服务端返回HTML文件,客户端只需解析HTML
  • 静态资源使用CDN
  • 将CSS放在文件头部,Javascript文件放在底部
  • 利用HTTP缓存
  • 利用webpack压缩文件
  • 图片优化
  • 减少重绘重排
  • 使用事件委托

39、手写数组方法reduce

reduce函数接收两个参数(callbackFn, initialValue),如下:

/**
 * reduce(callbackFn, initialValue)
 * callbackFn(pre,cur,index,array)
 */

Array.prototype.myReduce = function (callbackFn, initialValue) {
  if (typeof callbackFn !== "function") {
    throw TypeError(callbackFn + "is not function");
  }
  if (initialValue === undefined && this.length === 0) {
    throw TypeError("没有提供初始值且数组为空");
  }
  if (initialValue !== undefined && this.length === 0) {
    return initialValue;
  }
  if (initialValue === undefined && this.length === 1) {
    return this[0];
  }

  if (initialValue === undefined) {
    // 没有提供初始值
    let result = this[0],
      temp;
    for (let i = 1; i < this.length; i++) {
      if (this[i] === undefined) continue;
      temp = callbackFn(result, this[i], i, this);
      result = temp;
    }
    return result;
  } else {
    // 提供初始值
    let result = initialValue,
      temp;
    for (let i = 0; i < this.length; i++) {
      if (this[i] === undefined) continue;
      temp = callbackFn(result, this[i], i, this);
      result = temp;
    }
    return result;
  }
};

40、实现一个Task,其log方法直接输出,sleep方法暂停后继续执行

如 const task = new Task();

task.log(1).sleep(2).log(3)

这里肯定不能用async-await了,因为此类函数会有传染性,上述要求方法返回实例本身,所以用for循环来实现sleep函数如下:

class Task {
  log(param) {
    console.log(param);
    return this;
  }
  sleep(delay) {
    const start = new Date().valueOf();
    for (; new Date().valueOf() - start <= delay * 1000; ) {}
    return this;
  }
}

const task = new Task();
task.log(1).sleep(2).log(3);

41、对高阶函数的看法

高阶函数的定义:如果一个函数符合下面两个规范中的任何一个,那么该函数就是高阶函数:

  1. 若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数。
  2. 若A函数,调用的返回值依然是一个函数,那么A也可以称之为高阶函数。

常见的高阶函数有:Promise、setTimeout、数组中的map函数等等

函数的柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式

42、&&||非布尔值的情况

  • 对于非布尔值进行与或运算时,会先将其转换成布尔值,然后再运算,并且返回原值

  • &&运算:

      - 如果第一个值为ture,则必然返回第二个值
      - 如果第一个值为false,则直接返回第一个值
    
  • ||运算:

      - 如果第一个值为true,则直接返回第一个值
      - 如果第一个值为false,则返回第二个值
    

持续更新...