我的JavaScript面试题总结(长更)

js的内容很重要,这些是自己整理出来的内容,为了方便自己刷题,顺便分享出来,参考了很多文章和博客,希望能给大家一些帮助。
欢迎大家关注我的语雀知识库,js面试题实时更新中
https://www.yuque.com/docs/share/a95df8ad-9493-4624-846d-a921248b669d?# 《js》

1、数据类型

1.1 基本数据类型

在 JS 中共有 8 种基础的数据类型,分别为:

Undefined 、 Null 、 Boolean 、 Number 、 String 、 Object 、 Symbol 、 BigInt 。

  • 值类型是直接存储在栈(stack) 中的简单数据段,占据空间⼩、⼤⼩固定,属于被频繁使⽤数据, 所以放⼊栈中存储;
  • 引⽤类型存储在堆(heap) 中的对象,占据空间⼤、⼤⼩不固定。如果存储在栈中,将会影响程序 运⾏的性能;

其中 Symbol 和 BigInt 是 ES6 新增的数据类型,可能会被单独问:

• Symbol 代表独⼀⽆⼆的值,最⼤的⽤法是⽤来定义对象的唯⼀属性名。 • BigInt 可以表示任意⼤⼩的整数。

1.2 数据类型的判断

typeof:能判断所有值类型,函数。不可对 null、对象、数组进⾏精确判断,因为都返回 object 。

为什么typeof null是Object 因为在JavaScript中,不同的对象都是使⽤⼆进制存储的,如果⼆进制前三位都是0 的 话,系统会判断为是Object类型,⽽null的⼆进制全是0,⾃然也就判断为Object

instanceof:能判断对象类型,不能判断基本数据类型,其内部运⾏机制是判断在其原型链中能否找到该类型 的原型。instance.[proto…] === instance.constructor.prototype

Object.prototype.toString.call():所有原始数据类型都是能判断的,还有 Error 对象,Date 对象等。

在⾯试中有⼀个经常被问的问题就是:如何判断变量是否为数组?

  Array.isArray(arr); // true 
​
  arr.__proto__ ===  Array.prototype; // true  
​
  arr instanceof Array; // true  
​
  Object.prototype.toString.call(arr);  // "[object Array]"  

1.3 手写深拷贝

 /** 
​
 * 深拷⻉ 
​
*@param {Object} obj 要拷⻉的对象  
​
*@param {Map} map ⽤于存储循环引⽤对象的地址  
​
 **/  
​
   function deepClone(obj = {}, map  = new Map()) {  
​
        if (typeof obj !==  "object") {  return obj;  } 
​
       if (map.get(obj)) { 
​
            return map.get(obj);
​
      }  
​
  let result = {};  // 初始化返回结果 
​
 if (obj instanceof Array ||// 加 || 的原因是为了防⽌ Array 的 prototype 被重写,Array.isArray 也是如此       Object.prototype.toString(obj)  === "[object Array]") {
​
          result = [];
​
  }  
​
 map.set(obj, result); // 防⽌循环引⽤
​
  for (const key in obj) { 
​
 // 保证 key 不是原型属性
​
          if (obj.hasOwnProperty(key)) { 
​
         // 递归调⽤
​
          result[key] = deepClone(obj[key],  map);  
​
          }
​
  }  
​
  // 返回结果  
​
return result;  

JSON.parse(JSON.stringify(obj))

性能最快

▪ 具有循环引⽤的对象时,报错

▪ 当值为函数、undefined、或symbol时,⽆法拷⻉

浅拷⻉

以赋值的形式拷⻉引⽤对象,仍指向同⼀个地址,修改时原对象也会受到影响

  • Object.assign
  • 展开运算符(…) var shallowObj2 = { …obj1 }

1.4 根据 0.1+0.2 ! == 0.3,讲讲 IEEE 754 ,如何让其相等?

• 进制转换 :js 在做数字计算的时候,0.1 和 0.2 都会被转成⼆进制后⽆限循环 ,但是 js 采⽤ 的 IEEE 754 ⼆进制浮点运算,最⼤可以存储 53 位有效数字,于是⼤于 53 位后⾯的会全部截 掉,将导致精度丢失。

• 对阶运算 :由于指数位数不相同,运算时需要对阶运算,阶⼩的尾数要根据阶差来右移(0舍1⼊), 尾数位移时可能会发⽣数丢失的情况,影响精度。

JavaScript使⽤Number类型表示数字(整数和浮点数),遵循 IEEE 754 标准 通过64位来表示⼀个数字

在这里插入图片描述

图⽚⽂字说明 :

• 第0位:符号位,0表示正数,1表示负数(s) • 第1位到第11位:储存指数部分(e)

• 第12位到第63位:储存⼩数部分(即有效数字)f

js最⼤安全数是 Number.MAX_SAFE_INTEGER == Math.pow(2,53) - 1, ⽽不是Math.pow(2,52) - 1

why?尾数部分不是只有52位吗?

这是因为⼆进制表示有效数字总是1.xx…xx的形式,尾数部分f在规约形式下第⼀位默认为1(省略不写, xx…xx为尾数部分f,最⻓52位)。因此,JavaScript提供的有效数字最⻓为53个⼆进制位(64位浮点的后52 位+被省略的1位)

运算时发⽣了什么?

先按照IEEE 754转成相应的⼆进制,然后对阶运算

怎么解决精度问题?

1.将数字转成整数

这是最容易想到的⽅法,也相对简单

  function add(num1, num2) {  
​
     const num1Digits = (num1.toString().split('.')[1]  || '').length; 
​
     const num2Digits =  (num2.toString().split('.')[1] || '').length; 
​
     const baseNum = Math.pow(10,  Math.max(num1Digits, num2Digits)); 
​
     return (num1 * baseNum + num2 *  baseNum) / baseNum;  
​
}  

但是这种⽅法对⼤数⽀持的依然不好

2.三⽅库

这个是⽐较全⾯的做法,推荐2个库

1).Math.js

2).big.js

3 转成字符串,对字符串做加法运算。
 // 字符串数字相加 
 var addStrings = function  (num1, num2) {  
    let i = num1.length - 1;  
    let j = num2.length - 1;  
    const res = [];  
    let carry = 0;  
    while (i >= 0 || j >= 0) {  
        const n1 = i >= 0 ? Number(num1[i]) : 0;  
        const n2 = j >= 0 ? Number(num2[j]) : 0;  
        const sum = n1 + n2 + carry;  
        res.unshift(sum % 10);  
        carry = Math.floor(sum / 10);  i--;  j--;  
    }  
    if (carry) {  
        res.unshift(carry);  
    }  
    return res.join("");  
  };  
​
  function isEqual(a, b, sum) {  
    const [intStr1, deciStr1] =  a.toString().split(".");  
    const [intStr2, deciStr2] =  b.toString().split(".");  
    const inteSum =  addStrings(intStr1, intStr2); // 获取整数相加部分  
    const deciSum =  addStrings(deciStr1, deciStr2); // 获取⼩数相加部分  
    return inteSum + "."  + deciSum === String(sum);  
  }  
  console.log(isEqual(0.1, 0.2, 0.3)); // true  

2、 原型和原型链

• 原型:每⼀个 JavaScript 对象(null 除外)在创建的时候就会与之关联另⼀个对象,这个对象就是 我们所说的原型,每⼀个对象都会从原型"继承"属性,其实就是 prototype 对象。

• 原型链:由相互关联的原型组成的链状结构就是原型链。
在这里插入图片描述

3、 作⽤域与作⽤域链

• 作⽤域

规定了如何查找变量,也就是确定当前执⾏代码对变量的访问权限。

换句话说,作⽤域决定了代码区块中变量和其他资源的可⻅性。(全局作⽤域、函数作⽤域、块级作⽤域)

另外,for循环还有⼀个特别之处,就是设置循环变量的那部分是⼀个⽗作⽤域,⽽循环体内部是⼀个 单独的⼦作⽤域。

• 作⽤域链

从当前作⽤域开始⼀层层往上找某个变量,如果找到全局作⽤域还没找到,就放弃寻找 。这 种层级关系就是作⽤域链。(由多个执⾏上下⽂的变量对象构成的链表就叫做作⽤域链)

什么是⾃由变量

什么叫做 ⾃由变量 。

如下代码中,console.log(x)要得到x变量,但是在当前的作⽤域中没有定义x。当前作⽤域没有定义的变量,这成为 ⾃由变量 。⾃由变量的值如何得到 —— 要到创建这个函数的那个 域”。 作⽤域中取值,这⾥强调的是 “创建” ,⽽不是“调⽤”,切记切记——其实这就是所谓的"静态作⽤域"

 var x = 10  function fn() { 
    console.log(x)  } 
 function show(f) {  
    var x = 20  
    (function() {  
        f() //10,⽽不是20  
    })()//立即执行函数  
​
  } 
  show(fn)  

let块级作用域如何做到获取循环中的i

下⾯代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每⼀次循环的i其实都是⼀个新的变量,所以最后输出的是6。

你可能会问,如果每⼀轮循环的变量i都是重新声明的,那它怎么知道上⼀轮循环的值,从⽽计算出本轮循环的值?

这是因为 JavaScript 引擎内部会记住上⼀轮循环的值,初始化本轮的变量i时,就在上⼀轮循环的基础上进⾏计算

 var a = [];  、
 for (let i = 0; i < 10; i++)  {  
    a[i] = function () {  
        console.log(i);  
        };  
    }  
 a[6](); // 6  

作⽤域与执⾏上下⽂

我们知道JavaScript属于解释型语⾔,JavaScript的执⾏分为:

解释和执⾏两个阶段,这两个阶段所做的事并不 ⼀样:

解释阶段

• 词法分析

• 语法分析

• 作⽤域规则确定

执⾏阶段

• 创建执⾏上下⽂

• 执⾏函数代码

• 垃圾回收

JavaScript解释阶段便会确定作⽤域规则,因此作⽤域在函数定义时就已经确定了,⽽不是在函数调⽤时确定

但是执⾏上下⽂是函数执⾏之前创建的

执⾏上下⽂最明显的就是this的指向是执⾏时确定的

⽽作⽤域 访问的变量是编写代码的结构确定的。

作⽤域和执⾏上下⽂之间最⼤的区别是:

  • 执⾏上下⽂在运⾏时确定,随时可能改变;
  • 作⽤域在定义时就确 定,并且不会改变。

4、 闭包

根据 MDN 中⽂的定义,闭包的定义如下:

在 JavaScript 中,每当创建⼀个函数,闭包就会在函数创建的同时被创建出来。可以在⼀个内层函数中访问到其外层函数的作⽤域。

也可以这样说:

闭包是指那些能够访问⾃由变量的函数。 ⾃由变量是指在函数中使⽤的,但既不是函数参数也不是函数的局部变量的变量。

闭包 = 函数 + 函数能够访问的⾃由变量。

在某个内部函数的执⾏上下⽂创建时,会将⽗级函数的活动对象加到内部函数的 [[scope]] 中,形成作⽤域链,所以即使⽗级函数的执⾏上下⽂销毁(即执⾏上下⽂栈弹出⽗级函数的执⾏上下⽂),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从⽽实现了闭包。

闭包应⽤

函数作为参数被传递:

  function print(fn)  {  
    const a =  200;  
    fn();  
  }  
​
  const a =  100;  function fn()  {  
    console.log(a);  
  }  
​
  print(fn); // 100  函数作为返回值被返回:  
​
  function create()  {  
    const a =  100;  
    return function () {  
        console.log(a);  
        };  
    }  
​
  const fn = create();  
  const a =  200;  
  fn(); // 100  

⾃由变量的查找,是在函数定义的地⽅,向上级作⽤域查找。不是在执⾏的地⽅。

5、 call、apply、bind 实现

call

call() ⽅法在使⽤⼀个指定的 this 值和若⼲个指定的参数值的前提下调⽤某个函数或⽅法。

  Function.prototype.myCall  = function (context) {  
    // 判断调⽤对象  
    if (typeof this !== "function") {  
        throw new Error("Type  error");  
        }  
    // ⾸先获取参数  
    let args =  [...arguments].slice(1);  
    let result = null;  
    // 判断 context 是否传⼊,如果没有传就设置为window  
    context = context || window;  
    // 将被调⽤的⽅法设置为 context 的属性  
    // this 即为我们要调⽤的⽅法  
    context.fn = this;  
    // 执⾏要被调⽤的⽅法  
    result = context.fn(...args);  
    // 删除⼿动增加的属性⽅法  
    delete context.fn;  
    // 将执⾏结果返回  
    return result;  
​
  };  

apply

参数为数组并调⽤

 Function.prototype.myApply  = function (context) {  
    if (typeof this !== "function") {  
        throw new Error("Type  error");  
    }  
    let result = null;  
    context = context || window;  
    // 与call代码相⽐,我们使⽤ Symbol 来保证属性唯⼀  
    // 也就是保证不会重写⽤户⾃⼰原来定义在  context 中的同名属性  
    const fnSymbol = Symbol();  
    //context[fnSymbol]等同于context.fn
    context[fnSymbol] = this;  
    // 执⾏要被调⽤的⽅法  
    if (arguments[1]) {  
        result =  context[fnSymbol](...arguments[1]);  
    } else {  
        result = context[fnSymbol]();  
    }  
    delete context[fnSymbol];  
    return result;  
​
  };  

bind

bind() 返回的是⼀个函数

这个地⽅可以详细阅读这篇⽂章,讲的⾮常清楚: https://github.com/sisterAn/JavaScript-Algorithms/issues/81

  Function.prototype.myBind = function  (context) {  
  // 判断调⽤对象是否为函数  
  if (typeof this !== "function")   {  
    throw new Error("Type   error");  
  }  
  // 获取参数  
  const args = [...arguments].slice(1)
  const fn =  this;  
  return function Fn()  {  
    return fn.apply(this instanceof Fn  ? this : context,  
    // 当前的这个 arguments 是指 Fn 的参数  
    args.concat(...arguments)  
    );};  
  };  

6、 new 实现

1 ⾸先创⼀个新的空对象。

2 根据原型链,设置空对象的 proto 为构造函数的 prototype 。

3 构造函数的 this 指向这个对象,执⾏构造函数的代码(为这个新对象添加属性)。

4 判断函数的返回值类型,如果是引⽤类型,就返回这个引⽤类型的对象。

  function myNew(context) {  
    const obj = new Object();  
    obj.__proto__ =  context.prototype;  
    const res = context.apply(obj,  [...arguments].slice(1));  
    return typeof res === "object"  ? res : obj;  
  }  

7、 异步

7.1 event loop、宏任务和微任务

什么是宏任务与微任务?

我们都知道 Js 是单线程都,但是⼀些⾼耗时操作就带来了进程阻塞问题。为了解决这个问题,Js 有两种任务 的执⾏模式:同步模式(Synchronous)和异步模式(Asynchronous)。

在异步模式下,创建异步任务主要分为宏任务与微任务两种。

ES6 规范中,宏任务(Macrotask) 称为 Task, 微任务(Microtask) 称为 Jobs。宏任务是由宿主(浏览器、Node)发起的,⽽ 微任务由 JS ⾃身发起。

在这里插入图片描述

如何理解 script(整体代码块)是个宏任务呢

实际上如果同时存在两个 script 代码块,会⾸先在执⾏ 第⼀个 script 代码块中的同步代码,如果这个过程中创 建了微任务并进⼊了微任务队列,第⼀个 script 同步代 码执⾏完之后,会⾸先去清空微任务队列,再去开启第 ⼆个 script 代码块的执⾏。所以这⾥应该就可以理解 script(整体代码块)为什么会是宏任务。

什么是 EventLoop ?

1 判断宏任务队列是否为空

◦ 不空 --> 执⾏最早进⼊队列的任务 --> 执⾏下⼀步

◦ 空 --> 执⾏下⼀步

2 判断微任务队列是否为空

◦ 不空 --> 执⾏最早进⼊队列的任务 --> 继续检查微任务队列空不空

◦ 空 --> 执⾏下⼀步

因为⾸次执⾏宏队列中会有 script(整体代码块)任务,所以实际上就是 Js 解析完成后,在异步任务中,会先 执⾏完所有的微任务,这⾥也是很多⾯试题喜欢考察的。需要注意的是,新创建的微任务会⽴即进⼊微任务队 列排队执⾏,不需要等待下⼀次轮回。

7,2 Promise

1 Promise 是⼀个类,在执⾏这个类的时候会传⼊⼀个执⾏器,这个执⾏器会⽴即执⾏

2 Promise 会有三种状态

  • Pending 等待
  • Fulfilled 完成
  • Rejected 失败

3 状态只能由 Pending --> Fulfilled 或者 Pending --> Rejected,且⼀但发⽣改变便不可⼆次修改; 4 Promise 中使⽤ resolve 和 reject 两个函数来更改状态; 5 then ⽅法内部做但事情就是状态判断 ◦ 如果状态是成功,调⽤成功回调函数 ◦ 如果状态是失败,调⽤失败回调函数

⼤家可以参考这篇⽂章讲的很详细

https://juejin.cn/post/6945319439772434469

还有promise 练习题点这⾥https://juejin.cn/post/6844904077537574919

插⼊⼀段⼿写Promose

class Promise {
    // 构造函数
    constructor(excutor) {
        //  添加属性
        this.PromiseState = 'pending'
        this.PromiseResult = null
        // 存放回调函数
        this.callbacks = []
​
        const self = this;
        // 声明resolve
        function resolve(value) {
            //状态只能改变一次
            if (self.PromiseState !== 'pending') return
            // 函数中的this指向Window
            //1.修改对象状态
            self.PromiseState = 'fullfilled'
            //2.改变对象结果值
            self.PromiseResult = value
​
            //判断执行的回调函数 
            // 异步执行时,改变状态后才执行回调
            self.callbacks.forEach((callback) => {
                setTimeout(() => {
                    callback.onResolved(value)
                });
​
            });
​
        }
        // 声明reject
        function reject(value) {
            //状态只能改变一次
            if (self.PromiseState !== 'pending') return
            // 函数中的this指向Window
            //1.修改对象状态
            self.PromiseState = 'rejected'
            //2.改变对象结果值
            self.PromiseResult = value
​
            //判断执行的回调函数 
            //异步执行时,改变状态后才执行回调
            self.callbacks.forEach((callback) => {
                setTimeout(() => {
                    callback.onRejected(value)
                });
​
            });
​
        }
        try {
            // 同步调用执行器函数
            excutor(resolve, reject)
        } catch (error) {
            // 修改Promise的状态为失败
            reject(error)
        }
    }
​
​
    then(onResolved, onRejected) {
        const self = this
        // 判断回调参数
        if (typeof onRejected !== "function") {
            onRejected = (reason) => {
                throw reason
            }
        }
        if (typeof onResolved !== "function") {
            onResolved = value => value
        }
​
        // 返回Promise对象
        return new Promise((resolve, reject) => {
            //封装try catch
            function callback(type) {
                try {
                    let result = type(self.PromiseResult)
​
                    if (result instanceof Promise) {
                        //如果是Promise
                        result.then((r) => {
                            resolve(r)
                        }, (err) => {
                            reject(err)
                        })
                    } else {
                        // 改变promise的状态 不是promise的都会为成功
                        resolve(result)
                    }
                } catch (error) {
                    reject(error)
                }
            }
            // 调用状态
            // this指向实例对象p
            if (this.PromiseState === "fullfilled") {
                setTimeout(() => {
                    callback(onResolved)
                });
            }
            else if (this.PromiseState === "rejected") {
                setTimeout(() => {
                    callback(onRejected)
                });
            }
​
            // 处理异步调用
            else if (this.PromiseState === "pending") {
                // 异步调用 保存回调函数
                this.callbacks.push({
                    onResolved: function () {
                        callback(onResolved)
                    },
                    onRejected: function () {
                        callback(onRejected)
                    }
                })
            }
        });
​
​
    }
​
    catch(onRejected) {
        return this.then('undefined', onRejected)
​
    }
​
    // static 属于类,不属于实例对象
    static resolve(value) {
        return new Promise((resolve, reject) => {
            if (value instanceof Promise) {
                value.then((result) => {
                    resolve(result)
                }).catch((err) => {
                    reject(err)
                });
            } else {
                resolve(value)
            }
        });
    }
    static reject(error) {
        return new Promise((resolve, reject) => {
            reject(error)
        });
    }
    static all(promises) {
        // 返回Promise的结果数组,有一个错误则返回错误
        return new Promise((resolve, reject) => {
            let count = 0;
            let arr = [];
            for (let i = 0; i < promises.length; i++) {
                promises[i].then((result) => {
                    //如果这个成功,存贮结果
                    count++;
                    arr.push(result)
                    if (count == promises.length) {
                        resolve(arr)
                    }
                }, (err) => {
                    reject(err)
                })
            }
​
        });
    }
    static race(promises) {
        return new Promise((resolve, reject) => {
            promises.forEach(item => {
                item.then((result) => {
                    resolve(result)
                }, (err) => {
                    reject(err)
                })
            });
        });
    }
​
}

7.3 async/await 和 Promise 的关系

• async/await 是消灭异步回调的终极武器。

• 但和 Promise 并不互斥,反⽽,两者相辅相成。

• 执⾏ async 函数,返回的⼀定是 Promise 对象。

• await 相当于 Promise 的 then。

• try…catch 可捕获异常,代替了 Promise 的 catch。

8、 浏览器的垃圾回收机制

两种垃圾回收策略

• 标记清除:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是⾮活动对象)销毁。

• 引⽤计数:它把对象是否不再需要简化定义为对象有没有其他对象引⽤到它。如果没有引⽤指向该对象 (引⽤计数为 0),对象将被垃圾回收机制回收。

标记清除的缺点

• 内存碎⽚化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过⼤的对 象时找不到合适的块。

• 分配速度慢,因为即便是使⽤ First-fit 策略,其操作仍是⼀个 O(n) 的操作,最坏情况是每次都要遍历到 最后,同时因为碎⽚化,⼤对象的分配效率会更慢。

解决以上的缺点可以使⽤ 标记整理(Mark-Compact)算法 ,标记结束后,标记整理算法会将活着的对 象(即不需要清理的对象)向内存的⼀端移动,最后清理掉边界的内存

引⽤计数的缺点

• 需要⼀个计数器,所占内存空间⼤,因为我们也不知道被引⽤数量的上限。

• 解决不了循环引⽤导致的⽆法回收问题。

V8 的垃圾回收机制也是基于标记清除算法,不过对其做了⼀些优化。

• 针对新⽣区采⽤并⾏回收。

• 针对⽼⽣区采⽤增量标记与惰性回收。

9,继承

1.组合继承

组合继承, 有时候也叫做伪经典继承,指的是将原型链和借⽤构造函数的技术组合到⼀块,从⽽发挥两者之⻓的⼀种继承模式.

基本思路: 使⽤原型链实现对原型属性和⽅法的继承,通过借⽤构造函数来实现对实例属性的继承.

  function Father(name){  
    this.name = name;  
    this.colors = ["red","blue","green"];  
  }  
  Father.prototype.sayName =  function(){  
    alert(this.name);  
  };  
  function Son(name,age){  
    Father.call(this,name);//继承实例属性,第⼀次调⽤Father()  
    this.age = age;  
 }  
 Son.prototype = new Father();//继承⽗类⽅法,第⼆次调⽤Father()  
 Son.prototype.sayAge =  function(){  
    alert(this.age);  
 }  

组合继承避免了原型链和借⽤构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常⽤的继承模式. ⽽且, instanceof 和 isPrototypeOf( )也能⽤于识别基于组合继承创建的对象.

缺点:

组合继承其实调⽤了两次⽗类构造函数, 造成了不必要的消耗,

2.原型继承

该⽅法最初由道格拉斯·克罗克福德于2006年在⼀篇题为 《Prototypal Inheritance in JavaScript》 (JavaScript中的原型式继承) 的⽂章中提出. 他的想法是借助原型可以基于已有的对象创建新对象, 同时还 不必因此创建⾃定义类型. ⼤意如下:

在object()函数内部, 先创建⼀个临时性的构造函数, 然后将传⼊的对象作为这个构造函数的原型,最后返回了 这个临时类型的⼀个新实例.

function object(o){  
    function F(){}  
    F.prototype = o;  
    return new F();  
}  

在 ECMAScript5 中,通过新增 object.create() ⽅法规范化了上⾯的原型式继承.object.create() 接收两个参数:

• ⼀个⽤作新对象原型的对象

• (可选的)⼀个为新对象定义额外属性的对象 缺点:原型式继承中, 包含引⽤类型值的属性始终都会共享相应的值

3.寄⽣式继承

寄⽣式继承是与原型式继承紧密相关的⼀种思路, 同样是克罗克福德推⽽⼴之.

寄⽣式继承的思路与(寄⽣)构造函数和⼯⼚模式类似, 即创建⼀个仅⽤于封装继承过程的函数,该函数在内部 以某种⽅式来增强对象,最后再像真的是它做了所有⼯作⼀样返回对象.

 function createAnother(original){ 
    var clone = object(original);
    //通过调⽤object函数创建⼀个新对象  
    clone.sayHi = function(){
    //以某种⽅式来增强这个对象  
        alert("hi");  
    };  
    return clone;//返回这个对象  
​
  }  

缺点:使⽤寄⽣式继承来为对象添加函数, 会由于不能做到函数复⽤⽽降低效率;这⼀点与构造函数模式类似.

4.寄⽣组合式继承

基本思路: 不必为了指定⼦类型的原型⽽调⽤超类型的构造函数

         function SuperType() {
            this.property = true;
            this.say2 = function() {
                console.log("222");
            }
        }
​
        SuperType.prototype.getSuperValue = function() {
            console.log("yes");
        };
​
        function SubType(name, sex) {
            SuperType.call(this) //组合父构造函数this指向子
            this.name = name;
            this.sex = sex;
            this.say1 = function() {
                console.log(hi111);
            }
        }
​
        function inherit(son, father) {
            //构造一个对象obj,原型对象为father
            var obj = Object.create(father.prototype); //原型式
            //将son 赋值obj的构造函器 obj的原型对象指向father
            obj.constructor = son; //增强对象
            //将obj这个对象给son的原型对象
            son.prototype = obj;
        }
​
        inherit(SubType, SuperType);
        let sub1 = new SubType("lili", 12)
        let sub2 = new SuperType();
​
        console.log(sub1.__proto__);//super
        console.log(sub1.say2);
        console.log(sub2);

5,ES6的extend

//class 相当于es5中构造函数
        //class中定义方法时,前后不能加function,全部定义在class的protopyte属性中
        //class中定义的所有方法是不可枚举的
        //class中只能定义方法,不能定义对象,变量等
        //class和方法内默认都是严格模式
        //es5中constructor为隐式属性
        class People{
          constructor(name='wang',age='27'){
            this.name = name;
            this.age = age;
            console.log(`${this.name} ${this.age} eat food`)
          }
          eat(){
            console.log(`${this.name} ${this.age} eat food`)
          }
        }
        //继承父类
        class Woman extends People{ 
           constructor(name = 'ren',age = '27'){ 
             //继承父类属性
             super(name, age); 
             console.log(`${this.name} ${this.age} `)
           } 
            eat(){ 
             //继承父类方法
              super.eat() 
            } 
        } 
        let wonmanObj=new Woman('xiaoxiami'); 
        wonmanObj.eat();

在这里插入图片描述

10,模块化

模块化开发在现代开发中已是必不可少的⼀部分,它⼤⼤提⾼了项⽬的可维护、可拓展和可协作性。通常, 我们 在浏览器中使⽤ ES6 的模块化⽀持,在 Node 中使⽤ commonjs 的模块化⽀持。

分类:

◦ es6: import / export ◦ commonjs: requiremodule.exports / exports

◦ amd: require / defined

require与import的区别

◦ require⽀持 动态导⼊,import不⽀持,正在提案 (babel 下可⽀持) ◦ require是 同步 导⼊,import属于 异步 导⼊

◦ require是 值拷⻉,导出值变化不会影响导⼊值;import指向 内存地址,导⼊值会随导出值⽽变化

11·防抖与节流

防抖与节流函数是⼀种最常⽤的 ⾼频触发优化⽅式,能对性能有较⼤的帮助。

防抖 (debounce)

JavaScript 专题之跟着 underscore 学防抖 https://github.com/mqyqingfeng/Blog/issues/22

将多次⾼频操作优化为只在最后⼀次执⾏,通常使⽤的场景是:⽤户输⼊,只需再输⼊完成后做⼀次输⼊校验即可。

​
        function debounce(fn, delay) {
            let time = null; //只执行一次=null
            return function() { //返回函数     
                let context = this;
                if (time) { //清除掉time 后重新赋值time               
                    clearTimeout(time);
                }
                time = setTimeout(function() {
                    fn.apply(context,...arguments[0])
                  //  fn(arg);
                }, delay)
            }
        }
​
        function fullPage(e) {
            console.log(e);
            console.log('滚动了');
        }
​
        document.onwheel = debounce(fullPage, 1000)

节流(throttle)

JavaScript 专题之跟着 underscore 学节流 https://github.com/mqyqingfeng/Blog/issues/26

每隔⼀段时间后执⾏⼀次,也就是降低频率,将⾼频操作优化成低频操作,通常使⽤场景: 滚动 条事件 或者 resize 事件,通常每隔 100~500 ms执⾏⼀次即可。

  //  使⽤时间戳 
function throttle(func,  wait) {  
    let preTime =  0;  
    return function () {  
        let nowTime = +new  Date();  
        let context =  this;  
        let args =  arguments;  
        if (nowTime - preTime >  wait) {  
            func.apply(context, args);  
            preTime = nowTime;  
        }  
    };  
  }  
​
  //  定时器实现  
function throttle(func,  wait) {  
    let timeout;  
    return function () {  
        let context =  this;  
        let args =  arguments;  
        if (!timeout) {  
            timeout = setTimeout(function  () {  
                timeout =  null;  
                func.apply(context, args);  
            }, wait);  
        }  
    };  
​
  }   
 

12 ES6/ES7

由于 Babel 的强⼤和普及,现在 ES6/ES7 基本上已经是现代化开发的必备了。通过新的语法糖,能让代码 整体更为简洁和易读。

1 声明(let const)

◦ let / const: 块级作⽤域、不存在变量提升、暂时性死区、不允许重复声明

◦ const: 声明常量,⽆法修改

var声明变量可以重复声明,⽽let不可以重复声明 var是不受限于块级的,⽽let是受限于块级 var会与window相映射(会挂⼀个属性),⽽let不与window相映射 var可以在声明的上⾯访问变量,⽽let有暂存死区,在声明的上⾯访问变量会报错 const声明之后必须赋值,否则会报错

const定义不可变的量,改变了就会报错 const和let⼀样不会与window相映射、⽀持块级作⽤域、在声明的上⾯访问变量会报错

2 箭头函数

(1)⽤了箭头函数,this就不是指向window,⽽是⽗级(指向是可变的)所以箭头函数中的this的指向在它在 定义时⼀家确定了,之后不会改变。

(2)不能够使⽤arguments对象 箭头函数没有prototype

(3)不能⽤作构造函数,这就是说不能够使⽤new命令,否则会抛出⼀个错误

(4)不可以使⽤yield命令,因此箭头函数不能⽤作 Generator 函数

(5) call()、apply()、bind()等⽅法不能改变箭头函数中的this指向

3 模版字符串

基本的字符串格式化。

将表达式嵌⼊字符串中进⾏拼接。 ⽤${}来界定在ES5时我们通过反斜杠()来做多⾏字符串或者字符串⼀⾏⾏拼接。 ES6反引号(``)就能解决类模板字符串的功能

4 Set / Map: 新的数据结构

应⽤场景Set⽤于数据重组,Map⽤于数据储存,都可以for…of 遍历

Set:

(1)成员不能重复 、Set的值是唯⼀的可以做数组去重 (2)只有键值没有键名

Map:

(1)本质上是健值对的集合

(2)可以遍历,可以跟各种数据格式转换

5 异步解决⽅案

◦ Promise的状态 :pending、fulfilled、reject 两个过程:padding -> fulfilled、padding -> rejected当pending为rejectd 时,会进⼊catch

◦ generator:

▪ yield: 暂停代码

▪ next(): 继续执⾏代码

6 理解 async/await以及对Generator的优势

async、await 是⽤来解决异步的,async函数是Generator函数的语法糖

使⽤关键字async来表示,在函数内部使⽤ await 来表示异步

async函数返回⼀个 Promise 对象,可以使⽤then⽅法添加回调函数

当函数执⾏的时候,⼀旦遇到await就会先返回,等到异步操作完成,再接着执⾏函数体内后⾯的语句

async较Generator的优势

(1)内置执⾏器。

Generator 函数的执⾏必须依靠执⾏器,⽽ Aysnc 函数⾃带执⾏器,调⽤⽅式跟普通函数的调⽤ ⼀样

(2)更好的语义。

async 和 await 相较于 * 和 yield 更加语义化

(3)更⼴的适⽤性。

yield命令后⾯只能是 Thunk 函数或 Promise对象,async函数的await后⾯可以是Promise也可以 是原始类型的值

(4)返回值是 Promise。

async 函数返回的是 Promise 对象,⽐Generator函数返回的Iterator对象⽅便,可以直接使 ⽤ then() ⽅法进⾏调⽤

7 forEach、for in、for of三者区别

  • forEach更多的⽤来遍历数组
  • for in ⼀般常⽤来遍历对象或json ,循环出的是key
  • for of数组对象都可以遍历,遍历对象需要通过Object.keys() ,循环出的是value

8 AST

抽象语法树 (Abstract Syntax Tree),是将代码逐字⺟解析成 树状对象 的形式。这是语⾔之间的转换、代码语法 检查,代码⻛格检查,代码格式化,代码⾼亮,代码错误提示,代码⾃动补全等等的基础。

  function square(n){  return n * n  }  

通过解析转化成的AST如下图:

在这里插入图片描述

9,babel编译原理

• babylon 将 ES6/ES7 代码解析成 AST

• babel-traverse 对 AST 进⾏遍历转译,得到新的 AST • 新 AST 通过 babel-generator 转换成 ES5

10. 函数柯⾥化

在⼀个函数中,⾸先填充⼏个参数,然后再返回⼀个新的函数的技术,称为函数的柯⾥化。通常可⽤于在不侵 ⼊函数的前提下,为函数 预置通⽤参数,供多次重复调⽤。

const add = function add(x) {  
    return function (y) {  
        return x + y  
    }  
}  
​
  const add1 = add(1)  
​
  add1(2) === 3  add1(20) === 21  

11.数组(array)

• map: 遍历数组,返回回调返回值组成的新数组

• forEach: ⽆法break,可以⽤try/catch中throw new Error来停⽌ • filter: 过滤

• some: 有⼀项返回true,则整体为true

• every: 有⼀项返回false,则整体为false

• join: 通过指定连接符⽣成字符串

• push / pop: 末尾推⼊和弹出,改变原数组, push 返回数组⻓度, pop 返回原数组最后⼀项; • unshift / shift: 头部推⼊和弹出,改变原数组,unshift 返回数组⻓度,shift 返回原数组第⼀项 ; • sort(fn) / reverse: 排序与反转,改变原数组

• concat: 连接数组,不影响原数组, 浅拷⻉

• slice(start, end): 返回截断后的新数组,不改变原数组

• splice(start, number, value…): 返回删除元素组成的数组,value 为插⼊项,改变原数组

• indexOf / lastIndexOf(value, fromIndex): 查找数组项,返回对应的下标

• reduce / reduceRight(fn(prev, cur), defaultPrev): 两两执⾏,prev 为上次化简函数的return值,cur 为当前值

◦ 当传⼊ defaultPrev 时,从第⼀项开始;

◦ 当未传⼊时,则为第⼆项

• 数组乱序:

  var arr = [1, 2, 3, 4, 5, 6, 7,  8, 9, 10]; 
  arr.sort(function () {  
    return Math.random() - 0.5;  
  });  

• 数组拆解: flat: [1,[2,3]] --> [1, 2, 3]

  Array.prototype.flat = function()  {  
    return this.toString().split(',').map(item  => +item )  }  

web 存储

cookie,session localStorage 和 sessionStorage。

1、cookie

• 本身⽤于浏览器和 server 通讯。

是保存在客户端的,⼀般由后端设置值,可以设置过期时间 储存⼤⼩只有4K。⼀般⽤来保存⽤户的

• 被“借⽤”到本地存储来的。 • 可⽤ document.cookie = ‘…’ 来修改。 其缺点:

• 存储⼤⼩限制为 4KB。

• http 请求时需要发送到服务端,增加请求数量。 • 只能⽤ document.cookie = ‘…’ 来修改,太过简陋 • 在http下cookie是明⽂传输的, 较不安全

cookie属性

http-only:不能被客户端更改访问,防⽌XSS攻击(保证cookie安全性的操作) Secure:只允许在https下传输

Max-age: cookie⽣成后失效的秒数

expire: cookie的最⻓有效时间,若不设置则cookie⽣命期与会话期相同

2.session

session是保存在服务端的 session的运⾏依赖sessionId,⽽sessionId⼜保存在cookie中,所以如果禁⽤的 cookie,session也是不能⽤的,不过硬要⽤也可以,可以把sessionId保存在URL 中

session⼀般⽤来跟踪⽤户的状态

session 的安全性更⾼,保存在服务端,不过⼀般为使服务端性能更加,会考虑 部分信息保存在cookie中

3.localStorage 和 sessionStorage

• HTML5 专⻔为存储来设计的,最⼤可存 5M。 • API 简单易⽤, setItem getItem。 • 不会随着 http 请求被发送到服务端。 它们的区别:

• localStorage 数据会永久存储,除⾮代码删除或⼿动删除。 • sessionStorage 数据只存在于当前会话,浏览器关闭则清空。 • ⼀般⽤ localStorage 会多⼀些。

localstorage存满了怎么办?

划分域名,各域名下的存储空间由各业务组统⼀规划使⽤ 跨⻚⾯传数据:考虑单⻚应 ⽤、采⽤url传输数据 最后兜底⽅案:情调别⼈的存储

怎么使⽤cookie保存⽤户信息

document.cookie(“名字 = 数据;expire=时间”)

怎么删除cookie

⽬前没有提供删除的操作,但是可以把它的Max-age设置为0,也就是⽴⻢失 效,也就是删除了

设计模式

参考文章 https://juejin.cn/post/6844904099704471559

1.MVVM(Model-View-ViewModel)

在这里插入图片描述

如上图所示:MVVM模式是在MVP模式的基础上进⾏了改良,将Presenter改良成ViewModel(抽象视图):

• ViewModel:内部集成了Binder(Data-binding Engine,数据绑定引擎),在MVP中派发器View或Model的更新都 需要通过Presenter⼿动设置,⽽Binder则会实现View和Model的双向绑定,从⽽实现View或Model的⾃动更新。

• View:可组件化,例如⽬前各种流⾏的UI组件框架,View的变化会通过Binder⾃动更新相应的Model。

• Model:Model的变化会被Binder监听(仍然是通过观察者模式),⼀旦监听到变化,Binder就会⾃动实现视图的更新。

好处

例如:

• 提升了可维护性,解决了MVP⼤量的⼿动同步的问题,提供双向绑定机制。

• 简化了测试,同步逻辑是交由Binder处理,View跟着Model同时变更,所以只需要保证Model的正确性,View就正 确。

额外的问题

• 产⽣性能问题,对于简单的应⽤会造成额外的性能消耗。

• 对于复杂的应⽤,视图状态较多,视图状态的维护成本增加,ViewModel构建和维护成本⾼。

对前端开发⽽⾔MVVM是⾮常好的⼀种设计模式。在浏览器中,路由层可以将控制权交由适当的ViewModel,后者⼜可以更新并响应持续的View,并且通过⼀些⼩修改MVVM模式可以很好的运⾏在服务器端,其中的原因就在于Model与 View已经完全没有了依赖关系(通过View与Model的去耦合,可以允许短暂View与持续View的并存),这允许View经 由给定的ViewModel进⾏渲染。

⽬前流⾏的框架Vue、React以及Angular都是MVVM设计模式的⼀种实现,并且都可以实现服务端渲染。需要注意⽬前 的Web前端开发和传统Model2需要模板引擎渲染的⽅式不同,通过Node启动服务进⾏⻚⾯渲染,并且通过代理的⽅式 转发请求后端数据,完全可以从后端的苦海中脱离,这样⼀来也可以⼤⼤的解放Web前端的⽣产⼒。

2. 观察者模式和发布/订阅模式

观察者模式

观察者模式是使⽤⼀个subject⽬标对象维持⼀系列依赖于它的observer观察者对象,将有关状态的任何变更 ⾃动通知给这⼀系列观察者对象。当subject⽬标对象需要告诉观察者发⽣了什么事情时,它会向观察者对象 们⼴播⼀个通知。

如图所示:⼀个或多个观察者对⽬标对象的 状态感兴趣时,可以将⾃⼰依附在⽬标对象 上以便注册感兴趣的⽬标对象的状态变化, ⽬标对象的状态发⽣改变就会发送⼀个通知 消息,调⽤每个观察者的更新⽅法。如果观 察者对⽬标对象的状态不感兴趣,也可以将 ⾃⼰从中分离。

在这里插入图片描述

发布/订阅模式

发布/订阅模式使⽤⼀个事件通道,这个通道介于订阅者和发布者之间,该设计模式允许代码定义应⽤程序的特定事 件,这些事件可以传递⾃定义参数,⾃定义参数包含订阅者需要的信息,采⽤事件通道可以避免发布者和订阅者之 间产⽣依赖关系。

在这里插入图片描述

两者的区别
  • 观察者模式:允许观察者实例对象(订阅者)执⾏适当的事 件处理程序来注册和接收⽬标实例对象(发布者)发出的通 知(即在观察者实例对象上注册update⽅法),使订阅者 和发布者之间产⽣了依赖关系,且没有事件通道。不存 在封装约束的单⼀对象,⽬标对象和观察者对象必须合 作才能维持约束。 观察者对象向订阅它们的对象发布其 感兴趣的事件。通信只能是单向的。
  • 发布/订阅模式:单⼀⽬标通常有很多观察者,有时⼀个 ⽬标的观察者是另⼀个观察者的⽬标。通信可以实现双 向。该模式存在不稳定性,发布者⽆法感知订阅者的状态。

算法

1.冒泡排序

参考文章 1.1 冒泡排序 | 菜鸟教程 (runoob.com)

(冒泡排序的写法有很多种,所以选择一种自己喜欢的就好)

 function bubbleSort(arr) {  
 let len = arr.length;
    for (let i = 0; i < len-1; i++) { 
        for (let j = 0; j < len-i-1; j++) {  
            if (arr[j] > arr[j + 1]) { 
                let temp = arr[j] 
                arr[j] = arr[j + 1] 
                arr[j + 1] = temp  
            } 
        } 
      } 
  return arr;
    }  
 

2、快速排序

参考文章:快速排序 | 菜鸟教程 (runoob.com)

快速排序(Quicksort)的Javascript实现 - 阮一峰的网络日志 (ruanyifeng.com)

很清晰的视频: https://www.bilibili.com/video/BV1at411T75o?from=search

var quickSort = function(arr) {
​
  if (arr.length <= 1) { return arr; }
​
  var pivotIndex = Math.floor(arr.length / 2);
​
  var pivot = arr.splice(pivotIndex, 1)[0];
​
  var left = [];
​
  var right = [];
​
  for (var i = 0; i < arr.length; i++){
​
    if (arr[i] < pivot) {
​
      left.push(arr[i]);
​
    } else {
​
      right.push(arr[i]);
​
    }
​
  }
​
  return quickSort(left).concat([pivot], quickSort(right));
​
};

3 是否回⽂

回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。例如,121 是回文,而 123 不是。

 var isPalindrome = function (x) {
      if (x < 0) {
        return false
      }
      var x2 = parseInt(x.toString().split('').reverse().join(''))
      return x2 === x ? true : false
    }
    isPalindrome(121)
    console.log(isPalindrome(121))

4 斐波那契数列

斐波那契数列又被称为黄金分割数列,指的是这样的一个数列:1,1,2,3,5,8,13,21,34…

// num1前⼀项  // num2当前项 
function fb(n, num1 = 1, num2 =  1) {  
    if(n == 0) 
        return 0 
    if (n <=  2) { 
        return num2 
    } else {  
        return fb(n - 1, num2, num1 +  num2) 
    }  
}  

5、去重

• 利⽤ ES6 set 关键字:

function unique(arr) { 
    return [...new Set(arr)];  
}  

• 利⽤ ES5 filter ⽅法:

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

6、instanceof

这个⼿写⼀定要懂原型及原型链。

  function myInstanceof(target,  origin) {  
    if (typeof target !== "object"  || target === null) 
        return  false;  
    if (typeof origin !==  "function")  
        throw new TypeError("origin  must be function");  
    let proto = Object.getPrototypeOf(target);  
    // 相当于 proto =  target.__proto__;  
    while (proto) {  
        if (proto === origin.prototype)  
            return  true;  
        proto =  Object.getPrototypeOf(proto);  
    }  
    return  false;  
​
  }  

7 数组扁平化

重点,不要觉得⽤不到就不管,这道题就是考察你对 js 语法的熟练程度以及⼿写代码的基本能⼒。

  function flat(arr, depth = 1) {   
    if (depth > 0) {    
    // 以下代码还可以简化,不过为了可读性    
        return arr.reduce((pre, cur) => {     
            return pre.concat(Array.isArray(cur) ?  flat(cur, depth - 1) : cur);    
            }, []);   
        }   
        return arr.slice();  
  }  

8 ⼿写 reduce

先不考虑第⼆个参数初始值:

  Array.prototype.reduce =  function (cb) {  
    const arr = this; //this就是调⽤reduce⽅法的数组   
    let total = arr[0]; // 默认为数组的第⼀项   
    for (let i = 1; i < arr.length; i++) {    
        total = cb(total, arr[i], i, arr);   
    }   
    return total;  
  };  

考虑上初始值:

  Array.prototype.reduce =  function (cb, initialValue) {  
    const arr = this;   
    let total = initialValue || arr[0];   // 有初始值的话从0遍历,否则从1遍历   
    for (let i = initialValue ? 0 : 1; i <  arr.length; i++) {    
        total = cb(total, arr[i], i, arr);   
    }   
    return total;  
  };  

面试题博客推荐

这两个网址是我常用的刷题的网站,都是些大佬做的

前端常见面试题总结 | 大厂面试题每日一题 (shanyue.tech)

web前端面试 - 面试官系列 (vue3js.cn)

资源

因为我习惯用ipad反复背题,所以整理了pdf版本、markDown版本和goodnotes版本(会不定时更新)需要的小伙伴可以评论区或者私信踢我

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值