备战前端面试—JS基础篇

前言

  • 拿到一道面试题,第一时间应该思考什么?

    考点

  • 如何对待做不完的题海?

    考点不变,以不变应万变,抛砖引玉,举一反三

  • 如何准备面试?

    建立知识体系,由题转到知识点,由知识点反推可能会出现的题目

JS 基础

变量类型和计算

考点1:变量类型

  1. typeof 能判断哪些类型?

    • 值类型:number,string,boolean,undefined,symbol 。
    • 引用类型( null 是特殊的引用类型,指向空地址)除 function 外无法区分,都为 object。
  2. 值类型和引用类型的区别?

    值类型:

    • 占用空间固定,用栈存储。
    • 变量的值就是值本身。
    • 使用typeof检测数据的类型。

    引用类型:

    • 占用空间不固定,用堆存储,以便反复利用(因为对象的创建成本通常较大)。
    • 变量的值是内存地址,一个内存地址对应一个对象。
    • 使用instanceof检测数据类型。

    image-20210903234044457

  3. 浅拷贝?深拷贝?

    浅拷贝:

    • Array.prototype.cocat

      let arr = [1, 2, 3];
      let newArr = arr.concat();
      newArr[1] = 100;
      console.log(arr);//[ 1, 2, 3 ]
    • Object.assign

      let obj = { name: 'sy', age: 18 };
      const obj2 = Object.assign({}, obj, {name: 'sss'});
      console.log(obj2);//{ name: 'sss', age: 18 }
    • 展开运算符

      let arr = [1, 2, 3];
      let newArr = [...arr];//跟arr.slice()是一样的效果
    • Array.prototype.slice

      let arr = [1, 2, 3];
      let newArr = arr.slice();
      newArr[0] = 100;
      
      console.log(arr);//[1, 2, 3]

    深拷贝:

    • 判断是值类型还是引用类型。

    • 判断是数组还是对象。

    • 递归解析。

    // 简易版本,还有特殊对象、循环引用、函数类型不能处理。
    function deepClone(source) {
        if (typeof source === "object" && source !== null) {
        const target = Array.isArray ? [] : {};
        for (let key in source) {
          if (source.hasOwnProperty(key)) {
            // 保证 key 非原型属性
            target[key] = deepClone(source[key]);
          }
        }
        return target;
        } else {
        return source;
        }
    }
  4. '1'.toString()为什么可以调用?

    其实在这个语句运行的过程中做了这样几件事情:

    var s = new String('1');
    s.toString();
    s = null;

    第一步: 创建 String 类实例。

    第二步: 调用实例方法。

    第三步: 执行完方法立即销毁这个实例。

    整个过程体现了基本包装类型的性质,而基本包装类型恰恰属于基本数据类型,包括Boolean, NumberString

  5. instanceof 能否判断基本数据类型?

    能。比如下面这种方式:

    class PrimitiveNumber {
      static [Symbol.hasInstance](x) {
        return typeof x === 'number'
      }
    }
    console.log(111 instanceof PrimitiveNumber) // true

    其实就是自定义 instanceof 行为的一种方式,这里将原有的 instanceof 方法重定义,换成了 typeof,因此能够判断基本数据类型。

  6. 能不能手动实现一下instanceof的功能?

    function myInstanceof(left, right) {
        //基本数据类型直接返回false
        if(typeof left !== 'object' || left === null) return false;
        //getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
        let proto = Object.getPrototypeOf(left);
        while(true) {
            //查找到尽头,还没找到
            if(proto == null) return false;
            //找到相同的原型对象
            if(proto == right.prototype) return true;
            proto = Object.getPrototypeof(proto);
        }
    }

考点2:类型转换

  1. 何时使用 == or ===

    • 判断 null 和 undefined 用 ==
    • 其他情况一律 ===
  2. 加减运算?

    • 加法运算中只要含字符串,会被解析为字符串拼接,结果为字符串。
    • 减法运算中默认转换为数字类型。
  3. if条件判断?

    • 隐式类型转换。

    • truly变量:两步非运算为true。

    • falsely变量:两步非运算为false。

      // 除此之外,都是 truly 变量
      !!0 === false
      !!NaN === false
      !!'' === false
      !!null === false
      !!undefined === false
      !!false === false
  4. 对象转原始类型是根据什么流程运行的?

    对象转原始类型,会调用内置的 [ToPrimitive] 函数,对于该函数而言,其逻辑如下:

    1. 如果有 Symbol.toPrimitive() 方法,优先调用再返回

    2. 调用 valueOf(),如果转换为原始类型,则返回

    3. 调用 toString(),如果转换为原始类型,则返回

    4. 如果都没有返回原始类型,会报错

      var obj = {
        value: 3,
        valueOf() {
          return 4;
        },
        toString() {
          return "5";
        },
        [Symbol.toPrimitive]() {
          return 6;
        },
      };
      console.log(obj + 1); // 输出7
  5. 如何让if(a == 1 && a == 2)条件成立?

    var a = { 
        value: 0,
        valueOf: function() {
            this.value++;
            return this.value;
        }
    };
    console.log(a == 1 && a == 2);//true

原型和原型链

考点1:继承

  1. es5 和 es6 继承的区别?

    es5:

    • 借助原型链和构造函数实现继承。

    • 子类构造函数的原型对象指向父类构造函数的原型对象。

    • 需要修正子类的构造函数指向。

    • new 时先创建一个空对象将其绑定 this,子类构造函数再以 call、apply 调用父类方法。

      function Parent(name) {
        this.name = name;
      }
      Parent.prototype.getName = function () {
        console.log(this.name);
      };
      function Child(name, age) {
        Parent.call(this, name);
        this.age = age;
      }
      Child.prototype = Object.create(Parent.prototype);
      Child.prototype.constructor = Child;
      Child.prototype.getAge = function () {
        console.log(this.age);
      };
      var child1 = new Child("a", "1");
      child1.getName();
      child1.getAge();

    es6:

    • 通过关键字 class 定义类,extends 实现继承。

    • 类的 constructor 属性相当于 es5 中的构造函数。

    • new 时先创建一个空对象,如果是子类,在子类的构造函数中必须先调用 super 方法,通过父类的构造函数将该对象绑定给 this,否则将会报错。

      class Parent {
        constructor(name) {
          this.name = name;
        }
        getName() {
          console.log(this.name);
        }
      }
      class Child extends Parent {
        constructor(name, age) {
          super(name);
          this.age = age;
        }
        getAge() {
          console.log(this.age);
        }
      }
      var child1 = new Child("a", "1");
      child1.getName();
      child1.getAge();
  2. 能不能描述一下原型链?

    JavaScript对象通过 [[Prototype]] 指向父类对象,直到指向 Object 对象为止,这样就形成了一个原型指向的链条, 即原型链。在对象上如果找不到某个属性或者方法,就会到它的原型对象上找,这样就实现了对象之间方法和属性的共享。

    另外:

    • 对象的 hasOwnProperty() 来检查对象自身中是否含有该属性。
    • 使用 in 检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,也会返回 true。

考点2:显式原型和隐式原型

  1. 显式原型和隐式原型是什么?

    显式原型:

    • 每个函数都有一个 prototype 属性。
    • 定义函数时自动添加,默认值是一个空 Object 对象。

    隐式原型:

    • 每个实例对象都有一个__proto__属性。

    • new 对象时自动添加,默认值为构造函数的 prototype 属性值。

    • 对象的隐式原型的值为其对应构造函数的显式原型的值。

    • Object.prototype__proto__属性指向 null。

  1. 通过 Object.create() 创建出来的对象和 new Object() 字面量创建的对象有什么区别?

    Object.create():

    • 使用现有的对象来提供新创建的对象的__proto__

      var objA = Object.create(Base)
      // 简易版本
      Object.create =  function (Base) {
          var F = function () {};
          F.prototype = Base;
          return new F(); // objA._proto_ = F.prototype = Base};

    new Object():

    • 创建的对象是Object的实例。

      var objB = new Base()
      // 简易版本
       var objB = new Object(); 
       objB.__proto__ = Base.prototype;
       Base.call(objB);

    字面量创建:

    • 创建的对象是Object的实例。

    • 创建更高效一些,少了__proto__指向和 this 转换。

      var objC = {};
  2. 构造函数与原型对象、实例对象的关系?

    • 构造函数的属性 prototype 指向了原型对象。
    • 原型对象保存着实例共享的方法,有一个指针 constructor 指回构造函数。
    • 调用构造函数产生的实例对象,每个对象都有一个__proto__属性,指向这个对象的构造函数的原型对象。

作用域

考点1:作用域、作用域链

  1. 什么是作用域链?

    在 es6 之前,只存在两种作用域:函数作用域和全局作用域。当在执行上下文中访问一个变量时,首先会在当前作用域寻找。如果不存在对应的标识符,则会去父作用域找,一直查找到全局作用域。每个函数作用域中都保存着一个外界作用域的引用,这些作用域形成的链条就是作用域链。

    这里可适当引出词法环境、执行上下文的概念和作用域的本质。

    [[scope]]指向scope chain(ECMA-262v3)或者Lexical Environment(ECMA-262v5)。这对应于闭包的环境部分。
    [[scope]]中可访问的属性列表即是标识符列表,对象本身的引用则对应于环境。

  2. 为什么要引入 let、const?

    ES6之前是不支持块级作用域的,因为当初设计这门语言的时候,并没有想到JavaScript会火起来,所以只是按照最简单的方式来设计。没有了块级作用域,再把作用域内部的变量统一提升无疑是最快速、最简单的设计,不过这也直接导致了函数中的变量无论是在哪里声明的,在编译阶段都会被提取到执行上下文的变量环境中,所以这些变量在整个函数体内部的任何地方都是能被访问的,这也就是JavaScript中的变量提升。

    为了解决这些问题,ES6引入了let和const关键字,从而使JavaScript也能像其他语言一样拥有块级作用域。

考点2:闭包

  1. 什么是闭包?

红宝书(p178)上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数。

MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。

(其中自由变量,指在函数中使用的,但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另外一个函数作用域中的变量。)

更精确地:
闭包包含环境部分,它是个只有两项的list,这两项分别是:
(1)环境。
(2)一个标识符或者标识符列表。
以及控制部分,他是个只有一项的list,唯一的项是函数对象本身。

  1. 闭包产生的原因?

    闭包产生的本质就是,当前环境中存在指向父级作用域的引用,闭包中被引用的部分无法被垃圾回收。闭包的形成其实就是作用域链没有被释放,因此会造成内存泄露。

  2. 闭包有哪些表现形式?

    • 返回一个函数。

      function f1() {
        var a = 2
        function f2() {
          console.log(a);//2
        }
        return f2;
      }
      var x = f1();
      x();
    • 在函数外部调用。

      var f3;
      function f1() {
        var a = 2
        f3 = function() {
          console.log(a);
        }
      }
      f1();
      f3();
    • 作为函数参数传递。

      var a = 1;
      function foo(){
        var a = 2;
        function baz(){
          console.log(a);
        }
        bar(baz);
      }
      function bar(fn){
        // 这就是闭包
        fn();
      }
      // 输出2,而不是1
      foo();
    • 在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。(执行位置不在当前执行环境)

      // 定时器
      setTimeout(function timeHandler(){
        console.log('111');
      }100)
      
      // 事件监听
      $('#app').click(function(){
        console.log('DOM Listener');
      })
    • IIFE(立即执行函数表达式)创建闭包, 保存了全局作用域window当前函数的作用域

      var a = 2;
      (function IIFE(){
        // 输出2
        console.log(a);
      })();
    • 函数递归调用自身。

考点3:this

  1. 谈谈你对JS中 this 的理解。

    this 是非常简单的东西。只需要理解它的执行规则就OK。

    this 是运行时动态绑定,所以 this 的值与它所处的位置无关,而是与当前的执行环境(上下文)有关。

    一些 this 隐式绑定的场景:

    • 全局上下文默认this指向 window, 严格模式下指向 undefined。

    • 直接调用。this相当于全局上下文。

      let obj = {
        a: function() {
          console.log(this);
        }
      }
      let func = obj.a;
      func();
    • 对象.方法的情况,this指向这个对象。

      obj.method();
    • DOM事件绑定中,onclickaddEventerListener中 this 默认指向绑定事件的元素。

    • new + 构造函数,此时构造函数中的 this 指向实例对象。

    • 箭头函数没有 this,因此也不能绑定。里面的 this 会指向当前最近的非箭头函数的 this,找不到就是window(严格模式是undefined)。

    综上, this 的绑定是(按优先级):

    • apply、call 将 this 绑定为第一个参数。new 方式调用函数,this 绑定返回的对象。
    • this 绑定调用函数的上下文对象。
    • this 默认绑定 global/window,严格模式是 undefined。
    • 特殊,bind 只返回改变 this 指向的函数副本,不改变当前执行环境的 this。(返回的函数的执行上下文改变)

    es6 箭头函数中:

    • 箭头函数不会创建 this,只会从父作用域继承 this。
    • 指向最近的非箭头函数的 this。
    • 如果没找到,this 指向 window(严格模式 undefined)。

    this 传递的本质:

    obj.method 访问的结果并非只是一个函数,而是一个 Reference Type 的值。

    Reference Type 的值是一个三个值的组合 (base, name, strict),其中:

    • base 是对象。
    • name 是属性名。
    • strictuse strict 模式下为 true。

    () 被在 Reference Type 上调用时,它们会接收到关于对象和对象的方法的完整信息,然后可以设置正确的 this

  2. 如何改变 this?

    使用 call/apply/bind可以显示绑定:

    let obj1 = {
      name: "a",
      getName() {
        console.log(this.name);
      },
    };
    let obj2 = {
      name: "b",
    };
    obj1.getName() //a
    obj1.getName.call(obj2)//b
    obj1.getName.apply(obj2)//b
    obj1.getName.bind(obj2)()//b
    obj1.getName()//a
  3. 如何实现一个 call/apply 函数?

    关键是如何实现向原函数传递数量不定的参数。使用 es6 的展开运算符可以轻松做到(如果不限定 es3)。

    另外两个解决方案:

    • eval(string)魔鬼
    • new Function([arg1[, arg2[, ...argN]],] functionBody)

    call:

    //简易版,不考虑同名覆盖问题
    Function.prototype.call = function (context, ...args) {
      context = context || window;
      context.fn = this;
    
      var result = context.fn(...args);
    
      delete context.fn
      return result;
    }

    apply:

    Function.prototype.apply = function (context, args) {
      context = context || window;
      context.fn = this;
      var result = context.fn(...args);
    
      delete context.fn
      return result;
    }
  4. 如何模拟实现一个 bind 的效果?

    要实现 bind 效果,需要满足以下两点:

    1. 对于普通函数,绑定this指向。
    2. 对于构造函数,要保证原函数的原型对象上的属性不能丢失。
    Function.prototype.myBind = function bind(thisArg) {
      if (typeof this !== "function") {
        throw new TypeError(this + " must be a function");
      }
      // 存储调用bind的函数本身
      var self = this;
      // 去除thisArg的其他参数 转成数组
      var args = [].slice.call(arguments, 1);
      var bound = function () {
        // bind返回的函数 的参数转成数组
        var boundArgs = [].slice.call(arguments);
        var finalArgs = args.concat(boundArgs);
        // new 调用时,其实this instanceof bound判断也不是很准确。es6 new.target就是解决这一问题的。
        if (this instanceof bound) {
          // 这里是实现的 new 的第 1, 2, 4 步
          // 1.创建一个全新的对象
          // 2.并且执行[[Prototype]]链接
          // 4.通过`new`创建的每个对象将最终被`[[Prototype]]`链接到这个函数的`prototype`对象上。
          // self可能是ES6的箭头函数,没有prototype,所以就没必要再指向做prototype操作。
          if (self.prototype) {
            // ES5 提供的方案 Object.create()
            // bound.prototype = Object.create(self.prototype);
            // 但 既然是模拟ES5的bind,那浏览器也基本没有实现Object.create()
            // 所以采用 MDN ployfill方案 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create
            function Empty() {}
            Empty.prototype = self.prototype;
            bound.prototype = new Empty();
          }
          // 这里是实现的 new 的第 3 步
          // 3.生成的新对象会绑定到函数调用的`this`。
          var result = self.apply(this, finalArgs);
          // 这里是实现的 new 的第 5 步
          // 5.如果函数没有返回对象类型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`),
          // 那么`new`表达式中的函数调用会自动返回这个新的对象,否则返回函数的返回值。
          var isObject = typeof result === "object" && result !== null;
          var isFunction = typeof result === "function";
          if (isObject || isFunction) {
            return result;
          }
          return this;
        } else {
          // apply修改this指向,把两个函数的参数合并传给self函数,并执行self函数,返回执行结果
          return self.apply(thisArg, finalArgs);
        }
      };
      return bound;//返回的函数应具备普通函数和构造函数的功能
    };
  5. new 的模拟实现

    new 做了什么?

    1. 创建一个全新的对象。

    2. 这个对象会被执行 [[Prototype]](也就是__proto__)链接。

    3. 生成的新对象绑定到函数调用的 this。

    4. 通过 new 创建的每个对象最终将被 [[Prototype]]链接到函数的 prototype对象上。

    5. 如果函数没有返回对象类型Object(FunctionArrayDateRegExpError),那么 new 表达式的函数调用会自动返回这个新的对象。

    function newOperator(ctor) {
      if (typeof ctor !== "function") {
        throw "newOperator function the first param must be a function";
      }
      newOperator.target = ctor;
      var newObj = Object.create(ctor.prototype);
      var argsArr = [].slice.call(arguments, 1);
      var ctorReturnResult = ctor.apply(newObj, argsArr);
      var isObject =
        typeof ctorReturnResult === "object" && ctorReturnResult !== null;
      var isFunction = typeof ctorReturnResult === "function";
      if (isObject || isFunction) {
        return ctorReturnResult;
      }
      return newObj;
    }

数组

考点1:类数组对象

  1. 函数的 arguments 为什么不是数组?

    因为函数的 arguments 实质上是一个对象,其属性是 0,1,2……这样排列,最后还有 calle 和 length 属性。它类似于数组,却并不是数组。这样的对象叫做类数组对象。

    常用的类数组还有:

    • getElementsByTagName/ClassName() 获得的 HTMLCollection
    • querySelector 获得的 nodeList

    这样的话很多数组的方法就不能使用了。必要时需要将他们转化为数组。

  2. 类数组对象如何转化成数组?

    实现方法很多:

    • Array.prototype.slice.call()

      slice 返回原数组的浅拷贝

      function sum(a, b) {
        let args = Array.prototype.slice.call(arguments);
        return args.reduce((sum, cur) => sum + cur);
      }
    • Array.prototype.splice.call()

      splice 会返回删除的数组元素,可以利用这一点。

      function sum(a, b) {
        let args = Array.prototype.splice.call(arguments,0);
        return args.reduce((sum, cur) => sum + cur);
      }
    • Array.from()

      从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。

      也可用来转换 Set、Map

      function sum(a, b) {
        let args = Array.from(arguments);
        return args.reduce((sum, cur) => sum + cur);
      }
    • ES6 展开运算符

      function sum(a, b) {
        let args = [...arguments];
        return args.reduce((sum, cur) => sum + cur);
      }
    • concat + apply

      利用 apply 方法会把第二个参数展开。

      function sum(a, b) {
        let args = Array.prototype.concat.apply([], arguments);
        console.log(args.reduce((sum, cur) => sum + cur));
      }
    • 原始方法,for 循环遍历。

考点2:数组遍历

  1. forEach 中 return 有效果吗?如何中断 forEach 循环?

    在 forEach 中 return 函数不会返回。

    中断方法:

    • 使用 try...catchcatch 中捕获异常并抛出新异常。

    • 官方推荐方法(替换方法):用 everysome 替代 forEach 函数。every 在碰到 return false 的时候,中止循环。some 在碰到 return true 的时候,中止循环。

      everysome 都是对数组的每个元素用一个指定的函数检测。区别在于检测的结果。

      every 全部通过返回 truesome 至少一个通过返回 true

  2. 判断数组中是否包含某个值

    • Array.prototype.indexOf()

      arr.indexOf(searchElement[, fromIndex])

      返回在数组中可以找到给定元素的第一个索引。不存在返回 -1

      var arr=[1,2,3,4];
      var index=arr.indexOf(3);
      console.log(index);//2
    • Array.prototype.includes()

      arr.includes(valueToFind[, fromIndex])

      根据数组中是否存在指定元素返回 true 和 false

      var arr=[1,2,3,4];
      if(arr.includes(3))
          console.log("存在");
      else
          console.log("不存在");
    • Array.prototype.find()

      arr.find(callback[, thisArg])

      返回满足测试函数的第一个元素的值

      var arr=[1,2,3,4];
      var result = arr.find(item =>{
          return item > 3
      });
      console.log(result);
    • Array.prototype.findIndex()

      返回满足测试函数的第一个元素的索引,不存在返回 -1

      var arr=[1,2,3,4];
      var result = arr.findIndex(item =>{
          return item > 3
      });
      console.log(result);

考点3:数组扁平化

  1. 实现数组 flatten 方法

    多维数组转化为一维数组。

    1. es6 的 flat 方法

      按照指定的深度递归遍历数组,将所有元素和子数组的元素合并为一个新数组返回。

      ary = ary.flat(Infinity);
    2. replace + split + map

      replace[]替换为空字符串, split 分割,map 转换为数字。

      let str = JSON.stringify(ary)
      ary = str.replace(/(\[|\])/g, '').split(',').map(item=>+item)
    3. replace + JSON.parse

      replace 替换字符串,JSON.parse 转化为对象。

      let str = JSON.stringify(ary);
      str = str.replace(/(\[|\])/g, '');
      str = '[' + str + ']';
      ary = JSON.parse(str);
    4. 普通递归

      function flatten(arr) {
        let res = [];
        for (let i = 0; i < arr.length; i++) {
          if (Array.isArray(arr[i])) {
            res = res.concat(flatten(arr[i]));
          } else {
            res.push(arr[i]);
          }
          return res;
        }
      }
    5. reduce 迭代

      function flatten(arr) {
        return arr.reduce(
          (pre, cur) => pre.concat(Array.isArray(cur) ? flatten(cur) : cur),
          []
        );
      }
      function flatten(arr) {
        return Array.isArray(arr)
          ? arr.reduce((pre, cur) => [...pre, ...flatten(cur)], [])
          : [arr];
      }
    6. 扩展运算符

      利用展开语法结合 concat 特性

      function flatten(arr) {
        while (arr.some(Array.isArray)) {
          arr = [].concat(...arr);
        }
        return arr;
      }

考点4:数组去重

  1. 实现数组去重,方法越多越好

    • Set 去重

      有个小问题,不能完美处理稀疏数组

      [...new Set(arr)]
    • Map 去重

      function arrayNonRepeatfy(arr) {
        let map = new Map();
        let array = new Array();
        for (let i = 0; i < arr.length; i++) {
          if (!map.has(arr[i])) {
            map.set(arr[i]);
            if (i in arr) array.push(arr[i]); //处理稀疏数组情况
          }
        }
        return array;
      }
    • 两层循环遍历 + splice 去重

      缺点是会改变原数组

      function arrayNonRepeatfy(arr) {
        for (let i = 0; i < arr.length; i++) {
          for (j = i + 1; j < arr.length; j++) {
            if (arr[i] == arr[j]) {
              arr.splice(j, 1);
              j--;
            }
          }
        }
        return arr;
      }
    • 一层循环遍历 + indexOf 去重

      function arrayNonRepeatfy(arr) {
        let array = new Array()
        for (let i = 0; i < arr.length; i++) {
          if(arr.indexOf(arr[i]) == i){
            array.push(arr[i])
          }
        }
        return array;
      }
    • sort 排序两两比较去重

      function arrayNonRepeatfy(arr) {
        let array = new Array()
        arr = arr.sort((a,b)=>a-b)
        for (let i = 0; i < arr.length; i++) {
          if(arr[i] !== arr[i+1]){
            array.push(arr[i])
          }
        }
        return array;
      }
    • 一层循环遍历 + includes 去重

      function arrayNonRepeatfy(arr) {
        let array = new Array();
        for (let i = 0; i < arr.length; i++) {
          if (i in arr) {
            if (!array.includes(arr[i])) {
              array.push(arr[i]);
            }
          }
        }
        return array;
      }
    • filter + indexOf 去重

      function arrayNonRepeatfy(arr) {
        return arr.filter((prev, index, arr) => arr.indexOf(prev) == index);
      }
    • reduce + includes 去重

      function arrayNonRepeatfy(arr) {
        return arr.reduce(
          (acc, cur) => (acc.includes(cur) ? acc : [...acc, cur]),
          []
        );
      }

考点5:高阶函数

  1. 什么是高阶函数?

    一个函数可以接收一个或多个函数作为参数或者返回值为一个函数。

    可归纳为:

    • 输入上接收一个或多个函数作为参数。
    • 输出上返回一个函数。
  2. 数组中的高阶函数有哪些?

    1. Array.prototype.map()

      创建一个新数组,其结果是该数组中的每个元素调用一次提供的函数后的返回值。

      语法:

      var new_array = arr.map(function callback(currentValue[, index[, array]]) {
       // Return element for new_array 
      }[, thisArg])

      参数:

      callback:生成新数组元素的函数,使用三个参数:

      • currentValuecallback 数组中正在处理的当前元素。
      • index可选:callback 数组中正在处理的当前元素的索引。
      • array可选:map 方法调用的数组。

      thisArg可选:执行 callback 函数时值被用作this

    2. Array.prototype.reduce()

      对数组中的每一个元素执行提供的 reducer 函数,将其结果汇总为单个返回。

      语法:

      arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

      参数:

      callback:执行数组中每个值 (如果没有提供 initialValue则第一个值除外)的函数,包含四个参数:

      accumulator:累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或initialValue(见于下方)。

      • currentValue:数组中正在处理的元素。
      • index 可选:数组中正在处理的当前元素的索引。 如果提供了initialValue,则起始索引号为0,否则从索引1起始。
      • array可选:调用reduce()的数组

      initialValue可选:作为第一次调用 callback函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。

    3. Array.prototype.filter()

      创建一个新数组,包含通过提供的函数测试的所有元素。

      语法:

      var newArray = arr.filter(callback(element[, index[, array]])[, thisArg])

      参数:

      callback:用来测试数组的每个元素的函数。返回 true 表示该元素通过测试,保留该元素,false 则不保留。它接受以下三个参数:

      • element:数组中当前正在处理的元素。
      • index可选:正在处理的元素在数组中的索引。
      • array可选:调用了 filter 的数组本身。

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

    4. Array.prototype.sort()

      对数组元素进行 排序,并返回数组。

      语法:

      arr.sort([compareFunction])

      参数:

      compareFunction 可选:用来指定按某种顺序进行排列的函数。如果省略,元素按照转换为的字符串的各个字符的 Unicode 位点进行排序。

      • firstEl:第一个用于比较的元素。
      • secondEl:第二个用于比较的元素。

考点6:数组常用 API 实现

  1. 能不能实现数组 forEach 方法?

    需要注意的就是使用 in 来进行原型链查找。同时,如果没有找到就不处理,能有效处理稀疏数组的情况。

    Array.prototype.myforEach = function (callbackFn, thisArg) {
      //处理数组类型异常
      if (this == null) {
        throw new TypeError("Cannot read property 'forEach' of null or undefined");
      }
      //处理回调类型异常
      if (Object.prototype.toString.call(callbackFn) != "[object Function]") {
        throw new TypeError(callbackFn + " is not a function");
      }
      //草案中提到要先转换为对象
      let O = Object(this);
      //保证len为数字且为整数
      let len = O.length >>> 0;
    
      for (let i = 0; i < len; i++) {
        if (i in O) {
          callbackFn.call(thisArg, O[i], i, O);
        }
      }
    };
  2. 能不能实现数组 map 方法?

    与 forEach 类似,注意返回的数组与原数组等长的细节。

    Array.prototype.map = function (callbackFn, thisArg) {
      //处理数组类型异常
      if (this == null) {
        throw new TypeError("Cannot read property 'map' of null or undefined");
      }
      //处理回调类型异常
      if (Object.prototype.toString.call(callbackFn) != "[object Function]") {
        throw new TypeError(callbackFn + " is not a function");
      }
      //草案中提到要先转换为对象
      let O = new Object(this);
      let T = thisArg;
      //保证len为数字且为整数  
      let len = this.length >>> 0;
      let A = new Array(len);
    
      for (let k = 0; k < len; k++) {
        if (k in O) {
          let kValue = O[k];
          //依次传入this,当前项,当前索引,整个数组
          let mapValue = callbackFn.call(T, kValue, k, O);
          A[k] = mapValue;
        }
      }
      return A;
    };
  3. 能不能实现数组 reduce 方法?

    注意传不传初始值时的情况要分开处理。

    Array.prototype.reduce = function (callbackFn, initialValue) {
      //处理数组类型异常
      if (this == null) {
        throw new TypeError("Cannot read property 'reduce' of null or undefined");
      }
      //处理回调类型异常
      if (Object.prototype.toString.call(callbackFn) != "[object Function]") {
        throw new TypeError(callbackFn + " is not a function");
      }
      //草案中提到要先转换为对象
      let O = new Object(this);
      let len = this.length;
      let k = 0;
      let accumulator = initialValue;
      //不传初始值,默认找数组第一项
      if (accumulator === undefined) {
        for (; k < len; k++) {
          if (k in O) {
            accumulator = O[k];
            k++;
            break;
          }
        }
      }
      //表示数组全为空
      if (k === len && accumulator === undefined) {
        throw new Error("Each element of the array is empty");
      }
      for (; k < len; k++) {
        if (k in O) {
          let kValue = O[k];
          //依次传入 累加器,当前项,当前索引,整个数组
          accumulator = callbackFn.call(undefined, accumulator, kValue, k, O);
        }
      }
      return accumulator;
    };
  4. 能不能实现数组 push、pop 方法 ?

    push 方法:

    注意处理数组长度细节即可。

    Array.prototype.push = function (...items) {
      let O = Object(this);
      let len = O.length >>> 0;
      let argCount = items.length >>> 0;
      //处理数组长度异常
      if (len + argCount > 2 ** 53 - 1) {
        throw new TypeError(
          "The number of array is over the max value restricted!"
        );
      }
      for (let i = 0; i < argCount; i++) {
        O[len + i] = items[i];
      }
      let newLength = len + argCount;
      O.length = newLength;
      return newLength;
    };

    pop 方法:

    注意在删除前存储值。

    Array.prototype.pop = function () {
      let O = Object(this);
      let len = O.length >>> 0;
      //处理数组长度异常
      if (len === 0) {
        O.length = 0;
        return undefined;
      }
      len--;
      let value = O[len];
      delete O[len];
      O.length = len;
      return value;
    };
  5. 能不能实现数组filter方法 ?

    与 map 类似,注意返回值是通过测试项。

    Array.prototype.filter = function (callbackFn, thisArg) {
      // 处理数组类型异常
      if (this === null || this === undefined) {
        throw new TypeError("Cannot read property 'filter' of null or undefined");
      }
      // 处理回调类型异常
      if (Object.prototype.toString.call(callbackFn) != "[object Function]") {
        throw new TypeError(callbackFn + " is not a function");
      }
      let O = Object(this);
      let len = O.length >>> 0;
      let resLen = 0;
      let res = [];
      for (let i = 0; i < len; i++) {
        if (i in O) {
          let element = O[i];
          //依次传入 当前项,当前索引,整个数组
          if (callbackFn.call(thisArg, O[i], i, O)) {
            res[resLen++] = element;
          }
        }
      }
      return res;
    };
  6. 能不能实现数组 slice 方法?

    难点在于处理参数边界情况(非 number 类型和小数、负数、越界情况)。

    Array.prototype.slice = function (begin, end) {
      // 处理数组类型异常
      if (this == null) {
        throw new TypeError("Cannot read property 'slice' of null or undefined");
      }
      let array = Object(this);
      let len = array.length >>> 0;
      //参数边界处理
    
      //省略start,默认从 0 开始
      let start = isNaN(Number(begin)) ? 0 : Number(begin);
      if (start > 0) start = start > len ? len : Math.floor(start);
      if (start < 0)
        //start + len > 0, start 表示倒数第几项,否则为 0
        start = Math.ceil(start) + len < 0 ? 0 : Math.ceil(start) + len;
    
      //省略 end,提取到数组末尾
      let upTo = isNaN(Number(end)) ? len : Number(end);
      if (upTo > 0) upTo = upTo > len ? len : Math.floor(upTo);
      if (upTo < 0) upTo = Math.ceil(upTo) + len < 0 ? 0 : Math.ceil(upTo) + len;
    
      let size = Math.max(0, upTo - start);
      let res = new Array(size);
      // 不使用数组方法,因为 slice 可转换类数组对象
      for (let i = start; i < start + size; i++) {
        if (i in array) res[i - start] = array[i];
      }
      return res;
    };
  7. 能不能实现数组 splice 方法?

    注意重排序在不同情况时的遍历顺序。

    Array.prototype.splice = function (start, deleteCount, ...items) {
      let array = Object(this);
      let len = array.length >>> 0;
      //参数边界处理
    
      //startIndex默认从 0 开始
      start = isNaN(Number(start)) ? 0 : Number(start);
      if (start > 0) start = start > len ? len : Math.floor(start);
      if (start < 0)
        //start + len > 0, start 表示倒数第几项,否则为 0
        start = Math.ceil(start) + len < 0 ? 0 : Math.ceil(start) + len;
    
      //省略 deleteCount,start之后数组的所有元素都会被删除。
      deleteCount = isNaN(Number(deleteCount)) ? len - start : Number(deleteCount);
      if (deleteCount > 0)
        deleteCount =
          deleteCount > len - start ? len - start : Math.floor(deleteCount);
      if (deleteCount < 0) deleteCount = 0;
    
      // 判断 sealed 对象和 frozen 对象, 即 密封对象 和 冻结对象
      if (Object.isSealed(array) && deleteCount !== items.length) {
        throw new TypeError("the object is a sealed object!");
      } else if (Object.isFrozen(array) && (deleteCount > 0 || items.length > 0)) {
        throw new TypeError("the object is a frozen object!");
      }
    
      let deleteArr = new Array(deleteCount);
      // 拷贝删除元素
      for (let i = 0; i < deleteCount; i++) {
        let index = start + i;
        if (index in array) {
          let current = array[index];
          deleteArr[i] = current;
        }
      }
      //重排序
      const moveElements = (start, deleteCount, ...items) => {
        if (deleteCount === items.length) {
          return;
        }
        // 删除大于插入,整体前移
        if (deleteCount > items.length) {
          for (let i = start + deleteCount; i < array.length; i++) {
            // 从最后删除的元素下标开始
            let from = i;
            let to = i - deleteCount + items.length;
            if (from in array) {
              array[to] = array[from];
            } else {
              delete array[to];
            }
          }
          for (
            let i = array.length - 1;
            i >= array.length + items.length - deleteCount;
            i--
          ) {
            delete array[i];
          }
        }
        // 删除小于插入,整体后移
        if (deleteCount < items.length) {
          for (let i = array.length - 1; i > start + deleteCount; i--) {
            let from = i;
            let to = from - deleteCount + items.length;
            if (from in array) {
              array[to] = array[from];
            } else {
              delete array[to];
            }
          }
        }
      };
      moveElements(start, deleteCount, ...items);
      // 插入新元素
      for (let i = 0; i < items.length; i++) {
        array[start + i] = items[i];
      }
      array.length = array.length - deleteCount + items.length;
      return deleteArr;
    };

节流防抖

考点1:节流

  1. 能不能实现一个节流器?

    传统写法:

    function throttled(fn, delay) {
      let flag = true;
      return function (...args) {
        if (!flag) return;
        flag = false;
        setTimeout(() => {
          fn.apply(this, args);
          flag = true;
        }, delay);
      };
    }

    简洁写法:

    function throttled(fn, delay) {
        let timer;
        return function (...args) {
            if (!timer) {
                timer = setTimeout(() => {
                    fn.apply(this, args)
                    timer = null
                }, delay);
            }
        }
    }

    加强写法:

    function throttled(fn, delay) {
      let timer;
      let startTime = Date.now();
      return function (...args) {
        let curTime = Date.now();
        let remaining = delay - (curTime - startTime);
        let context = this;
        clearTimeout(timer);
        if (remaining <= 0) {
          fn.apply(context, args);
          startTime = Date.now();
        } else {
          timer = setTimeout(() => fn.apply(context, args), remaining);
        }
      };
    }

考点2:防抖

  1. 能不能实现一下防抖器?

    传统防抖:

    function debounce(fn, interval) {
      let timer;
      return function (...args) {
        if (timer) clearTimeout(timer);
        let context = this;
        timer = setTimeout(() => {
          fn.apply(context, args);
        }, interval);
      };
    }

    首次立刻执行的防抖:

    function debounce(fn, delay, immediate) {
      let timeout;
      return function (...args) {
        if (timeout) clearTimeout(timeout);
        if (immediate) {
          let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发
          timeout = setTimeout(() => {
            timeout = null;
          }, delay);
          if (callNow) {
            fn.apply(this, args);
          }
        } else {
          timeout = setTimeout(() => {
            fn.apply(this, args);
          }, delay);
        }
      };
    }

考点3:合并

  1. 能不能实现同时防抖和节流?

    function throttle(fn, delay) {
      let last = 0, timer = null;
      return function (...args) {
        let context = this;
        let now = new Date();
        if(now - last < delay){
          clearTimeout(timer);
          setTimeout(function() {
            last = now;
            fn.apply(context, args);
          }, delay);
        } else {
          // 这个时候表示时间到了,必须给响应
          last = now;
          fn.apply(context, args);
        }
      }
    }

后记

JS进阶内容见 esnext 篇。