前端面试·JS基础

2,261 阅读28分钟

以下是面试前端实习时遇到的js基础问题,合集参见前端小白的面试问题集

1.Promise平常怎么用?

// resolve代表成功 reject失败 都是一个函数

let p = new Promise(function(reslove,reject){

    reslove('成功')  //状态由等待变为成功,传的参数作为then函数中成功函数的实参

    reject('失败')  //状态由等待变为失败,传的参数作为then函数中失败函数的实参

})

//then中有2个参数,第一个参数是状态变为成功后应该执行的回调函数,第二个参数是状态变为失败后应该执行的回调函数。

p.then((data)=>{

    console.log('成功'+data)

},(err)=>{

    console.log('失败'+err)

})

2.如何实现Promise 基础版

class Promise{
  constructor(executor){
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    // 成功存放的数组
    this.onResolvedCallbacks = [];
    // 失败存放法数组
    this.onRejectedCallbacks = [];
    let resolve = value => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        // 一旦resolve执行,调用成功数组的函数
        this.onResolvedCallbacks.forEach(fn=>fn());
      }
    };
    let reject = reason => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        // 一旦reject执行,调用失败数组的函数
        this.onRejectedCallbacks.forEach(fn=>fn());
      }
    };
    try{
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }
  then(onFulfilled,onRejected) {
    if (this.state === 'fulfilled') {
      onFulfilled(this.value);
    };
    if (this.state === 'rejected') {
      onRejected(this.reason);
    };
    // 当状态state为pending时
    if (this.state === 'pending') {
      // onFulfilled传入到成功数组
      this.onResolvedCallbacks.push(()=>{
        onFulfilled(this.value);
      })
      // onRejected传入到失败数组
      this.onRejectedCallbacks.push(()=>{
        onRejected(this.reason);
      })
    }
  }
}

3.Promise如何实现等待多个请求

  • promise.all()
    Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
Promise.all([p1, p2]).then((result) => {
  console.log(result)               //['成功了', 'success']
}).catch((error) => {
  console.log(error)
})
  • promise.race()
    Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。
Promise.race([p1, p2]).then((result) => {
  console.log(result)
}).catch((error) => {
  console.log(error)  // 打开的是 'failed'
})

4.说一说JS动画吧

  • JS动画主要通过setTimeInterval,setTimeout来实现,但是二者都存在延迟加载的问题,很多时候无法和用户屏幕的刷新保持一致。
  • requestAnimationFrame是一个全局函数。调用requestAnimationFrame后,它会要求浏览器根据自己的频率进行一次重绘,它接收一个回调函数作为参数,在即将开始的浏览器重绘时,会调用这个函数,并会给这个函数传入调用回调函数时的时间作为参数。
  • css相比于js动画的不足在于无法控制播放的时机,比如无法实现鼠标触发后调用动画。 css动画常用属性:
    animation-name
    animation-duration
    animation-timing-function
    animation-delay
    animation-iteration-count
    animation-direction
    transition
    @keyframe

5.说一说箭头函数的特点

  • 箭头函数this是距离箭头函数最近的函数作用域或全局作用域,在运行期间的this值
  • 箭头函数不能作为构造函数,不能使用new
  • 箭头函数没有arguments,caller,callee
  • 箭头函数通过call和apply调用,不会改变this指向,只会传入参数
  • 箭头函数没有原型属性
  • 箭头函数不能作为Generator函数,不能使用yield关键字
  • 不能充当对象的方法

6.如何原生JS实现节点前插入元素和节点最后插入元素?

选中节点,节点最后node.parentNode.appendChild(newnode),某节点前:node.parentNode.insertBefore( newNode , node );

7.如何原生JS进行HTTP请求?

XMLHttpRequest对象

8.JS是如何实现继承的

  • 构造函数继承
const head = 1, body = 1, feet = 2;// ❌ 公有属性和方法定义在外面失去了封装性
function Person(name,gender){
    this.name = name;// 👍 可以定义私有 引用类型不会被共享
    this.gender = gender;
    this.head = head; // ❌ 可以定义公有 但需要放在外部
    this.body = body;
    this.feet = feet;
}
function Man(name){
    Person.call(this,name,"male"); // 👍 可以在子类传递参数给父类
}
  • 原型链继承
function Person(){};
Person.prototype={
    constructor: Person,// ❌ 需要手动绑定 constructor
    this.name :"human";// ❌ 不能定义私有属性,全部都是公有
    this.head : 1;// 👍 可以定义公有属性 所有实例都引用这个
    this.eat :()=>{ // 👍 方法被共享了
        console.log("ummmmm");
    }
}
function Man(){}; // ❌ 没办法向父类传递参数
Man.prototype=new Person();// 使用 new 操作符创建并重写 prototype
Man.prototype.constructor = Man; // ❌ 每次继承都需要手动修改 constructor 谁叫你是覆盖 prototype 属性呢
  • 组合式继承
function Person(name,gender){
    // 😀 私有的写这里
    this.name = name; // 👍 可以定义私有属性
    this.gender = gender;
}
Person.prototype = {
    // 😀 公有的写这里
    constructor :Person,// ❌ 需要手动绑定 constructor
    this.head : 1;// 👍 这里定义的公有属性会被共享
    this.eat : function(){
        console.log("ummmm");
    }
}
function Man(name){
    Person.call(this,name);// 👍 可以向父类传递参数 ⚡ 这里又调用了一次 Parent
}
Man.prototype=new Person();// 使用 new 操作符创建并重写 prototype
Man.prototype.constructor = Man; // ❌ 每次继承都需要手动修改 constructor 谁叫你是覆盖 prototype 属性呢
  • 原型式继承
let person = {
    name: 'human',
    head : 1; // ❌ 父类的引用属性全部被子类所共享
    eat : function(){ // 👍 父类方法可以复用
        console.log("ummmm");
    }
}

let man = Object.create(person) // ❌ 子类不能向父类传递参数
  • 寄生式继承
let person = {
    name: 'human',
    head : 1; 
    eat : function(){ // 👍 父类方法可以复用
        console.log("ummmm");
    }
}

function create(obj) {
    let clone = Object.create(obj) // 本质上还是 Object.create
    clone.print = function() { // 👍 增加一些属性或方法
        console.log(this.name)
    }
    return clone
}

let man = create(person)❌ 子类不可传递参数给父类
  • 寄生组合式继承
function Person(name, friends) {
    this.name = name//👍 私有的写在构造函数
    this.friends = friends
}
Person.prototype = {
    constructor :Person,// ❌ 需要手动绑定 constructor
    this.head : 1;// 👍 这里定义的公有属性会被共享
    this.eat : function(){
        console.log("ummmm");
    }
}

function Man(name) {
    Parent.call(this, name) // ⚡ 这里只需要调用一次 Parent
    this.gender = gender
}
// 上半部分和组合继承一样

let F = function() {} // 创建一个中介函数
F.prototype = Person.prototype // 这个中介的原型指向 Parent 的原型
Man.prototype = new F() // 注意这里没有使用 new 操作符调用 Parent
Man.prototype.constructor = Man
  • ES6 Class
class Person {
    constructor(name) { // 该属性在构造函数上,不共享
        this.name = name
    }
    log() { // 该方法在原型上,共享
        return this
    }
}
Person.prototype.share = [1, 2, 3] // 原型上的属性,共享

class Man extends Person {
    constructor(name) {
        super(name)
        this.gender = "male"
    }
}

9.JS数据类型有哪些

js 一共有六种基本数据类型,分别是 Undefined、Null、Boolean、Number、String,还有在 ES6 中新增的 Symbol 类型, 代表创建后独一无二且不可变的数据类型,它的出现我认为主要是为了解决可能出现的全局变量冲突的问题。

目前BigInt正在加入到标准中,解决Number只能表示2^53-1内的数字的缺陷。

引用类型:Object Array Date RegExp Function

10.事件捕获与冒泡的过程,默认是事件捕获还是事件冒泡?

与绑定事件的方法有关:

  • element.addEventListener(type, listener[, useCapture]); // IE6~8不支持(捕获和冒泡通过useCapture,默认false(冒泡),手动设置true后捕获)
  • element.attachEvent(’on’ + type, listener); // IE6~10,IE11不支持(只执行冒泡事件)
  • element['on' + type] = function(){} // 所有浏览器(默认执行冒泡事件) 默认是捕获还是冒泡-冒泡(IE只支持冒泡),addEventListener第三个参数默认为false, 如果设置为true才是捕获

参考:JavaScript捕获和冒泡探讨

W3C规范中定义了3个事件阶段,依次是捕获阶段、目标阶段、冒泡阶段。事件对象按照上图的传播路径依次完成这些阶段。如果某个阶段不支持或事件对象的传播被终止,那么该阶段就会被跳过。举个例子,如果Event.bubbles属性被设置为false,那么冒泡阶段就会被跳过。如果Event.stopPropagation()在事件派发前被调用,那么所有的阶段都会被跳过。 (滚动事件无法取消传播)

  • 捕获阶段:在事件对象到达事件目标之前,事件对象必须从window经过目标的祖先节点传播到事件目标。 这个阶段被我们称之为捕获阶段。在这个阶段注册的事件监听器在事件到达其目标前必须先处理事件。
  • 目标阶段:事件对象到达其事件目标。 这个阶段被我们称为目标阶段。一旦事件对象到达事件目标,该阶段的事件监听器就要对它进行处理。如果一个事件对象类型被标志为不能冒泡。那么对应的事件对象在到达此阶段时就会终止传播。
  • 冒泡阶段: 事件对象以一个与捕获阶段相反的方向从事件目标传播经过其祖先节点传播到window。这个阶段被称之为冒泡阶段。在此阶段注册的事件监听器会对相应的冒泡事件进行处理。

在一个事件完成了所有阶段的传播路径后,它的Event.currentTarget会被设置为null并且Event.eventPhase会被设为0。Event的所有其他属性都不会改变(包括指向事件目标的Event.target属性).

11.异步加载有哪些方法

    1. 动态的添加一个script标签,叫做Script DOM Element:
    1. onload:script的方法放在一个函数里面,然后放在window的onload函数中
  • 3.defer:html5新特性,只有ie可以用,将在onload之后加载
    <script defer src="example.js"></script>
  • async:将在donload之后加载
    <script async src="example.js"></script>

12.类名冲突如何解决

使用CSS Module

  • CSS Modules 能最大化地结合现有 CSS 生态和 JS 模块化能力,API 简洁到几乎零学习成本。
  • 发布时依旧编译出单独的 JS 和 CSS。它并不依赖于 React,只要你使用 Webpack,可以在 Vue/Angular/jQuery 中使用。

13.说一说ES6的模块化

  • 第一种是 CommonJS 方案,它通过 require 来引入模块,通过 module.exports 定义模块的输出接口。这种模块加载方案是 服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式 加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
  • 第二种是 AMD 方案,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定 义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范。
  • 第三种是 CMD 方案,这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现了 CMD 规范。它和 require.js 的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。
  • 第四种方案是 ES6 提出的方案,使用 import 和 export 的形式来导入导出模块。这种方案和上面三种方案都不同。

14.事件委托是什么?

  • 事件委托本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到 目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件代理。
  • 使用事件代理我们可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理我们还可以实现事件的动态绑定,比如说新增了一个子节点,我们并不需要单独地为它添加一个监听事件,它所发生的事件会交给父元素中的监听函数来处理。

15.如何实现统计Dom树的每个节点下有多少个节点?

递归每个节点,统计子节点,直至没有子节点,每统计一个计数器加1。

16.静态作用域与动态作用域的区别

  • 静态作用域是指声明的作用域是根据程序正文在编译时就确定的,有时也称为词法作用域。JavaScript是静态作用域。
  • 在采用动态作用域的语言中,程序中某个变量所引用的对象是在程序运行时刻根据程序的控制流信息来确定的。
var value = 1;

function foo() {
  console.log(value);
}

function bar() {
  var value = 2;
  foo();
}

bar();  //输出1

17.数组去重方法

①for循环双重遍历,找到相同的就用splice删除重复元素; ②数组includes、indexOf方法,一个一个从原数组弹出,检查新数组有没有包含该元素; ③利用对象的属性key唯一的特性去重,建立辅助对象,出现过的元素就将属性置为1; ④map用set和has,原理与对象的key相同; ⑤Es6的Set方法Array.from(new Set(arr)); ⑥sort()排序之后,逐一比对相邻元素,返回新数组; ⑦利用reduce作为累加器,返回新数组

    function distinct(arr) {
        return arr.sort().reduce((init, current) => {
            if(init.length === 0 || init[init.length-1] !== current) {
                init.push(current);
            }
            return init;
        }, []);
    }

参考:www.jianshu.com/p/70b978f51…

18.如何动态设置this

  • call/apply/bind
  • call()方法可以传递两个参数。第一个参数是指定函数内部中this的指向(也就是函数执行时所在的作用域),第二个参数是函数调用时需要传递的参数。
  • apply方法的作用与call方法类似,也是改变this指向(函数执行时所在的作用域),然后在指定的作用域中,调用该函数。同时也会立即执行该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数。
  • bind方法用于指定函数内部的this指向(执行时所在的作用域),然后返回一个新函数。bind方法并非立即执行一个函数。
    具体原理+实现:www.jianshu.com/p/473a86d50…

19.如何绑定事件

  • 在Dom中直接绑定
<input type="button" value="点我呦" onclick="alert("hello world!")"/>
  • 在script中绑定
 document.getElementById("btn").onclick = function () {
    console.log("hello world!");
};
  • 事件监听函数
document.getElementById("btn").addEventListener("click", function(){
    document.getElementById("demo").innerHTML = "Hello World";
});

20.Js严格模式是什么

  • 严格模式的作用:
  • 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
    消除代码运行的一些不安全之处,保证代码运行的安全;
    提高编译器效率,增加运行速度;
    为未来新版本的Javascript做好铺垫。
  • 进入标志:
    将"use strict"放在脚本文件的第一行,则整个脚本都将以"严格模式"运行。如果这行语句不在第一行,则无效,整个脚本以"正常模式"运行。可以针对脚本文件或单个函数使用
  • 语法改变:
    不允许使用未声明的变量;
    不允许删除变量或对象;
    不允许删除函数;
    不允许变量重名;
    不允许使用八进制;
    禁止this关键字指向全局对象
    参考:www.runoob.com/js/js-stric…

21.实现一个Promise执行队列

class Scheduler {
  constructor(maxSize) {
    this.maxSize = maxSize;
    this.currentSize = 0;
    this.queue = [];
  }
  add(promiseCreator) {
    if (this.currentSize === this.maxSize)
      return new Promise((resolve, reject) => {
        this.queue.push({ resolve, reject, promiseCreator });
      });
    else {
      this.currentSize += 1;
      return promiseCreator().then(
        value => {
          this.next();
          return Promise.resolve(value);
        },
        error => {
          this.next();
          return Promise.reject(error);
        }
      );
    }
  }
  next() {
    this.currentSize -= 1;
    if (this.queue.length === 0) return;
    const { promiseCreator, reject, resolve } = this.queue.shift();
    const wrapPromise = () => {
      return promiseCreator().then(
        value => resolve(value),
        error => reject(error)
      );
    };
    this.add(wrapPromise);
  }
}

const scheduler = new Scheduler(2);
const generateTask = function(msg, time) {
  console.time(msg);
  return () => new Promise(resolve => setTimeout(() => resolve(msg), time));
};
const task1 = scheduler.add(generateTask("Task 1", 1000));
task1.then(value => {
  console.log(value);
  console.timeEnd(value);
});
const task2 = scheduler.add(generateTask("Task 2", 2000));
task2.then(value => {
  console.log(value);
  console.timeEnd(value);
});
const task3 = scheduler.add(generateTask("Task 3", 500));
task3.then(value => {
  console.log(value);
  console.timeEnd(value);
});
const task4 = scheduler.add(generateTask("Task 4", 1200));
task4.then(value => {
  console.log(value);
  console.timeEnd(value);
});

22.JS垃圾回收机制

深度解读:深入理解之V8引擎的垃圾回收机制

①标记-清除 (参考:前端面试:谈谈 JS 垃圾回收机制

基本的垃圾回收算法称为“标记-清除”,定期执行以下“垃圾回收”步骤:

垃圾回收器获取根并“标记”(记住)它们。 然后它访问并“标记”所有来自它们的引用。 然后它访问标记的对象并标记它们的引用。所有被访问的对象都被记住,以便以后不再访问同一个对象两次。 以此类推,直到有未访问的引用(可以从根访问)为止。 除标记的对象外,所有对象都被删除。 例如,对象结构如下:

我们可以清楚地看到右边有一个“不可到达的块”。现在让我们看看“标记并清除”垃圾回收器如何处理它。

第一步标记根 然后标记他们的引用 以及子孙代的引用 现在进程中不能访问的对象被认为是不可访问的,将被删除 这就是标记清楚的工作原理。JavaScript引擎应用了许多优化,使其运行得更快,并且不影响执行。

  • 分代回收——对象分为两组:“新对象”和“旧对象”。许多对象出现,完成它们的工作并迅速结 ,它们很快就会被清理干净。那些活得足够久的对象,会变“老”,并且很少接受检查。
  • 增量回收——如果有很多对象,并且我们试图一次遍历并标记整个对象集,那么可能会花费一些时间,并在执行中会有一定的延迟。因此,引擎试图将垃圾回收分解为多个部分。然后,各个部分分别执行。这需要额外的标记来跟踪变化,这样有很多微小的延迟,而不是很大的延迟。
  • 空闲时间收集——垃圾回收器只在 CPU 空闲时运行,以减少对执行的可能影响。

②引用-计数

这种方式常常会引起内存泄漏,低版本的IE使用这种方式。机制就是跟踪一个值的引用次数,当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1,当这个变量指向其他一个时该值的引用次数便减一。当该值引用次数为0时就会被回收。

该方式会引起内存泄漏的原因是它不能解决循环引用的问题:

function sample(){
    var a={};
    var b={};
    a.prop = b;
    b.prop = a;
}

这种情况下每次调用sample()函数,a和b的引用计数都是2,会使这部分内存永远不会被释放,即内存泄漏。

23.生成器和Promise的区别

  • 生成器函数能生成一组值的序列。显式地向生成器请求一个新的值,随后生成器响应一个新生成的值,或者告诉我们它之后不会再生成新的值。
  • promise对象是对我们现在尚未得到但将来会得到的值的占位符。如果我们兑现了承诺,结果会得到一个值。如果发生了问题,结果则是一个错误。

24.async/await原理

ES7 中引入了 async/await,这种方式能够彻底告别执行器和生成器,实现更加直观简洁的代码。根据 MDN 定义,async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。可以说async 是Generator函数的语法糖,并对Generator函数进行了改进。 内置执行器。Generator 函数的执行必须依靠执行器,而 async

  • 1.函数自带执行器:无需手动执行 next() 方法。

  • 2.更好的语义:async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

  • 3.更广的适用性:co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

  • 4.返回值是 Promise:async 函数返回值是 Promise 对象,比 Generator 函数返回的 Iterator 对象方便,可以直接使用 then() 方法进行调用。

25.bind,apply,call的区别?

bind返回对应函数, 便于稍后调用; apply, call则是立即调用,且调用之后下次同样的操作还需要再次call,而bind则不用。

26.如何实现bind,apply,call?

// call函数实现
Function.prototype.myCall = function(context) {
  // 判断调用对象
  if (typeof this !== "function") {
    console.error("type error");
  }

  // 获取参数
  let args = [...arguments].slice(1),
    result = null;

  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;

  // 将调用函数设为对象的方法
  context.fn = this;

  // 调用函数
  result = context.fn(...args);

  // 将属性删除
  delete context.fn;

  return result;
};

// apply 函数实现

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

  let result = null;

  // 判断 context 是否存在,如果未传入则为 window
  context = context || window;

  // 将函数设为对象的方法
  context.fn = this;

  // 调用方法
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }

  // 将属性删除
  delete context.fn;

  return result;
};

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

  // 获取参数
  var args = [...arguments].slice(1),
    fn = this;

  return function Fn() {
    // 根据调用方式,传入不同绑定值
    return fn.apply(
      this instanceof Fn ? this : context,
      args.concat(...arguments)
    );
  };
};

27.为什么会出现箭头函数?解决的问题是什么?

在 ES6 的箭头函数下, call 和 apply 将失效, 对于箭头函数来说:

  • 箭头函数体内的 this 对象, 就是定义时所在的对象, 而不是使用时所在的对象;所以不需要类似于var _this = this这种丑陋的写法;

  • 箭头函数不可以当作构造函数,也就是说不可以使用 new 命令, 否则会抛出一个错误 箭头函数不可以使用 arguments 对象,,该对象在函数体内不存在. 如果要用, 可以用 Rest 参数代替

  • 不可以使用 yield 命令, 因此箭头函数不能用作 Generator 函数.

28.闭包的作用?实现一个简单闭包

闭包的出现:局部变量无法共享和长久的保存,而全局变量可能造成变量污染,所以我们希望有一种机制既可以长久的保存变量又不会造成全局污染,闭包允许函数访问并操作函数外部的变量,只要变量或函数存在于声明函数时的作用域内,闭包即可使函数访问这些变量或函数。

function car(){
    var speed = 0
    function fn(){
        speed++
        console.log(speed)
    }
    return fn
}
var speedUp = car()
speedUp() //1
speedUp() //2

29.实现防抖节流

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

// 函数节流: 规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。

// 函数防抖的实现
function debounce(fn, wait) {
  var timer = null;

  return function() {
    var context = this,
      args = arguments;

    // 如果此时存在定时器的话,则取消之前的定时器重新记时
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  };
}

// 函数节流的实现;
function throttle(fn, delay) {
  var preTime = Date.now();

  return function() {
    var context = this,
      args = arguments,
      nowTime = Date.now();

    // 如果两次时间间隔超过了指定时间,则执行函数。
    if (nowTime - preTime >= delay) {
      preTime = Date.now();
      return fn.apply(context, args);
    }
  };
}

30.json对象如何转为字符串?

使用JSON

31.如何实现深拷贝?

数组:

  • 遍历旧数组,将元素一个一个推入新数组,实现拷贝;
  • 借助于slice 方法实现数组的深拷贝,var son = father.slice(0);
  • 借助于concat 方法实现数组的深拷贝,var son = father.concat();
  • 使用ES6扩展运算符实现数组的深拷贝,var [ ...son ] = father.

对象:

  • jquery:使用$.extend({},obj);
  • lodash:使用_.cloneDeep(value);
  • JSON:使用JSON.parse(JSON.stringify(obj));
  • 自定义:
var clone = function (obj) { 
    if(obj === null) return null 
    if(typeof obj !== 'object') return obj;
    if(obj.constructor===Date) return new Date(obj); 
    var newObj = new obj.constructor ();  //保持继承链
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {   //不遍历其原型链上的属性
            var val = obj[key];
            newObj[key] = typeof val === 'object' ? arguments.callee(val) : val; // 使用arguments.callee解除与函数名的耦合
        }
    }  
    return newObj;  
}; 

32.proimse连续调用两个then()第一个then时间很长,最终的调用顺序是怎么样的?

new Promise(function (resolve, reject) {

    setTimeout(() => resolve(1), 10000); // (*)

}).then(function (result) { // (**)

    console.log("first")

}).then(function (result) { // (***)

    console.log("second")

}).then(function (result) {

    console.log("third")
})
输出结果:
first
second
third

严格链式调用=.=

33.隐式类型转换

  • null和undefined,相等。
  • 数字和字符串,转化为数字再做比较。 1 == '1' true
  • 如果有true或false,转化为1或0,再比较。
  • 引用类型会比较地址 [1] == [1] false
  • 其余都不相等。
  • true+"false" == "truefalse"
  • NaN == NaN false
  • 0开头数字表示八进制

34.JS常见的设计模式及实现

1.工厂模式

  • 定义

定义一个用于创建对象的接口,让子类决定将哪一个类实例化。Factory Method使一个类的实例化延迟到其子类。

  • 适用范围

适用于复杂逻辑判断的情况,例如购物的商品,可以有上架中、出售中、下单中、出货中、派送中、到手等一系列复杂逻辑判断。

class Image {}
class Link {}
class Text {}

class ElementFactory {
    createElement(type, option){
         const ELEMENT = {
            "image" : Image,
            "text"  : Text,
            "link"  : Link
        }
        let ElementConstructor = ELEMENT(type),
            element = null;
        if(ElementConstructor){
            element = new ElementConstructor(option);
        }
        return element;
    }
}

2.单例模式

  • 定义

确保只有一个实例,可以全局访问

  • 适用范围

适用于弹框的实现, 全局缓存。 实现弹框的一种做法是先创建好弹框, 然后使之隐藏, 这样子的话会浪费部分不必要的 DOM 开销, 我们可以在需要弹框的时候再进行创建, 同时结合单例模式实现只有一个实例, 从而节省部分 DOM 开销。

function sigle () {
    let instance;
    return function(name,age) {
        if (instance) {
            return instance;
        }
        
        this.name = name;
        this.age = age;

        instance = this;
        return instance;
    }
}

3.观察者模式

  • 定义

定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。

  • 适用范围

1、当观察的数据对象发生变化时, 自动调用相应函数。比如 vue 的双向绑定;

2、每当调用对象里的某个方法时, 就会调用相应'访问'逻辑。比如给测试框架赋能的 spy 函数;

const obj = {
    name: 'blue',
    age: 21
}

const bookHandle = {
    set(target, key, value) {
        target[key] = value;
        console.log('write blue');
        return true;
    },
    get(target, key) {
        console.log('call blue');
        return target[key];
    }
}
const nameBook = new Proxy(blue, bookHandle)

4.发布订阅模式

定义:在观察者模式中间,增加消息代理进行通信,来实现更更松的解耦。

发布订阅模式和观察者模式的差异:

1、在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。

2、在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。

3、观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。

4、观察者模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。

class Subject {
  constructor() {
    this.subs = []
    this.state = '张三' // 触发更新的状态
  }
  getState() {
    return this.state
  }
  setState(state) {
    if (this.state === state) {
      // 发布者一样
      return
    }
    this.state = state
    this.notify() // 有更新,触发通知
  }
  addSub(sub) {
    this.subs.push(sub)
  }
  removeSub(sub) {
    const idx = this.subs.findIndex(i => i === sub)
    if (idx === -1) {
      // 不存在该观察者
      return
    }
    this.subs.splice(idx, 1)
  }
  notify() {
    this.subs.forEach(sub => {
      sub.update() // 与观察者原型方法update对应!
    })
  }
}

// 观察人,相当于订阅者
class Observer {
  update() {
    console.log('update')
  }
}

// 测试代码
const subject = new Subject()
const ob = new Observer()
const ob2 = new Observer()
ob2.update = function() {
  //修改update方法,实现不同逻辑
  console.log('laifeipeng')
}

//目标添加观察者了
subject.addSub(ob)
subject.addSub(ob2)

//目标发布消息调用观察者的更新方法了
// subject.notify(); // 不使用手动触发,通过内部状态的设置来触发
subject.setState('李四')

35.JS new()对象的过程?

  • 创建一个新的空对象;
  • 将构造函数的作用域(this)指向新的对象;
  • 执行构造函数的内容,为新对象添加属性和方法;
  • 最终返回新的对象。
function _new(){
  // 1、创建一个新对象
  let target = {};
  let [constructor, ...args] = [...arguments];  // 第一个参数是构造函数
  // 2、原型链连接
  target.__proto__ = constructor.prototype;
  // 3、将构造函数的属性和方法添加到这个新的空对象上。
  let result = constructor.apply(target, args);
  if(result && (typeof result == "object" || typeof result == "function")){
    // 如果构造函数返回的结果是一个对象,就返回这个对象
    return result
  }
  // 如果构造函数返回的不是一个对象,就返回创建的新对象。
  return target
}
let p2 = _new(Person, "小花")
console.log(p2.name)  // 小花
console.log(p2 instanceof Person) // true

36.Vue如何监听到数组的变化?

  • Vue2.0

-- 使用函数劫持的方式,重写了数组的方法;

-- Vue将data中的数组,进行了原型链重写,指向了自己定义的数组原型方法,这样当调用数组api时,可以通知依赖更新。如果数组中包含引用类型,会对数组中的引用类型再次进行监控。

  • Vue3.0

-- 对所有数据转为Proxy代理,当触发Model里的数据变化时,必须经过Proxy这一层,从而监听数据的变化。

37.js数据存储在哪里?

首先内存中分为两块区域一块叫栈,一块叫堆。

简单数据类型是直接存在栈空间里面的,

引用数据类型则是存在堆空间里。

复制

  • 对于基本数据类型,如果进行复制,系统会自动为新的变量在栈内存中分配一个新值,很容易理解。
  • 对于数组、对象这样的引用数据类型复制,系统也会自动为新的变量在栈内存中分配一个值,但这个值仅仅是一个地址。也就是说,复制出来的变量和原有的变量具有相同的地址值,指向堆内存中的同一个对象。

特别注意,对于字符串的修改,不是直接在原地址赋值,而是另开辟一个空间去存新的字符串,然后变量名指向新的空间,因为之前的空间没有名字来调去使用了,所以浏览器会将其删除。这也是为什么当存储的字符串较多时,处理速度会慢的原因,这也是字符串的不可改变性。

38.变量提升

if(false){
    var a=1;
    let b=2;
}
console.log(a);//undefined
console.log(b);//未定义,报错

var会提升定义,在语句外定义 var a,但由于a=1在if(false)内,所以不执行,a的值为undefined,而let没有提升,没有定义,最终会报错。

var a = 1;
if(true){
    console.log(a);//报错
    let a = 2;
}

ECMAScript 2015(即ES6),let会提升变量到代码块顶部。然而,在变量声明前引用变量会导致ReferenceError错误。在代码块开始到变量声明之间变量处于暂时死区(temporal dead zone)。

在这里只是提升到顶部定义,但是还没有声明,因此报错。

39.如何用js将数组乱序输出

通过js内部的sort可以重载的特点:

functuon randomOrder(arr){
    arr.sort((a,b)=>{
        return Math.random()-0.5;
    })
}

40.用promise实现一个sleep函数

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function demo() {
console.log('Taking a break...');
await sleep(2000);
console.log('Two seconds later');
}

41.优化转换函数

42.JS的多态

同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。本质上是描述的“将‘做什么’和‘谁去做以及怎样去做’分开”,归根结底就是先消除不同对象的耦合关系。

非多态代码示例

var makeSound = function(animal) {
    if(animal instanceof Duck) {
        console.log('嘎嘎嘎');
    } else if (animal instanceof Chicken) {
        console.log('咯咯咯');
    }
}
var Duck = function(){}
var Chiken = function() {};
makeSound(new Chicken());
makeSound(new Duck());

多态的代码示例

var makeSound = function(animal) {
    animal.sound();
}

var Duck = function(){}
Duck.prototype.sound = function() {
    console.log('嘎嘎嘎')
}
var Chiken = function() {};
Chiken.prototype.sound = function() {
    console.log('咯咯咯')
}

makeSound(new Chicken());
makeSound(new Duck());

43.map与forEach的区别?

1、forEach()返回值是undefined,不可以链式调用。

2、map()返回一个新数组,原数组不会改变。

  • 项目踩坑:🚑这里特别注意,不改变针对数值元素,当map中的元素为对象时,map return改变属性的对象,是会改变原数组的

3、没有办法终止或者跳出forEach()循环,除非抛出异常,所以想执行一个数组是否满足什么条件,返回布尔值,可以用一般的for循环实现,或者用Array.every()或者Array.some();

44.双问号语法

const result = response?.settings?.n ?? 100

这个 ?? 的意思是,如果 ?? 左边的值是 null 或者 undefined,那么就返回右边的值。

45.关于js的this指向

var name = "name1"
const obj = {
    name: "name2",
    callName: function () {
        console.log(this.name);
    }
}
obj.callName();//🍗这里的词法作用域是obj本身,this指向obj,输出name2

const outCall = obj.callName;
outCall();//🍖这里没有传入this,this指向window,输出name1

function trans(f) {
    f();
}
trans(obj.callName);//🎃这里将function作为参数传入,执行时指向全局,输出name1

46.错误判断

Cannot read property ‘prototype’ of undefined是什么意思? 答:取某个对象的原型或者属性的时候,这个值还未定义,或者为undefined类型