高级前端面试最优解回答

704 阅读21分钟

基础是地基,是探究更深入内容的钥匙,是进阶之路上最重要的一环,需要每个开发者重视。在前端技术快速发展迭代的今天,在「前端市场是否饱和」,「前端求职火爆异常」,「前端入门简单,钱多人傻」等众说纷纭的浮躁环境下,对基础内功的修炼就显得尤为重要。这也是你在前端路上能走多远、走多久的关键。
从面试的角度看,面试题归根结底是对基础的考察,只有对基础烂熟于胸,才能具备突破面试的基本条件。

数组 reduce

数组方法非常重要:因为数组就是数据,数据就是状态,状态反应着视图。对数组的操作我们不能陌生,其中 reduce 方法更要做到驾轻就熟。我认为这个方法很好地体现了「函数式」理念,也是当前非常热门的考察点之一。

它的使用语法:

arr.reduce(callback[, initialValue])

这里我们简要介绍一下。

  • reduce 第一个参数 callback 是核心,它对数组的每一项进行「叠加加工」,其最后一次返回值将作为 reduce方法的最终返回值。 它包含 4 个参数:

    • previousValue 表示「上一次」 callback 函数的返回值
    • currentValue 数组遍历中正在处理的元素
    • currentIndex 可选,表示 currentValue 在数组中对应的索引。如果提供了 initialValue,则起始索引号为 0,否则为 1
    • array 可选,调用 reduce() 的数组
  • initialValue 可选,作为第一次调用 callback 时的第一个参数。如果没有提供 initialValue,那么数组中的第一个元素将作为  callback 的第一个参数。

求和

[1,2,3,4].reduce((acc, cur) => {
  return acc + cur
}, 10)
// 10 + 1 + 2 + 3 + 4
// 20

将二维数组转为一维数组

const testArr = [[1,2], [3,4], [5,6]]
testArr.reduce((acc, cur) => {
  return acc.concat(cur)
}, [])
// [1,2,3,4,5,6]

计算数组中每个元素出现的个数

const testArr = [1, 3, 4, 1, 3, 2, 9, 8, 5, 3, 2, 0, 12, 10]
testArr.reduce((acc, cur) => {
  if (!(cur in acc)) {
    acc[cur] = 1
  } else {
    acc[cur] += 1
  }
  return acc
}, {})

// {0: 2, 1: 3, 2: 3, 3: 4, 4: 2, 5: 2, 8: 2, 9: 2, 10: 2, 12: 2}

按属性给数组分类

什么叫按照属性给数组分类,其实就是给定一个依据,把符合条件的归并到一起。再拿账单举例,就是按各个消费类型归为一类。

const bills = [
  { type: 'shop', momey: 223 },
  { type: 'study', momey: 341 },
  { type: 'shop', momey: 821 },
  { type: 'transfer', momey: 821 },
  { type: 'study', momey: 821 }
];
bills.reduce((acc, cur) => {
  // 如果不存在这个键,则设置它赋值 [] 空数组
  if (!acc[cur.type]) {
    acc[cur.type] = [];
  }
  acc[cur.type].push(cur)
  return acc
}, {})

v2-3381df245cb2db2e513d6331b0ac45b5_1440w.jpeg

数组去重

这个就不解释了,直接上代码。

const testArr = [1,2,2,3,4,4,5,5,5,6,7]
testArr.reduce((acc, cur) => {
  if (!(acc.includes(cur))) {
    acc.push(cur)
  }
  return acc
}, [])
// [1, 2, 3, 4, 5, 6, 7]

reduce 实现 runPromiseInSequence

我们看它的一个典型应用,按顺序运行 Promise:

const runPromiseInSequence = (array, value) => array.reduce(
   (promiseChain, currentFunction) => promiseChain.then(currentFunction),
   Promise.resolve(value)
)

runPromiseInSequence 方法将会被一个每一项都返回一个 Promise 的数组调用,并且依次执行数组中的每一个 Promise,请读者仔细体会。如果觉得晦涩,可以参考示例:

const f1 = () => new Promise((resolve, reject) => {
   setTimeout(() => {
       console.log('p1 running')
       resolve(1)
   }, 1000)
})

const f2 = () => new Promise((resolve, reject) => {
   setTimeout(() => {
       console.log('p2 running')
       resolve(2)
   }, 1000)
})

const array = [f1, f2]

const runPromiseInSequence = (array, value) => array.reduce(
   (promiseChain, currentFunction) => promiseChain.then(currentFunction),
   Promise.resolve(value)
)

runPromiseInSequence(array, 'init')

执行结果如下图

WechatIMG443.png

reduce 实现 pipe函数颗粒化

reduce 的另外一个典型应用可以参考函数式方法 pipe 的实现:pipe(f, g, h) 是一个 curry 化函数,它返回一个新的函数,这个新的函数将会完成 (...args) => h(g(f(...args))) 的调用。即 pipe 方法返回的函数会接收一个参数,这个参数传递给 pipe 方法第一个参数,以供其调用

const pipe = (...functions) => input => functions.reduce(
   (acc, fn) => fn(acc),
   input
)

仔细体会 runPromiseInSequence 和 pipe 这两个方法,它们都是 reduce 应用的典型场景。

实现一个 reduce

那么我们该如何实现一个 reduce 呢?参考来自 MDN 的 polyfill:

if (!Array.prototype.reduce) {
  Object.defineProperty(Array.prototype, 'reduce', {
    value: function(callback /*, initialValue*/) {
      if (this === null) {
        throw new TypeError( 'Array.prototype.reduce ' + 
          'called on null or undefined' )
      }
      if (typeof callback !== 'function') {
        throw new TypeError( callback +
          ' is not a function')
      }
      var o = Object(this)
      var len = o.length >>> 0
      var k = 0
      var value

      if (arguments.length >= 2) {
        value = arguments[1]
      } else {
        while (k < len && !(k in o)) {
          k++
        }
        if (k >= len) {
          throw new TypeError( 'Reduce of empty array ' +
            'with no initial value' )
        }
        value = o[k++]
      }
      while (k < len) {
        if (k in o) {
          value = callback(value, o[k], k, o)
        }
        k++
      }

      return value
    }
  })
}

上述代码中使用了 value 作为初始值,并通过 while 循环,依次累加计算出 value 结果并输出。但是相比 MDN 上述实现,我个人更喜欢的实现方案是:

Array.prototype.reduce = Array.prototype.reduce || function(func, initialValue) {
   var arr = this
   var base = typeof initialValue === 'undefined' ? arr[0] : initialValue
   var startPoint = typeof initialValue === 'undefined' ? 1 : 0
   arr.slice(startPoint)
       .forEach(function(val, index) {
           base = func(base, val, index + startPoint, arr)
       })
   return base
}

核心原理就是使用 forEach 来代替 while 实现结果的累加,它们本质上是相同的。

我也同样看了下 ES5-shim 里的 pollyfill,跟上述思路完全一致。唯一的区别在于:我用了 forEach 迭代而 ES5-shim 使用的是简单的 for 循环。实际上,如果「杠精」一些,我们会指出数组的 forEach 方法也是 ES5 新增的。因此,用 ES5 的一个 API(forEach),去实现另外一个 ES5 的 API(reduce),这并没什么实际意义——这里的 pollyfill 就是在不兼容 ES5 的情况下,模拟的降级方案。此处不多做追究,因为根本目的还是希望读者对 reduce 有一个全面透彻的了解。

通过 Koa only 模块源码认识 reduce

通过了解并实现 reduce 方法,我们对它已经有了比较深入的认识。最后,再来看一个 reduce 使用示例——通过 Koa 源码的 only 模块,加深印象:

var o = {
   a: 'a',
   b: 'b',
   c: 'c'
}
only(o, ['a','b'])   // {a: 'a',  b: 'b'}

该方法返回一个经过指定筛选属性的新对象
only 模块实现

var only = function(obj, keys){
   obj = obj || {}
   if ('string' == typeof keys) keys = keys.split(/ +/)
   return keys.reduce(function(ret, key) {
       if (null == obj[key]) return ret
       ret[key] = obj[key]
       return ret
   }, {})
}

小小的 reduce 及其衍生场景有很多值得我们玩味、探究的地方。举一反三,活学活用是技术进阶的关键。

递归通过name找id

WechatIMG511.png

    let str = "";
    var getNameById = (tempData, id) => {
      for (let i = 0; i < tempData.length; i++) {
        let item = tempData[i];
        if (item.id == id) {
          str = item.name;
          return;
        }
        if (item.children.length > 0) {
          getNameById(item.children, id);
        }
      }
      return str;
    };
    console.log(getNameById(data, 4));

JavaScript 类型及其判断

JavaScript 具有七种内置数据类型,它们分别是:

基本类型:null undefined boolean number string object symbol
object: function、array、date

使用 typeof 判断类型

typeof适用于上面7种数据类型中除null、object的其他5种类型的判断

typeof undefined;  // "undefined";
typeof 1;          // "number";
typeof '1';        // "string";
typeof true;       // "boolean";
typeof Symbol();   // "symbol";
typeof  []    // "object";

原理: 实际上(typeof的其中一种实现),js底层储存变量时会在变量的机器码低位1-3位储存变量类型信息的。这几位机器码就是上文说到的“类型标签“;下面列举下几个类型的”类型标签“:

  • 000: 对象

  • 010: 浮点数

  • 100: 字符串

  • 110: 布尔值

  • 1: 整数 以上都是可靠正确而且易于理解的,接下来我们一起看几个正确的,但似乎不是我们期望的结果的例子:

  • typeof null === "object"; // true 原因:由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null的类型标签也成为了 0,typeof null就错误的返回了"object"。

var a = null;
!a && typeof a === 'object' // true
  • typeof function () {} === "function"; // true 因为,函数是一个“特殊的对象”,它特殊就特殊在它内部有一个属性[[call]],这个属性使它变成一个“可调用的对象”

使用 instanceof 判断类型

typeof能判断一个数据是不是对象,却不能判断它是对象的哪个“子类型”,比如说不能判断是否为数组,是否为日期等。这个时候就可以用instanceof来进行判断了。但是,instanceof只能判断是否是你预期的类型且只能用于对象子类型的判断

**使用 a instanceof B 判断的是:a 是否为 B 的实例,即 a 的原型链上是否存在 B 构造函数**。

var a = []
a.__proto__=Array.prototype

我们使用以下代码来模拟 instanceof 原理:

// L 表示左表达式,R 表示右表达式
const instanceofMock = (L, R) => {
   if (typeof L !== 'object') {
       return false
   }
   while (true) {
       if (L === null) {
           // 已经遍历到了最顶端
           return false
       }
       if (R.prototype === L.__proto__) {
           return true
       }
       L = L.__proto__
   }
}

使用 constructor

我们发现对于 undefined 和 null,如果尝试读取其 constructor 属性,将会进行报错。并且 constructor 返回的是构造函数本身,一般使用它来判断类型的情况并不多见。

Object.prototype.toString

使用 Object.prototype.toString 判断类型,我们称之为「万能方法」,「终极方法」

Object.prototype属性表示Object的原型对象;toString()方法返回一个表示该对象的字符串;call用于指定this。所以使用Object.prototype.toString.call(date)会返回一个表示date的原型对象的字符串(如果date不是对象,会先转化为对象,null和undefined除外)

JavaScript 类型判断总结

才能进行判断,所以在日常工作中应结合上面几种判断类型的方法,选择最简单且可靠的。

综上所述,当你想判断一个基本类型的数据时,你可以用typeof去判断,它很简单,而且可靠;当你想判断一个对象属于哪个子类型时,你可以使用instanceof运算符或constructor属性,但是你需要有个预期的类型,不然就要针对每一种类型写不一样的if...else...语句,还有一点需要注意的就是constructor属性可以被修改,所以并不可靠;如果你不嫌代码量多,要求准确且全面,那你可以用Object.prototype.toString.call()进行判断。

JavaScript 类型转换

console.log(1 + true)
// 2
console.log(1 + false)
// 1

结论 当使用 + 运算符计算时,如果存在复杂类型,那么复杂类型将会转换为基本类型,再进行运算

这就涉及到「对象类型转基本类型」这个过程。具体规则:

结论 对象在转换基本类型时,会调用该对象上 valueOf 或 toString 这两个方法,该方法的返回值是转换为基本类型的结果

那具体调用 valueOf 还是 toString 呢?这是 ES 规范所决定的,实际上这取决于内置的 toPrimitive 调用结果。主观上说,这个对象倾向于转换成什么,就会优先调用哪个方法。如果倾向于转换为 Number 类型,就优先调用 valueOf;如果倾向于转换为 String 类型,就只调用 toString。这里我建议大家了解一些常用的转换结果,对于其他特例情况会查找规范即可。

深入理解原型和原型链

彼此之间的关系

  • 构造函数中一属性prototype:指向原型对象
  • 原型对象一constructor属性,又指回了构造函数。
  • 每个构造函数生成的实例对象都有一个proto属性,这个属性指向原型对象。 20191024090640781.png

原型链是什么

顾名思义,肯定是一条链,既然每个对象都有一个_proto_属性指向原型对象,那么原型对象也有_proto_指向原型对象的原型对象,直到指向上图中的null,这才到达原型链的顶端。

原型链中Function与Object的关系

  • [^](#ref_1_0)或者说,Object 和 Function 都是 Function 类的实例。Function 的父类是 Object,Object 没有父类。

  • [^](#ref_2_0)即 A 是否是 B 类的实例。如果 B 中没有 Symbol.hasInstance 的干扰。 tc39.es/ecma262/#se…

Object.__proto__===Function.prototype 
Function.__proto__===Function.prototype
Array.__proto__===Function.prototype
Function.prototype.__proto__===Object.prototype
Array.prototype.__proto__===Object.prototype

Function.prototype.__proto__===Object.prototype

实现 new

function newFunc(...args) {
 // 取出 args 数组第一个参数,即目标构造函数
 const constructor = args.shift()

 // 创建一个空对象,且这个空对象继承构造函数的 prototype 属性
 // 即实现 obj.__proto__ === constructor.prototype
 const obj = Object.create(constructor.prototype)

 // 执行构造函数,得到构造函数返回结果
 // 注意这里我们使用 apply,将构造函数内的 this 指向为 obj
 const result = constructor.apply(obj, args)

 // 如果造函数执行后,返回结果是对象类型,就直接返回,否则返回 obj 对象
 return (typeof result === 'object' && result != null) ? result : obj
}

上述代码并不复杂,几个关键点:

  • 使用 Object.create 将 obj 的 __proto__ 指向为构造函数的原型
  • 使用 apply 方法,将构造函数内的 this 指向为 obj
  • 在 newFunc 返回时,使用三目运算符决定返回结果

JavaScript 中实现继承

  • 原型链实现继承最关键的要点是:

Child.prototype = new Parent()

  • 构造函数实现继承的要点是:

function Child (args) {  Parent.call(this, args) }

  • 组合继承的实现才基本可用,其要点是:
function Child (args1, args2) {
   // ...
   this.args2 = args2
   Parent.call(this, args1)
}
Child.prototype = new Parent()
Child.prototype.constrcutor = Child

它的问题在于 Child 实例会存在 Parent 的实例属性。因为我们在 Child 构造函数中执行了 Parent 构造函数。同时,Child.__proto__ 也会存在同样的 Parent 的实例属性,且所有 Child 实例的 __proto__ 指向同一内存地址。

数字去重的几种方案

return Array.from(new Set(arr))

var arr=[1,2,13,1,2,1,2,1]
var newArr = [];
for(var i=0;i<arr.length;i++){
    if(newArr.indexOf(arr[i]) == -1){
        newArr.push(arr[i]);
    }
}
console.log(newArr);
let newArr = arr.reduce((prev, cur) => {
    prev.indexOf(cur) === -1 && prev.push(cur);
    return prev;
},[]);
console.log(newArr)

为什么微任务比宏任务执行更早

  1. 宏任务:setTimeout、setInterval、Ajax、DOM事件
  2. 微任务:Promise、async/await
  3. 微任务比宏任务执行的更早 为什么微任务比宏任务执行更早: 因为触发时间不同
  • 宏任务:DOM渲染后触发,因为是浏览器规定的,在渲染时调用
  • 微任务:DOM渲染前触发,是ES6语法规定的,在编译时调用

栈内存和堆内存

为什么会有栈内存和堆内存之分?
当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(方法的参数传递时很常见),则这个对象依然不会被销毁,只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。

栈(操作系统):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈

栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放
堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。

堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些
堆(数据结构):堆可以被看成是一棵树,如:堆排序
栈(数据结构):一种后进先出的的数据结构

let为什么不能变量提升

  • var存在变量提升原因:在 foo 函数执行时,对于变量 b 的声明或读值情况是在其上层函数 bar 作用域中获取的。 同时「更上层作用域」也可以顺着作用域范围向外扩散,一直找到全局作用域,我们看到,变量作用域的查找是一个扩散过程,就像各个环节相扣的链条,逐次递进,这就是作用域链说法的由来。
  • let的话:因此在相应花括号形成的作用域中,存在一个「死区」,起始于函数开头,终止于相关变量声明的一行。

WechatIMG434.png

coust

首先,我们用const声明一个 “常量”,实际是创建了一个不可修改的指针,指向内存中一块区域。我们交给const“常量”的内容就在这块区域中。

const A = 1       // 值不可改变
const B = '1'     // 值不可改变
const C = true    // 值不可改变
const D = {d1: 1}      // 属性实际可以改变 例如 D.d1 = 2
const E = ['e1']      // item实际可以改变 例如 E[0] = 'e2'

由此可见,基础数据类型由于其值是存放在栈中的,修改值后实际内存地址也会变更,所以违反了const声明常量不可修改指针的原则。

而引用数据类型则不同,由于修改其属性不影响堆中对应地址变化,所以值可以改变。但是例如以下示例,实际修改了引用地址,则会运行错误:

const D = {d1: 1}
const E = ['e1']
D = {d1: 2}         // 错误
E = ['e2']          // 错误

防抖与节流

防抖

每次触发事件时都取消之前的延时调用方法

  debounce(fn) {
    let timeout = null
    return function () {
      clearTimeout(timeout)
      timeout = setTimeout(() => {
        fn.apply(this, arguments)
      }, 500)
    }
  },

节流

每次触发事件时都会判断当前是否有等待执行的延时函数

  throttle(fn) {
    let canRun = true // 通过闭包保存一个标记
    return function () {
      if (!canRun) return
      canRun = false
      setTimeout(() => {
        fn.apply(this, arguments)
        canRun = true
      }, 500)
    }
  }

浏览器缓存

WechatIMG481.png

强缓存

  1. expires,这是http1.0时的规范;它的值为一个绝对时间的GMT格式的时间字符串,如Mon, 10 Jun 2015 21:31:12 GMT,如果发送请求的时间在expires之前,那么本地缓存始终有效,否则就会发送请求到服务器来获取资源\

  2. cache-control:max-age=number,这是http1.1时出现的header信息,主要是利用该字段的max-age值来进行判断,它是一个相对值;资源第一次的请求时间和Cache-Control设定的有效期,计算出一个资源过期时间,再拿这个过期时间跟当前的请求时间比较,如果请求时间在过期时间之前,就能命中缓存,否则就不行;cache-control除了该字段外,还有下面几个比较常用的设置值:

    • no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。

    • no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。

    • public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。

    • private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。

  注意:如果cache-control与expires同时存在的话,cache-control的优先级高于expires

强缓存如何重新加载缓存缓存过的资源

需要为我们的静态资源添加md5 hash后缀,避免资源更新而引起的前后端文件无法同步的问题。 通过更新页面中引用的资源路径,让浏览器主动放弃缓存,加载新资源。

协商缓存

  • Last-Modified/If-Modified-Since 二者的值都是GMT格式的时间字符串,具体过程

    • 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上Last-Modified的header,这个header表示这个资源在服务器上的最后修改时间\

    • 浏览器再次跟服务器请求这个资源时,在request的header上加上If-Modified-Since的header,这个header的值就是上一次请求时返回的Last-Modified的值

    • 服务器再次收到资源请求时,根据浏览器传过来If-Modified-Since和资源在服务器上的最后修改时间判断资源是否有变化,如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源内容。当服务器返回304 Not Modified的响应时,response header中不会再添加Last-Modified的header,因为既然资源没有变化,那么Last-Modified也就不会改变,这是服务器返回304时的response header

    • 浏览器收到304的响应后,就会从缓存中加载资源

    • 如果协商缓存没有命中,浏览器直接从服务器加载资源时,Last-Modified的Header在重新加载的时候会被更新,下次请求时,If-Modified-Since会启用上次返回的Last-Modified值

  • Etag/If-None-Match 这两个值是由服务器生成的每个资源的唯一标识字符串,只要资源有变化就这个值就会改变;其判断过程与Last-Modified/If-Modified-Since类似,与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。

既生Last-Modified何生Etag

Last-Modified 与ETag****是可以一起使用的,服务器会优先验证ETag ,一致的情况下,才会继续比对Last-Modified ,最后才决定是否返回304

es6模块化

模块化是工程化的基础

概述

  1. CommonJS 和 AMD 两种:前者用于服务器,后者用于浏览器,主要是初试化时全部加载,缓存,在去查找对应的对象属性。不利于性能优化【初次不使用的方法属性也要加载】,不存在动态更新【初次缓存了,拿的是缓存的】
  2. es6的Module:运行时加载,可以采用webpack进行代码减掉。是动态的。完全取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。

ES 模块化为什么要设计成静态的

一个明显的优势是:通过静态分析,我们能够分析出导入的依赖。如果导入的模块没有被使用,我们便可以通过 tree shaking 等手段减少代码体积,进而提升运行性能。这就是基于 ESM 实现 tree shaking 的基础。

es6模块加载原理

<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>

由于type属性设为module,所以浏览器知道这是一个 ES6 模块。浏览器对于带有type="module"<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。

  • async属性:只要该模块加载完成,就执行该模块
  • defer属性:等到整个页面渲染完,再执行模块脚本

循环问题:写成函数来解决,具有提升作用

CommonJS 模块加载原理

CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。

{
  id: '...',   // 模块名
  exports: { ... }, // exports属性是模块输出的各个接口
  loaded: true,     // 该模块的脚本是否执行完毕
  ...
}

以后需要用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。

Node.js 的模块加载方法

CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容。语法上面,两者最明显的差异是,CommonJS 模块使用require()module.exports,ES6 模块使用importexport

.mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置。

未来趋势和思考

个人认为,ES 模块化是未来不可避免的发展趋势,它的优点毫无争议,比如开箱即用的 tree shaking 和未来浏览器兼容性支持。Node.js 的 CommonJS 模块化方案甚至也会慢慢过渡到 ES 模块化上。如果你正在使用 webpack 构建应用项目,那么 ES 模块化是首选;如果你的项目是一个前端库,也建议使用 ES 模块化。这么看来,或许只有在编写 Node.js 程序时,才需要考虑 CommonJS。

ES6 箭头函数 与 this指向

开发者习惯使用箭头函数来对 this 指向进行干预,那么反过来说,「不需要进行 this 指向干预的情况下,我们就不适合使用箭头函数」。总结下来,有:

闭包和垃圾回收机制

闭包之面试大型翻车现场

你不知道的promise

深入理解promise

JS事件冒泡和事件代理(委托)

事件冒泡

会从当前触发的事件目标一级一级往上传递,依次触发,直到document为止。

//如出发消息列表里的删除按钮,先执行了删除操作,在向上冒泡执行‘ 查看消息信息’。  
<body>
    <div id="parentId"> 
        查看消息信息
        <div id="childId1"> 删除消息信息 </div>
    </div>
</body>
<script>
    let parent = document.getElementById('parentId');
    let childId1 = document.getElementById('childId1');
    parent.addEventListener('click', function () {
        alert('查看消息信息');
    }, false);
    childId1.addEventListener('click', function () {
        alert('删除消息信息');
    }, false); 
    // 打印:删除消息信息 查看消息信息
</script>

取消事件冒泡

    try {
        e.stopPropagation(); //非IE浏览器
    } catch (e) {
        window.event.cancelBubble = true; //IE浏览器
    }
    <div @click.stop="doSomething($event)">vue取消事件冒泡</div>

事件代理(委托)

使用原因

比如ul下有100个li,用for循环遍历所有的li,然后给它们添加事件,需要不断的与dom节点进行交互,访问dom的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少DOM操作的原因. 如果要用事件委托,就会将所有的操作放到js程序里面,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能;

使用原理

事件委托:利用事件冒泡的特性,将本应该注册在子元素上的处理事件注册在父元素上,这样点击子元素时发现其本身没有相应事件就到父元素上寻找作出相应。这样做的优势有:

  1. 减少DOM操作,提高性能。
  2. 随时可以添加子元素,添加的子元素会自动有相应的处理事件。
<div id="box">
    <input type="button" id="add" value="添加" />
    <input type="button" id="remove" value="删除" />
    <input type="button" id="move" value="移动" />
    <input type="button" id="select" value="选择" />
</div>  
    // 方式一:需要4次dom操作
    window.onload = function () {
        var Add = document.getElementById("add");
        var Remove = document.getElementById("remove");
        var Move = document.getElementById("move");
        var Select = document.getElementById("select");
        Add.onclick = function () {
            alert('添加');
        };
        Remove.onclick = function () {
            alert('删除');
        };
        Move.onclick = function () {
            alert('移动');
        };
        Select.onclick = function () {
            alert('选择');
        }
    }
    // 方式二:委托它们父级代为执行事件
    window.onload = function () {
        var oBox = document.getElementById("box");
        oBox.onclick = function (ev) {
            var ev = ev || window.event;
            var target = ev.target || ev.srcElement;
            if (target.nodeName.toLocaleLowerCase() == 'input') {
                switch (target.id) {
                    case 'add':
                        alert('添加');
                        break;
                    case 'remove':
                        alert('删除');
                        break;
                    case 'move':
                        alert('移动');
                        break;
                    case 'select':
                        alert('选择');
                        break;
                }
            }
        }

    }
    // 用事件委托就可以只用一次dom操作就能完成所有的效果,比上面的性能肯定是要好一些的

事件捕获

会从document开始触发,一级一级往下传递,依次触发,直到真正事件目标为止。

    <div> 
        <button>
            <p>点击捕获</p>
        </button>
    </div>
    <script>
        var oP = document.querySelector('p');
        var oB = document.querySelector('button');
        var oD = document.querySelector('div');
        var oBody = document.querySelector('body');
        oP.addEventListener('click', function () {
            console.log('p标签被点击')
        }, true);
        oB.addEventListener('click', function () {
            console.log("button被点击")
        }, true);
        oD.addEventListener('click', function () {
            console.log('div被点击')
        }, true);
        oBody.addEventListener('click', function () {
            console.log('body被点击')
        }, true);
    </script> 
    点击<p>点击捕获</p>,打印的顺序是:body=>div=>button=>p

流程:先捕获,然后处理,然后再冒泡出去。

深入浅出模块化(含 tree shaking)

深入浅出模块化(含 tree shaking