# JavaScript

# 1. var & let & const 和变量提升

  • 变量提升:准确的说,它们都会存在变量提升,在JS编译的时候会把声明语句拆分为声明语句和赋值语句,声明语句先执行,只不过对于letconst而言,声明语句到赋值语句之间的这部分代码块是不用使用该变量的,称为暂存死区;而var声明的变量,在未运行到赋值代码之前,值为undefined

  • var可以重复声明,var是函数作用域,letconst是块级作用域

    foo = 18;
    var foo;
    console.log(foo);
    
    // 输出18,变量提升
    
    foo = 18;
    let foo;
    console.log(foo);
    
    // ReferenceError 暂存死区内不可使用变量
    
  • 函数:函数声明会被提升,并且优先于变量声明,但是函数表达式不会提升。

    foo();
    
    var foo;
    
    function foo() {
      console.log(1)
    }
    
    function foo() {
      console.log(3);
    }
    
    foo = function() {
      console.log(2)
    }
    
    foo()
    
    // 3
    // 2
    
    // 两个foo的声明都被提升,并且第二个覆盖了第一个
    // 重复的foo=function...的声明被忽略,但仍然在第二次调用foo前生效
    

# 2. 八大数据类型

# 2.1 概念

  • 原始数据类型(7):Boolean、Null、Undefined、Number、BigInt、String、Symbol

  • 引用类型(1):Object

  • 其他的引用类型(5):Array、Function、Date、RegExp、Set

  • Polyfill是一块代码(通常是 Web 上的 JavaScript),用来为旧浏览器提供它没有原生支持的较新的功能。

  • Object和Map的区别

    方面 Map Object
    结构 纯哈希结构 不是,拥有自己的内部逻辑
    键的类型 任意,可以是函数、对象或者任意基本类型 必须是String类型或者Symbol类型
    键的顺序 key是按照插入的顺序保存的 无序的(也不完全是无序,不同类型不一样)
    Size .size获取 键值对的个数只能手动获取
    迭代 可以直接被迭代 迭代键,来获取值

# 2.2 为什么引用数据类型会变化呢?

  • 基本数据类型的变量的值是存储在调用栈中,可以直接在调用栈中改变基本数据类型的变量的值。
  • 引用数据类型的变量也是存储在调用栈中的,但是这个值是一个地址,该地址对应的内存空间中的位置才是存储的该引用数据类型的变量的真正的值,这些值是存储在堆空间中的,所以对这一个值进行修改,其他的引用得到的结果也发生了同步变化。

# 2.3 nullundefined的区别

  • 含义:
    • null:“没有对象”,此处不应该有具体值,应该为空
    • undefined:“缺少值”,此处应该有值,但是还没有被定义
  • 类型:
    • typeof null === 'object'
    • typeof undefined = 'undefined'
  • 数值:
    • Number(null) === 0
    • Number(undefined) === NaN,意为“Not a Number”

# 3. 闭包

# 3.1 产生闭包:

  • 预扫描内部函数
  • 把内部函数引用的外部变量保存到堆中(若无闭包,本来应该保存在栈中)
  • 其实闭包也可以理解为:在调用栈中,函数执行完毕之后,函数的执行上下文从栈顶弹出,但是留下了词法作用域中被其内部函数所引用的变量。

# 3.2 概念

在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。

// 补充一个例子
for (let i = 0; i < 18; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
};

// 正常输出了 0 ~ 17
// 需要注意的是,for循环头部的let声明会有一个特殊的行为:即该变量在循环过程中不止被声明一次,每次迭代都会声明

# 3.3 应用场景

# 3.3.1 setTimeout(还有就是防抖、节流)

for (var i = 0; i < 5; i++) {
    (function(j) {  // j = i
        setTimeout(function() {
            console.log(new Date, j);
        }, 1000);
    })(i);
}

console.log(new Date, i);

# 3.3.2 工厂函数

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;

// 生产不同的函数,这样为不同的按钮添加点击事件处理程序

# 3.3.3 私有变量

// 私有变量:函数中定义的变量
function MyObject() {
  // 私有变量和私有函数
  var privateVariable = 10;
  
  function privateFunction() {
    return false;
  }
  
  // 特权方法
  this.publicMethod = function (){
    privateVariable++;
    return privateFunction();
  };
}

let obj1 = new MyObject();
let obj2 = new MyObject();
// obj1 与 obj2 并不共享私有变量

// 另一个浅显的例子
function Fun(){
  var name = 'tom';
  
  this.getName = function (){
    return name;
  }
}

var fun = new Fun(); 
console.log(fun.name); //输出undefined,在外部无法直接访问name
console.log(fun.getName()); //可以通过特定方法去访问

// 对象属性有两种:数据属性,访问器属性,访问器属性就有点像是这个
// 数据属性:configurable、enumerable、writable、value
// 访问器属性:configurable、enumerate、get、set

# 3.3.4 单例模式

var singleton = function() {
  // 私有变量和私有函数
  var privateVariable = 10;
  function privateFunction() {
    return false;
  }
  
  // 特权/公有方法和属性
  return {
    publicProperty: true,
    publicMethod: function() {
      privateVariable++;
      return privateFunction();
    }
  };
}();

// 使用一个返回对象的匿名函数,在这个匿名函数内部,首先定义了私有变量和函数。
// 然后,将一个对象字面量作为函数的返回值返回。
// 返回的对象字面量中只包含可以公开的属性和方法

# 3.3.5 柯里化

sum(2, 2, 3)(4, 5)(5).toValue() === 21

function sum() {
  let args = [...arguments];
  let fn = function () {
    args.push(...arguments);
    return fn;
  };
  fn.toValue = function () {
    return args.reduce((x, y) => x + y);
  };
  return fn;
}

let ans = sum(2, 2, 3)(4, 5)(5);
console.log(ans.toValue());

// 什么是柯里化?将函数与传递给函数的参数结合在一起,产生出一个新的函数(有点工厂函数的意思?)
Function.method('curry', function() {
  let args = arguments;
  let that = this;
  return function() {
    return that.apply(null, args.concat(arguments));
  };
});

# 4. setTimeout & setInterval

# 4.1 Chrome实现setTimeout

chrome维护两个队列:1. 正常的消息队列 2. 另一个保存需要延迟执行的消息队列

  • 当执行setTimeout的时候,设置一个定时器,然后继续执行后面的代码
  • 定时器时间到,将setTimeout中的任务放到保存延迟执行任务的消息队列中,但并不立即执行
  • 当目前js引擎正在处理的任务结束时,才从延迟执行任务的消息队列中取出任务,进行执行

# 4.2 setTimeout的注意点

  1. 如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒,因为在 Chrome 中,定时器被嵌套调用 5 次以上,系统会判断该函数方法被阻塞了。
  2. 未激活的页面,setTimeout执行的最小间隔是1000毫秒,目的是为了优化后台页面的加载损耗以及降低耗电量。
  3. 延迟执行时间有最大值,Chrome、Safari、Firefox 都是以 32 个 bit 来存储延时值的,32bit 最大只能存放的数字是 2147483647 毫秒(即24.8天)。

# 4.3 setInterval

  1. 机制:首先创建一个delay的定时器,然后每隔delay的时间,(在任务队列中没有定时器中的代码实例的时候)向任务队列的尾部加入一个定时器中代码的实例func,等到func前面的任务都被执行完毕,再执行func
  2. 目标:希望func任务能够以delay的时间间隔执行。(这里并不关心上一个任务结束与下一个任务开始之间的时间间隔)
  3. 注意:setInterval()仅当任务队列中没有定时器的任何其他代码实例时,才会将定时器代码加到队列中。
    • 多个定时器的代码执行之间的间隔可能会比预期的小:第二个定时器的任务已经被加入到队列了,第一个定时器的任务还在执行
    • 某些间隔会被跳过:第三个定时器都到表了,准备向队列中添加第三个定时器任务了,第一个定时器的任务还在执行

# 5. promise

# 5.1 概念

  • 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
  • 从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
  • 三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)
  • JS异步编程的六种方案:
    • 回调函数callback
    • 事件监听 on / addEventListener
    • 发布-订阅者模式 / 观察者模式
    • Promise/ A+
    • 生成器 Generators / yield
    • Async / Await

# 5.2 一些特性和 Promise/A+

# 5.2.1 特性

  • Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被onReject函数处理或catch语句捕获为止。
  • onFulfilledonRejected都是微任务,并且这两个函数都只能执行一次,但是then可以多次执行。
  • resolvereject函数应该不是setTimeout来实现异步的,虽然手写是这么实现的。
  • 还是没太明白:resolve是异步,reject接收到一个reject状态的promise的时候,不是异步,遇到then的时候会立即执行?并且以接收到的这个reject状态的promise作为参数调用自己的reject()函数。例子见 5.11 习题十一

# 5.2.2 Promise/A+

  • then必须返回一个 promise,即promise2 = promise1.then(onFulfilled, onRejected);
    • 不论promise1reject还是resolve,promise2都会被resolve,只有出现异常promise2才会被reject
    • onFulfilledonRejected如果出现了错误(抛出异常,非正常执行完毕),都会返回一个 rejected promise 新实例。
  • Promise 解决过程 是一个抽象的操作,其需输入一个 promise 和一个值,我们表示为 [[Resolve]](promise, x)
    • 如果 x 是一个值,则其用 x 的值来执行 promise
    • 如果 x 是函数或者对象,递归尝试x.then()(或者说,返回的 Promise 对象会“跟随”这个 thenable 的对象,采用它的最终状态(指 resolved/rejected/pending/settled))
    • 如果 xpromise 指向同一对象,以TypeError为据因,拒绝执行promiseTypeError: Chaining cycle detected for promise
    • 如果 xthen 方法且看上去像一个 Promise ,解决程序即尝试使 promise 接受 x 的状态;
      • 如果 x 处于等待态, promise 需保持为等待态直至 x 被执行或拒绝
      • 如果 x 处于执行态,用相同的值执行 promise
      • 如果 x 处于拒绝态,用相同的据因拒绝 promise

# 5.3 promise 的返回值

resolvereject两个函数只会有一个被调用,函数的返回值将被用作创建then返回的Promise对象。这两个参数的返回值可以是以下三种情况中的一种:

  • return 一个同步的值 ,或者 undefined(当没有返回一个有效值时,默认返回undefined),then方法将返回一个resolved状态的Promise对象,Promise对象的值就是这个返回值。
  • return 另一个 Promise,then方法将根据这个Promise的状态和值创建一个新的Promise对象返回。
  • throw 一个同步异常,then方法将返回一个rejected状态的Promise, 值是该异常。

# 5.4 API

# 5.4.1 promise.resolve(...)

  • 该方法返回一个以给定值解析后的Promise对象:
    • 当参数是一个Promise对象时,它直接返回这个Promise
    • 当参数是普通值时,它返回一个resolved状态的Promise对象,对象的值就是这个参数。
  • 即使executor函数中包含了同步执行的resolve(p1),但通过new的方式创建的Promise对象都是一个新的对象。

# 5.4.2 promise.reject(reason)

  • 该方法返回一个带有拒绝原因的Promise对象。

# 5.4.3 promise.race(iterable)

  • 多个Promise任务同时执行,返回最先执行结束的Promise任务的结果,不管这个Promise结果是成功还是失败。

# 5.4.4 promise.any(iterable)

  • 接收一个promise可迭代对象,只要其中的一个 promise 完成,就返回那个已经有完成值的 promise
  • 如果可迭代对象中没有一个 promise 完成(即所有的 promises 都失败/拒绝),就返回一个拒绝的 promise,返回值还有待商榷。

# 5.4.5 promise.all(iterable)

  • 这个方法返回一个新的promise对象,该promise对象在iterable参数对象里所有的promise对象都成功的时候才会触发成功。
  • 一旦有任何一个iterable里面的promise对象失败则立即触发该promise对象的失败,失败的原因是第一个失败promise 的结果。

# 5.4 promise 执行时序总结

  • 前一个promise如果返回了新的promise,则后面的promise要等待其结果,并根据前面的promise创建一个新的promise(与前面这个promise的状态相同)返回。
  • 不管创建promise实例时还是创建完promise实例后,只要执行了resolve()或者reject()就会改变promise的状态,并且这个状态只能改变一次。
  • pending状态下,then只是将传给then的参数(一个函数)推送到该promise的回调队列中,但是并不执行。
  • 两个链式调用的then,第二个then中的代码需要等待第一个then中的同步代码执行完毕才能执行。
  • 两个同步调用的then,是顺序(几乎同时)执行的,第一个then在等待回调的时候,就进入执行第二个then,二者交替执行。
  • 猜测:resolved状态下,then的参数(一个函数)应该是直接被放倒了微任务队列中,因此then可以多次执行
  • 猜测:执行resolve的时候会检查该promise的回调队列中有没有未执行的任务,有则放入微任务队列中。
  • 疑惑:我不确定then注册的时候是不是直接注册的,即同时放入微任务队列

# 5.5 实现一个简单的 promise

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

function MyPromise(executor) {
  const self = this;
  self.value = null;
  self.error = null;
  self.status = PENDING;
  self.onFulfilledCallbacks = [];
  self.onRejectedCallbacks = [];

  function resolve(value) {
    if (value instanceof MyPromise) {
      return value.then(resolve, reject);
    }
    if (self.status === PENDING) {
      setTimeout(() => {
        self.status = FULFILLED;
        self.value = value;
        self.onFulfilledCallbacks.forEach(callback => callback(self.value));
      }, 0);
    }
  }

  function reject(error) {
    if (self.status === PENDING) {
      setTimeout(function () {
        self.status = REJECTED;
        self.error = error;
        self.onRejectedCallbacks.forEach(callback => callback(self.error));
      }, 0);
    }
  }
  
  try {
    executor(resolve, reject);
  } catch (e) {
    reject(e);
  }
}

MyPromise.prototype.then = function (onFulfilled, onRejected) {
  if (this.status === PENDING) {
    this.onFulfilledCallbacks.push(onFulfilled);
    this.onRejectedCallbacks.push(onRejected);
  } else if (this.status === FULFILLED) {
    onFulfilled(this.value);
  } else {
    onRejected(this.error);
  }
  return this;
};

MyPromise.prototype.catch = function (onRejected) {
  return this.then(null, onRejected);
};


// 但这个 promise 还有几个缺陷:

// 1.then 方法返回的是 this,本应该返回一个新的 promise 实例。因此,不支持串行异步任务
// 诸如:串行异步任务:p.then(f1).then(f2).then(f3).catch(errorLog),分别读取文件 f1、f2、f3 并且在读取完之后打印
// 但这个会一直输出 f1、f1、f1,因为这几个任务都放到了 p1 的回调队列 onFulfilledCallbacks 里面

// 2.一个状态为 resolve / reject 的 promise,在调用 then 的时候,也不应该被立即执行

// 3.在代码执行了函数 resolve() / reject() 之后,手写代码并没有马上将状态改为 resolved / rejected,而是等待 resolve() / reject() 的回调函数执行了之后,才更改状态,而在真正的 promise 中,是立即改变的。
// 终极版
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

function MyPromise(fn) {
  const self = this;
  self.value = null;
  self.error = null;
  self.status = PENDING;
  self.onFulfilledCallbacks = [];
  self.onRejectedCallbacks = [];

  function resolve(value) {
    if (value instanceof MyPromise) {
      return value.then(resolve, reject);
    }
    if (self.status === PENDING) {
      setTimeout(() => {
        self.status = FULFILLED;
        self.value = value;
        self.onFulfilledCallbacks.forEach((callback) => callback(self.value));
      }, 0);
    }
  }

  function reject(error) {
    if (self.status === PENDING) {
      setTimeout(function () {
        self.status = REJECTED;
        self.error = error;
        self.onRejectedCallbacks.forEach((callback) => callback(self.error));
      }, 0);
    }
  }

  try {
    fn(resolve, reject);
  } catch (e) {
    reject(e);
  }
}

function resolvePromise(bridgepromise, x, resolve, reject) {
  if (bridgepromise === x) {
    return reject(new TypeError('Circular reference'));
  }

  let called = false;
  if (x instanceof MyPromise) {
    if (x.status === PENDING) {
      x.then(y => {
        resolvePromise(bridgepromise, y, resolve, reject);
      }, error => {
        reject(error);
      });
    } else {
      x.then(resolve, reject);
    }
  } else if (x != null && ((typeof x === 'object') || (typeof x === 'function'))) {
    try {
      let then = x.then;
      if (typeof then === 'function') {
        then.call(x, y => {
          if (called) return;
          called = true;
          resolvePromise(bridgepromise, y, resolve, reject);
        }, error => {
          if (called) return;
          called = true;
          reject(error);
        });
      } else {
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    resolve(x);
  }
}

MyPromise.prototype.then = function (onFulfilled, onRejected) {
  const self = this;
  let bridgePromise;
  onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
  onRejected = typeof onRejected === "function" ? onRejected : error => {
    throw error;
  };
  if (self.status === FULFILLED) {
    return bridgePromise = new MyPromise((resolve, reject) => {
      setTimeout(() => {
        try {
          let x = onFulfilled(self.value);
          resolvePromise(bridgePromise, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      }, 0);
    });
  }
  if (self.status === REJECTED) {
    return bridgePromise = new MyPromise((resolve, reject) => {
      setTimeout(() => {
        try {
          let x = onRejected(self.error);
          resolvePromise(bridgePromise, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      }, 0);
    });
  }
  if (self.status === PENDING) {
    return bridgePromise = new MyPromise((resolve, reject) => {
      self.onFulfilledCallbacks.push((value) => {
        try {
          let x = onFulfilled(value);
          resolvePromise(bridgePromise, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      });
      self.onRejectedCallbacks.push((error) => {
        try {
          let x = onRejected(error);
          resolvePromise(bridgePromise, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      });
    });
  }
};
MyPromise.prototype.catch = function (onRejected) {
  return this.then(null, onRejected);
};

MyPromise.deferred = function () {
  let defer = {};
  defer.promise = new MyPromise((resolve, reject) => {
    defer.resolve = resolve;
    defer.reject = reject;
  });
  return defer;
};
try {
  module.exports = MyPromise;
} catch (e) {
}

# 5.6 手写实现串行异步任务的思路

  • **let promise2 = new Promise(...):**then 不再返回自身实例,而是一个新的promise对象,衔接后续操作
  • **resolvePromise(promise2, x, resolve, reject):**一个函数,用来解析回调函数的返回值x,x可能是普通值也可能是个promise对象,同时还可能等于promise2,即x === promise2,此时应当 reject promise with a TypeError
  • then中通过调用resolvePromise方法,判断当前调用resolve(value)的返回值是一个普通值还是promise对象:
    • 如果是promise对象:
      • 如果为pending状态,则继续往下走,继续执行resolvePromise判断这个promsie的返回值是普通值还是promise对象。
      • 否则,执行x.then(resolve, reject)
    • 否则,是正常值,则执行resolve(x)
  • 其实我自己的理解就是:判断当前promise中的resolve的onFulfilled函数返回的是不是一个promise对象,如果是返回的是一个promise并且该对象为pending状态,则在这个promise里面解析它的结果。本质上应该是是切换了不同的执行上下文,让当前的this指向不同阶段的promise实例,并将传给 then() 的参数 onFulfilled 和 onRejected 函数,push 到对应的 promise实例的 callback 队列中,避免了所有的回调函数都被 push 到最开始的那个promise实例中,也就是相当于这些回调函数,都被这个最开始的promise执行,导致每次的执行结果都一样。

# 6.对象

# 6.1 属性类型

# 6.1.1 数据属性

var person = {
  name: "range"
}

# 6.1.2 访问器属性

// 访问器属性不包含数据值,它们包含一对儿getter和setter函数,都是可选的
var book = {
  _year: 2004,
  edition: 1
}

// 访问器属性不能直接定义,必须使用 Object.defineProperty()来定义
// 这里就定义了一个 year 的访问器属性
Object.defineProperty(book, "year", {
  get: function() {
    return this._year;
  },
  set: function(newValue) {
    if (newValue > 2004) {
      this._year = newValue;
      this.edition += newValue - 2004;
    }
  }
});

# 6.2 创建对象

  • 工厂模式
  • 构造函数模式
  • 原型模式
  • 组合模式
  • 动态原型模式
  • 寄生构造函数模式
  • 稳妥构造函数模式

# 6.2.1 工厂模式

但是工厂模式没有解决对象识别的问题(即怎么样知道一个对象的类型)

function createPerson(name, age, job) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function() {
    alert(this.name);
  };
  return o;
}

let person = create('range', 24, 'foolish');

# 6.2.2 构造函数模式

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {
    alert(this.name);
  }
}
// 步骤
// 1.创建一个新对象
// 2.将构造函数的作用域赋给新对象,因此 this 就指向了这个新对象
// 3.执行构造函数中的代码,为这个新对象添加属性
// 4.返回新对象

// 但是每次都要生成一个新的 function 对象,就是 sayName指向的那个
// 不同的实例中的 sayName 不是同一个,它们都是该 function 的一个实例

# 6.2.3 原型模式

function Person() {}

Person.prototype.name = "Range";
Person.prototype.age = 24;
Person.prototype.job = "Student";
Person.prototype.sayName() = function() {
  alert(this.name);
}

// 或者
Person.prototype = {
  name: "Range",
  age: 24,
  job: "Student",
  sayName: function() {
    alert(this.name);
  }
};

# 6.2.4 组合模式(构造函数模式 + 原型模式)

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
}

Person.prototype = {
  constructor: Person,
  sayName: function() {
    console.log(this.name)
  }
}

# 6.2.5 动态原型模式

function Person(name) {
    this.name = name;
    if (typeof this.getName != "function") {
        Person.prototype.getName = function () {
            console.log(this.name);
        }
    }
}

var person1 = new Person();

注意:使用动态原型模式时,不能用对象字面量重写原型

function Person(name) {
    this.name = name;
    if (typeof this.getName !== "function") {
        Person.prototype = {
          // 这里使用了对象字面量进行赋值
            constructor: Person,
            getName: function () {
                console.log(this.name);
            }
        }
    }
}

// 使用字面量进行赋值,相当于将一个新的地址传给了 Person.prototype
// 但是 new 的过程:
// 1.新建一个对象obj
// 2.将obj的__proto__ 指向 Person.prototype
// 3.Person.apply(obj)
// 4.return obj
// 注意第二步,这里传入的是 Person.prototype 的地址
// 所以第三步里面,是给 Person.prototype 赋值了一个新的对象,传入的是地址,而没有改变原先的地址中的值
// 所以这里,obj 仍然指向的是原先的 Person.prototype的地址,也就是说,没有发生对应的改变

# 6.2.6 寄生构造函数模式

function Person(name) {
  var o = {};
  o.name = name;
  o.getName = function() {
    console.log(this.name);
  };
  
  return o
}

// 寄生-在-构造函数-的模式
// 在构造函数中创建新的对象,并且返回
// 实例与构造函数没有什么关系

let person = new Person('range');

// 注意寄生构造函数模式与工厂模式在构造函数上并没有区别
// 区别在于实例化的时候,寄生构造函数使用了 new
// 而工厂模式则直接使用了 构造函数返回的对象。

# 6.2.7 稳妥构造函数模式

function Person(name, age, job) {
  var o = new Object();
  o.sayName = function() {
    alert(name);
  };
  
  return o;
}

var friend = Person("range", 29, "Student");
friend.sayName(); // "range"

// 利用闭包在内存中保留了传入的 name 属性
// 新创建对象的实例不引用 this
// 不使用 new 操作符调用构造函数

# 7. 继承

# 7.1 原型

image

# 7.2 继承

  • 原型链继承
  • 借用构造继承
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承
  • ES6类继承
// 定义一个动物类
function Animal(name) {
  this.name = name || 'Animal';
  this.sleep = function() {
    console.log(this.name + ' is sleeping.');
  };
};

// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + ' is eating ' + food);
};

let cat = new Animal('cat');
cat.sleep()  // cat is sleeping.

# 7.2.1 原型链继承

function Cat() {}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
Cat.prototype.constructor = Cat;

let cat = new Cat();
cat.sleep()

// 引用类型的属性被所有实例共享
// 在创建子类型的实例的时候(就是 let cat = new Cat()的时候),不能向超类型的构造函数中传递参数
// 注意这里还要修改 prototype 的 constructor

# 7.2.2 借用构造继承

function Cat(name) {
  Animal.call(this);
  this.name = name || 'Tom';
  // Animal.call(this, name);
}

let cat = new Cat();
cat.sleeping()

// 避免了引用类型的属性被所有实例共享
// 只能继承父类实例的属性和方法,不能继承父类原型上的属性和方法

# 7.2.3 组合继承

function Cat(name) {
  Animal.call(this);
  this.name = name || "Tom";
  // Animal.call(this, name);
}

Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

// 优点:可以继承实例属性/方法,也可以继承原型属性/方法
// 缺点:需要调用两次超类型构造函数
// 第一次:创建子类型原型的时候,就是上面的 new Animal()
// 第二次:子类型构造函数的内部,就是上面的 Animal.call(this);

# 7.2.4 原型式继承

function object(obj) {
  // 先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型
  // 最后返回这个临时类型的一个新实例。
  // 其中,object() 对传入其中的对象执行了一次浅拷贝
  function F(){}
  F.prototype = obj;
  return new F();
}

// 注意这里通过类似 let f = object(obj) 形式实例化出来的 f 是没有构造函数(即constructor这个属性的)
// 在 F.prototype = obj 这句之后,新构造的 new F() 的 constructor 就不是 F 了

// 这个 person 其实就是作为一个我们可以看得见的 prototype
var person = {
  name: "A",
  friends: ["B", "C"]
}

var anotherPerson = object(person);
anotherPerson.name = "D";
anotherPerson.friends.push("E");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "F";
yetAnotherPerson.friends.push("G");

console.log(person.friends); //  BCEG

// ES5 通过新增 Object.create() 方法规范化了原型式继承
var person = {
  name: "A",
  friends: ["B", "C"]
}

var anotherPerson = Object.create(person);
anotherPerson.name = "D";
anotherPerson.friends.push("E");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "F";
yetAnotherPerson.friends.push("G");

console.log(person.friends); //  BCEG

# 7.2.5 寄生式继承

function createObj(obj) {
  var clone = Object.create(obj);
  clone.sayName = function() {
    console.log('Hey.')
  }
  return clone;
}

// 创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来克隆、增强对象,最后返回克隆的增强的对象

# 7.2.6 寄生组合式继承

function inheritPrototype(subType, superType) {
    var prototype = Object.create(superType.prototype)
    prototype.constructor = subType
    subType.prototype = prototype
}

function SuperType(name) {
  this.name = name;
}

function SubType(name) {
    SuperType.call(this,name)
}

inheritPrototype(SubType, SuperType);

// 只调用了一次超类 superType 的构造函数,避免了在 subType.prototype 上面创建不必要的多余的属性
// 是引用类型最理想的继承范式

# 7.2.7 ES6类继承

class Polygon {
  constructor(height, width) {
    this.name = 'Polygon';
    this.height = height;
    this.width = width;
  }
}

class Square extends Polygon {
  constructor(length) {
    super(length, length);
    this.name = 'Square';
  }
  
  // Getter
  get area() {
    return this.calcArea();
  }
  
  // Method
  calcArea() {
    return this.height * this.width;
  }
}

const square = new Square(10);
console.log(square.area); // 100,注意这里不是调用函数

ES5继承和ES6继承的区别:

  • ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.call(this)).
  • ES6的继承有所不同,实质上是先创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错。

# 8. DOM事件流

# 8.1 事件流

当一个HTML元素触发一个事件时,该事件会在元素结点与根结点之间的路径传播。传播按顺序分为三个阶段:捕获阶段、目标阶段、冒泡阶段,这个传播过程就是 DOM 事件流。

  1. 捕获阶段:从根节点window到目标节点
  2. 目标阶段:目标节点上的事件触发按照代码执行顺序触发
  3. 冒泡阶段:从目标节点到根节点

常用事件:

  • onclick:点击
  • onmouseover/onmouseout:鼠标经过/离开
  • onchange:文本框内容改变
  • onselect:文本框内容被选中
  • onfocuse/onblur:光标聚集/离开
  • onload/onunload:网页导入(图片、样式、脚本都加载完毕)/关闭

# 8.2 事件处理程序

  • HTML 事件处理程序
  • DOM 0 级事件处理程序
  • DOM 2 级事件处理程序
  • IE 事件处理程序
  • 跨浏览器的事件处理程序

# 8.3.1 HTML 事件处理程序

<!-- 用一个与该事件处理程序同名的HTML特性来指定。-->
<input type="button" value="Click me" onclick="alert(‘Clicked’)"/>
<input type="button" value="Click me" onclick="alert(&quot;success&quot;)"/>
<input type="button" value="Click me" onclick="showMessage()"/>

<script>
  function showMessage() {
    console.log("Done.")
  }
</script>

# 8.3.2 事件绑定与事件监听(DOM 0 级事件处理程序)

<button id="btn" type="button">
<script>
  document.getElementById("btn").onclick = function() {
		alert("hello world!");
  }
	// 常规的事件绑定只执行最后绑定的事件。
</script>

# 8.3.3 事件监听(DOM 2 级事件处理程序)

document.getElementById("btn1").addEventListener("click",hello);
function hello(){
	alert("hello world!");
}

// 事件监听可以绑定多个事件。

# 8.3.4 IE 事件处理程序

IE只支持事件冒泡,不支持事件捕获,它也不支持addEventListener函数,不会用第三个参数来表示是冒泡还是捕获。

  • attachEvent(事件处理程序名称,事件处理程序函数);
  • detachEvent(事件处理程序名称,事件处理程序函数);
  • 通过该方法添加的事件处理程序,都会被添加到冒泡阶段。
// 添加多个事件处理程序。后添加先执行。

var handler = function(){
    alert(this.id);
};
btn.attachEvent (“onclick”, handler);
btn.detachEvent (“onclick”, handler);

# 8.3.5 跨浏览器事件处理程序

  • 创建一个方法addHandler():区分使用哪种方法来添加事件;
    • 依次尝试:
      • DOM 2级事件处理程序:addEventListener
      • IE 事件处理程序:attachEvent
      • HTML事件处理程序
  • 创建一个对象EventUtil。拥有两个方法。
  • addHandler(要操作的元素,事件名称,事件处理函数)。
  • removeHandler(要操作的元素,事件名称,事件处理函数)。
var EventUtil = {
    addHandler: function(element,type,handler){
        if(element.addEventLister) {
            element.addEventListener(type,handler,false);
        } else if(element.attachEvent) {
            element.attachEvent("on"+type, handler);
        }else {
            element.["on"+type] =handler;
        }
    },
    removeHandler: function(element,type,handler){
        if(element.addEventLister) {
            element.removeEventListener(type,handler,false);
        } else if(element.attachEvent) {
            element.detachEvent("on"+type, handler);
        }else {
            element.["on"+type] = null;
        }
    }
};

EventUtil.addHandler(btn ,”click”,handler);
EventUtil.removeHandler(btn ,”click”,handler);

# 8.3 停止传播

<div>
  <button>点击</button>
</div>
let div = document.getElementsByTagName('div')[0]
div.addEventListener('click', (e) => {
  console.log('div')
})

let button = document.getElementsByTagName('button')[0]
button.addEvenetListener('click', (e) => {
  console.log('button')
})

// 点击button会先后打印 button div

# 8.3.1 addEventListener 事件监听

通过设置 addEventListener 的capture参数可以决定事件是否在捕获阶段触发

div.addEventListener('click',(e)=>{
  console.log('div')
}{capture: true})

// 或者

div.addEventListener('click',(e)=>{
  console.log('div')
}true)

// 点击button就会先触发div的click事件,再触发button的click事件。
// 即先打印 div 然后打印 button

# 8.3.2 event.stopPropagation() 阻止冒泡

stopPropagation方法会让事件传播到目标阶段后停止传播,所以也叫阻止冒泡。相当于让事件流只剩下捕获阶段和目标阶段。

button.addEventListener('click',(e)=>{
  e.stopPropagation()
  console.log('button')
})

div.addEventListener('click',(e)=>{
  console.log('div1')
}true)

div.addEventListener('click',(e)=>{
  console.log('div2')
})

// 先打印 div1 然后打印 button

# 8.3.3 event.stopImmediatePropagation() 马上停止传播

有个特例,如果目标阶段的节点绑定了多个事件,它们不会区分捕获和冒泡,事件触发的顺序为代码执行的顺序,而且event.stopPropagation()在目标阶段不会生效。

如果目标阶段有 a、b、c 三个触发事件会按顺序执行,在 b 事件里设置event.stopPropagation()并不会影响 c 事件的触发。 但是如果在 b 事件里设置event.stopImmediatePropagation()后 ,事件触发到b之后就会停止触发后面的所有事件。

# 8.3.4 preventDefault() 阻止默认事件,但不阻止传播

告诉User Agent:如果此事件没有被显式处理,它默认的动作也不应该照常执行。此事件还是继续传播,除非碰到事件侦听器调用stopPropagation()stopImmediatePropagation(),才停止传播。

# 8.4 事件委托(事件代理)

# 8.4.1 概念

  • 通俗的说就是将元素的事件委托给它的父级或者更外级的元素处理,它的实现机制就是事件冒泡。
  • 事件委托就是利用事件冒泡,只制定一个时间处理程序,就可以管理某一类型的所有事件。
  • DOM遍历 + 事件冒泡。
  • 个人理解:冒泡本质上是触发事件的event.target的向上传播

# 8.4.2 例子

<ul class="color_list">        
    <li>red</li>        
    <li>orange</li>        
    <li>yellow</li>        
    <li>green</li>        
    <li>blue</li>        
    <li>purple</li>    
</ul>
<div class="box"></div>
.color_list{            
    display: flex;            
    display: -webkit-flex;        
}        
.color_list li{            
    width: 100px;            
    height: 100px;            
    list-style: none;            
    text-align: center;            
    line-height: 100px;        
}
//每个li加上对应的颜色,此处省略
.box{            
    width: 600px;            
    height: 150px;            
    background-color: #cccccc;            
    line-height: 150px;            
    text-align: center;        
}
// 不使用事件委托
// 需要给每一个 li 元素添加一个 eventListener
var color_list=document.querySelector(".color_list");            
var colors=color_list.getElementsByTagName("li");            
var box=document.querySelector(".box");            
for(var n=0;n<colors.length;n++){                
    colors[n].addEventListener("click",function(){                    
        console.log(this.innerHTML)                    
        box.innerHTML="该颜色为 "+this.innerHTML;                
    })            
}

// 使用事件委托
// 只需要给 ul 元素添加一个事件即可
function colorChange(e){                
    var e=e||window.event;//兼容性的处理         
    if(e.target.nodeName.toLowerCase()==="li"){                    
        box.innerHTML="该颜色为 "+e.target.innerHTML;                
    }                            
}            
color_list.addEventListener("click",colorChange,false);

由于事件冒泡机制,点击了 li 后会冒泡到 ul ,此时就会触发绑定在 ul 上的点击事件,再利用 target 找到事件实际发生的元素,就可以达到预期的效果。

# 8.4.3 特点

  • 只需要将同类元素的事件委托给父级或者更外级的元素,不需要给所有的元素都绑定事件,减少内存占用空间,提升性能。
  • 动态新增的元素无需重新绑定事件
  • 提高JavaScript性能。事件委托可以显著的提高事件的处理速度,减少内存的占用。
  • ajax的局部刷新,导致每次加载完,都要重新绑定事件。事件委托避免了这一点。

注意点:

  • 事件委托的实现依靠的冒泡,因此不支持事件冒泡的事件就不适合使用事件委托。
  • 不是所有的事件绑定都适合使用事件委托,不恰当使用反而可能导致不需要绑定事件的元素也被绑定上了事件。

# 9. Error

# 9.1 Error的标准属性

  • Error.prototype.name :错误的名字
  • Error.prototype.message:错误的描述
  • Error.prototype.toString:返回表示一个表示错误的字符串。

# 9.2 Error的种类

  • InternalError: 创建一个代表Javascript引擎内部错误的异常抛出的实例。 如: "递归太多"。非ECMAScript标准。
  • RangeError: 数值变量或参数超出其有效范围。例子:var a = new Array(-1);
  • EvalError: 与eval()相关的错误。eval()本身没有正确执行。
  • ReferenceError: 引用错误。 例子:console.log(b);
  • SyntaxError: 语法错误。例子:var a = ;
  • TypeError: 变量或参数不属于有效范围。例子:[1,2].split('.')
  • URIError: 给 encodeURI或 decodeURl()传递的参数无效。例子:decodeURI('%2')

# 10. ES6 新特性

# 10.1 let & const

# 10.2 模板字符串

// bad
const foo = 'this is a' + example;

// good
const foo = `this is a ${example}`;

let url = oneLine `
    www.taobao.com/example/index.html
    ?foo=${foo}
    &bar=${bar}
`;

# 10.3 箭头函数

箭头函数没有函数作用域,以下情况避免使用箭头函数:

  • 定义对象的方法

    // bad
    let foo = {
      value: 1,
      getValue: () => console.log(this.value)
    }
    
    foo.getValue();  // undefined
    
  • 定义原型方法

    // bad
    function Foo() {
      this.value = 1
    }
    
    Foo.prototype.getValue = () => console.log(this.value)
    
    let foo = new Foo()
    foo.getValue();  // undefined
    
  • 作为事件的回调函数

    // bad
    const button = document.getElementById('myButton');
    button.addEventListener('click', () => {
        console.log(this === window); // => true
        this.innerHTML = 'Clicked button';
    });
    

# 10.4 新数据类型

  • WeakSet
    • 元素只能是Object类型,不能是其他类型的值。
    • 弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果该对象不在被其他变量引用,那么垃圾回收机制就会自动回收该对象所占用内存,所以只要 WeakSet 成员对象在外部消失,它们在 WeakSet 里面的引用就会自动消失。
    • 由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。
  • WeakMap
    • 同 WeakMap
  • symbol
    • 唯一 const s = Symbol();
    • 应用场景:
      • 消除魔法字符串
      • 作为对象属性,防止属性名被覆盖
      • 模拟类的私有方法

# 10.4.1 唯一值

一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的。

const symbol1 = Symbol();
const symbol2 = Symbol(42);
const symbol3 = Symbol('foo');

console.log(typeof symbol1);
// expected output: "symbol"

console.log(symbol2 === 42);
// expected output: false

console.log(symbol3.toString());
// expected output: "Symbol(foo)"

console.log(Symbol('foo') === Symbol('foo'));
// expected output: false

// 用法
let name = Symbol("name");
let person = {};
person[name] = "range";

console.log(person[name]);		// range
console.log(person["name"]);	// undefined

# 10.4.2 共享值

let uid = Symbol.for("uid");
console.log(Symbol.keyFor(uid));    // "uid"

let uid2 = Symbol.for("uid");
console.log(Symbol.keyFor(uid2));   // "uid"

let uid3 = Symbol("uid");
console.log(Symbol.keyFor(uid3));   // undefined

# 10.4.2 不可强制转换类型

let uid = Symbol('uid')
uid + '' // Uncaught TypeError: Cannot convert a Symbol value to a string

# 10.5 Set 和 Map

// 数组去重

[...new Set(array)]

# 10.6 for of

可以遍历的对象:

  • 数组
  • Set
  • Map
  • 类数组(可以通过索引属性访问元素并且拥有 length 属性的对象。)
  • Generator
  • 字符串
for (const v of ['a', 'b', 'c']) {
  console.log(v);
}

# 10.7 Promise

# 10.8 Class

# 10.9 拓展运算符

function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}
// <=>
const sortNumbers = (...numbers) => numbers.sort();

Math.max(...[14, 3, 77])
// <=>
Math.max(14, 3, 77);

let [a, b, ...arr] = [1, 2, 3, 4, 5];

# 10.10 解构赋值

# 10.11 对象属性简写(属性增强)

# 11. Async / Await

# 11.1 基本语法

# 11.1.1 async函数返回一个Promise对象

async function f() {
  return "Hey,Range!"
};
f().then(v => console.log(v)); // Hey,Range!

# 11.1.2 如果 async 函数内部抛出异常,则会导致返回的 Promise 对象状态变为 reject 状态。抛出的错误而会被 catch 方法回调函数接收到。

async function e() {
  throw new Error('error');
};
e().then(v => console.log(v)).catch(e => console.error);

# 11.1.3 async函数内部return返回的值,会成为then方法回调函数的参数。

// 也就是说,只有当 `async` 函数内部的异步操作都执行完,才会执行 `then` 方法的回调。
const delay = timeout => new Promise(resolve=> setTimeout(resolve, timeout));
async function f(){
    await delay(1000);
    await delay(2000);
    await delay(3000);
    return 'done';
}

f().then(v => console.log(v)); // 等待6s后才输出 'done'

# 11.2 错误处理

# 11.2.1 当 async 函数中只要一个 await 出现 reject 状态,则后面的 await 都不会被执行。

let a;
async function f() {
  await Promise.reject('error');
  a = await 1; // 这段 await 并没有执行
}

f().then(v => console.log(v)).catch(e => console.log(a)) // undefined

# 11.2.2 使用try/catch

let a;
async function f() {
  try {
    await Promise.reject('error');
  } catch (error) {
    console.log(error);
  }
  a = await 1;

}

f().then(v => console.log(a))
// error
// 1

# 11.3 在项目中使用async

  • 安装依赖:

    npm install babel-preset-es2015 babel-preset-stage-3 babel-runtime babel-plugin-transform-runtime
    
  • 修改.babelrc.

    "presets": ["es2015", "stage-3"],
    "plugins": ["transform-runtime"]
    

# 12. fetch

# 12.1 概念

  • Fetch本质上是一种标准,该标准定义了请求、响应和绑定的流程。Fetch标准还定义了Fetch () JavaScript API,它在相当低的抽象级别上公开了大部分网络功能。
  • Fetch API 提供了一个获取资源的接口(包括跨域),它类似于 XMLHttpRequest ,但新的API提供了更强大和灵活的功能集。 Fetch 的核心在于对 HTTP 接口的抽象,包括 Request,Response,Headers,Body,以及用于初始化异步请求的 global fetch。
  • Fetch API 提供了一种全局**fetch()**方法,它返回一个 promise,这个 promise 会在请求响应后被 resolve,并传回 Response 对象。

# 12.2 特点

  • 当接收到一个代表错误的 HTTP 状态码时,从 fetch() 返回的 Promise 不会被标记为 reject, 即使响应的 HTTP 状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (但是会将 resolve 的返回值的 ok 属性设置为 false ),仅当网络故障时或请求被阻止时,才会标记为 reject。
  • fetch() 可以接受跨域 cookie;你可以使用 fetch() 建立起跨域会话。
  • fetch() 默认不会发送 cookie。除非你使用了credentials 的初始化选项。(自 2017 年 8 月 25 日以后,默认的 credentials 政策变更为 same-origin。Firefox 也在 61.0b13 版本中进行了修改)
  • 不支持超时控制
  • 由于fetch是比较底层的API,所以需要我们手动将参数拼接成name=test的格式,而jquery ajax已经封装好了。所以fetch并不是开箱即用的。

# 12.3 举例

fetch('https://example.com/movies.json')
	.then(function(response) {
    return response.json();
  })
	.then(function(myJson) {
  console.log(myJson);
	})

# 13. PWA(Progressive Web Apps,渐进式网络应用程序)

# 13.1 概念

PWA相对于传统Web应用,主要在以下几个方面变得更强:

  • 观感方面:在手机上,可以添加Web应用到桌面,并可提供类似于Native应用的沉浸式体验(也就是可以隐藏浏览器的脑门)。这部分背后的技术是manifest。
  • 性能方面:由于本文主角Service Worker具有拦截浏览器HTTP请求的超能力,搭配CacheStorage,PWA可以提升Web应用在网络条件不佳甚至离线时的用户体验和性能。
  • 其它方面:推送通知、后台同步等可选的高级功能,这些功能也是利用Service Worker来实现的。

简单一点说:

  • 我们一般写 web 应用,在 pc 上是没有缓存的,打开页面的时去请求数据。(通过CacheStorage实现)
  • 没有像 app 一样的小图标放在桌面,一点开就进入了应用,而是通过打开浏览器输入网址。(通过manifest.json实现)
  • 不能像 app 一样给用户推送消息,像微博会跟你推送说有谁评论了你的微博之类的功能。(ServiceWorker实现)

# 13.2 service worker

# 13.2.1 简介

  • Service Worker是浏览器在后台独立于网页运行的、用JavaScript编写的脚本。
  • 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。
  • 它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步API
// 不起眼的一行if,除了防止报错之外,也无意间解释了PWA的P:
// 如果浏览器不支持Service Worker,那就当什么都没有发生过
if ('serviceWorker' in navigator) {
    window.addEventListener('load', function () {
        // 所以Service Worker只是一个挂在navigator对象上的HTML5 API而已
        navigator.serviceWorker.register('/service-worker.js').then(function (registration) {
            console.log('我注册成功了666');
        }, function (err) {
            console.log('我注册失败了');
        });
    });
}

以下代码,可以拦截网页上所有png图片的请求,返回你的支付宝收款码图片,只要用户够多,总会有人给你打钱的。

// service-worker.js
// 虽然可以在里边为所欲为地写任何js代码,或者也可以什么都不写,
// 都不妨碍这是一个Service Worker,但还是举一个微小的例子:
self.addEventListener('fetch', function (event) {
    if (/\.png$/.test(event.request.url)) {
        event.respondWith(fetch('/images/支付宝收款码.png'));
    }
});

# 13.2.2 生命周期

Service Worker生命周期:安装中、安装后、激活中、激活后、已卸载。

  • 首次导航到网站时,会下载、解析并执行Service Worker文件,触发install事件,尝试安装Service Worker。
  • 如果install事件回调函数中的操作都执行成功,标志Service Worker安装成功,此时进入waiting状态。注意这时的Service Worker只是准备好了而已,并没有生效。
  • 当用户二次进入网站时,才会激活Service Worker,此时会触发activate事件,标志Service Worker正式启动,开始响应fetch、post、sync等事件。

# 13.2.3 主要事件

  • install:Service Worker安装时触发,通常在这个时机缓存文件。
  • activate:Service Worker激活时触发,通常在这个时机做一些重置的操作,例如处理旧版本Service Worker的缓存。
  • fetch:浏览器发起HTTP请求时触发,通常在这个事件的回调函数中匹配缓存,是最常用的事件。
  • push:和推送通知功能相关。
  • sync:和后台同步功能相关。

# 13.2.4 应用

  • 缓存静态资源:可以利用CacheStorage API来缓存js、css、字体、图片等静态文件。
  • 离线体验:如果我们首页index.html缓存下来,那我们的网页甚至可以支持离线浏览。
  • 消息推送

# 14. 防抖和节流

# 14.1 防抖

# 14.1.1 概念

触发事件后 n 秒后才执行函数,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

# 14.1.2 例子

// 无防抖

<div id="content"
    style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div>
  <script>
    let num = 1;
    const content = document.getElementById('content');
    function count() {
      content.innerHTML = num++;
    };
    content.onmousemove = count;
  </script>
// 非立即执行
// 触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

function debounce(func, delay) {
  let timeout;
  return function () {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(this, arguments)
    }, delay);
  }
}

// html中修改
content.onmousemove = debounce(count, 100);
// 立即执行
// 触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。
function debounce(func, delay) {
  let timeout;
  return function() {
    clearTimeout(timeout);
    const callNow = !timeout;
    timeout = setTimeout(() => {
      timeout = null;
    }, delay);
    if (callNow) func.apply(this, arguments);
  }
}

content.onmousemove = debounce(count, 100);

# 14.1.3 应用场景

  1. Google搜索的输入框
  2. Scroll滚动的时候检查位置(坐标)
  3. 函数防抖就是法师丢技能的时候要读条,技能读条没完再按技能就会重新读条。

# 14.2 节流

# 14.2.1 概念

  • 所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。
  • 区别于防抖:
    • 立即/非立即执行的防抖函数,每次触发事件都会刷新定时器
    • 节流是只有事件真正执行的时候才会刷新定时器,保证某一时段内只执行一次函数。

# 14.2.2 例子

// 时间戳 实现
function throttle(func, delay) {
  let pre = 0;
  return function() {
    let now = Date.now();
    if (now - pre > delay) {
      func.apply(this, arguments);
      pre = now;
    }
  }
}
// 定时器 实现
function throttle(func, delay) {
  let timeout;
  return function() {
    if (!timeout) {
      timeout = setTimeout(() => {
        timeout = null;
        func.apply(this, arguments);
      }, delay)
    }
  }
}

# 14.2.3 应用场景

  1. 监听滚动事件,比如是否滑到底部自动加载更多
  2. leetcode代码提交按钮
  3. 函数节流就是fps游戏的射速,就算一直按着鼠标射击,也只会在规定射速内射出子弹。

# 15. JS阻塞DOM的解析

# 15.1 UI渲染线程 与 JS引擎 互斥

  • 当浏览器在解析 HTML 页面时遇到了 <script>...</script> 标签,将无法继续构建DOM树,必须立即执行脚本。
    • 原因:UI 渲染线程与 JS 引擎是互斥的,当 JS 引擎执行时 UI 线程会被挂起。(所以 DOMContentLoaded 有可能在所有脚本执行完毕后触发。)
  • 外部样式表并不会阻塞 DOM 的解析,但是如果在样式后面有一个内联脚本,那么脚本必须等待样式先加载完:
    • 原因:JS 因为有可能会去获取 DOM 的样式,所以 JS 会等待样式表加载完毕,而 JS 是阻塞 DOM 的解析的,所以在有外部样式表的时候,JS 会一直阻塞到外部样式表下载完毕

# 15.2 async和defer

  • 相同点:
    • asyncdefer 属性仅仅对外部脚本起作用,并且他们在 src 不存在时会被自动忽略。
    • 带有 asyncdefer 的脚本的下载是和 HTML 的下载与解析是并行的,但是 JS 的执行一定是和 UI 线程是互斥的
  • 不同点:
    • 带有 async 的脚本是优先执行先加载完的脚本,他们在页面中的顺序并不影响他们执行的顺序。
    • 带有 defer 的脚本会在页面加载和解析完毕后执行,刚好在 DOMContentLoaded 之前执行。
  • 结论:
    • defer 和 async 在网络读取(下载)这块儿是一样的,都是异步的(相较于 HTML 解析)
    • 它俩的差别在于脚本下载完之后何时执行,显然 defer 是最接近我们对于应用脚本加载和执行的要求的
    • 关于 defer,此图未尽之处在于它是按照加载顺序执行脚本的,这一点要善加利用
    • async 则是一个乱序执行的主,反正对它来说脚本的加载和执行是紧紧挨着的,所以不管你声明的顺序如何,只要它加载完了就会立刻执行
    • 仔细想想,async 对于应用脚本的用处不大,因为它完全不考虑依赖(哪怕是最低级的顺序执行),不过它对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的

# 15.3 js加载时间线

  1. 创建Document对象,开始解析web页面。解析HTML元素和他们的文本内容后添加Element对象和Text节点到文档中。这个阶段document.readyState= 'loading'。
  2. 遇到link外部css,创建线程加载,并继续解析文档。
  3. 遇到script外部js,并且没有设置async、defer,浏览器加载,并阻塞,等待js加载完成并执行该脚本,然后继续解析文档
  4. 遇到script外部js,并且设置有async、defer,浏览器创建线程加载,并继续解析文档。对于async属性的脚本,脚本加载完成后立即执行。( 异步禁止使用document.write(),因为这个方法会将之前所有的文档流消除 )
  5. 遇到img等,先正常解析dom结构,然后浏览器异步加载src,并继续解析文档。
  6. 当文档解析完成(domTree加载完),document.readyState ='interactive'
  7. 文档解析完成后,所有设置有defer的脚本会按照顺序执行。(注意与async的不同,但同样禁止使用document.write());
  8. document对象触发 DOMContentLoaded 事件,这也标志着程序执行从同步脚本执行阶段,转化为事件驱动阶段。
  9. 当所有async的脚本加载完成并执行后、img等加载完成后,document.readyState= 'complete',window对象触发load事件
  10. 从此,以异步响应方式处理用户输入、网络事件等。

注意: DOMContentLoaded 事件只能用 addEventListener 进行绑定;如果希望script是在domTree加载完(img等还没加载)就立马执行,可以将 script 写在 head 标签里面;这种写法比 window.onload 事件快,因为load事件是让 script 在img等加载完之后再执行;当然,通用的写法还是将 script 写在 body 标签的最下面

# 16 深拷贝与浅拷贝

# 16.1 浅拷贝

  • Object.assign()
  • 函数库lodash的_.clone方法
  • 展开运算符...
  • Array.prototype.concat()
  • Array.prototype.slice()

# 16.2 深拷贝

  • JSON.parse(JSON.stringify())
  • 函数库lodash的_.cloneDeep方法
  • .jQuery.extend()

# 17. 前端模块化

# 17.1 CommonJS

  • 特点

    • CommonJS 模块输出的是一个值的拷贝(引用类型是浅拷贝)
    • CommonJS 模块是 Node.js 专用的(在服务端,模块文件都存在本地磁盘,支持同步的方式执行;但是在浏览器端,由于网络的原因,更优的方式是异步加载),与 ES6 模块不兼容。
    • CommonJS 的require()命令不能加载 ES6 模块,会报错。require()不支持 ES6 模块的一个原因是,它是同步加载,而 ES6 模块内部可以使用顶层await命令,导致无法被同步加载。
    • 所有代码都运行在模块作用域,不会污染全局作用域。
    • 模块加载的顺序,按照其在代码中出现的顺序。
  • 加载机制

    • CommonJS 模块是运行时加载。
    • CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。
    • CommonJS 模块是对象,整体加载某个模块(即加载该模块的所有方法),生成一个对象,然后再从这个对象上面读取 该对象所拥有的方法。
    • CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
  • 基本语法

    • // 暴露模块
      // example.js
      var x = 5;
      var addX = function (value) {
        return value + x;
      };
      module.exports.x = x;
      module.exports.addX = addX;
      
      // 引入
      var example = require('./example.js');//如果参数字符串以“./”开头,则表示加载的是一个位于相对路径
      console.log(example.x); // 5
      console.log(example.addX(1)); // 6
      

# 17.2 AMD

  • 特点

    • AMD规范则是非同步加载模块,允许指定回调函数。
    • 如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外AMD规范比CommonJS规范在浏览器端实现要来着早。
  • 基本语法

    • // 暴露模块
      //定义没有依赖的模块
      define(function(){
         return 模块
      })
      //定义有依赖的模块
      define(['module1', 'module2'], function(m1, m2){
         return 模块
      })
      
      // 引入模块
      require(['module1', 'module2'], function(m1, m2){
         使用m1/m2
      })
      

# 17.3 CMD

  • 特点

    • CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。
  • 基本语法

    • // 暴露模块
      //定义没有依赖的模块
      define(function(require, exports, module){
        exports.xxx = value
        module.exports = value
      })
      //定义有依赖的模块
      define(function(require, exports, module){
        //引入依赖模块(同步)
        var module2 = require('./module2')
        //引入依赖模块(异步)
          require.async('./module3', function (m3) {
          })
        //暴露模块
        exports.xxx = value
      })
      
      // 引入模块
      define(function (require) {
        var m1 = require('./module1')
        var m4 = require('./module4')
        m1.show()
        m4.show()
      })
      

# 17.4 ES Module

  • 特点

    • ES6 模块输出的是值的引用。
    • ES6 模块的import命令可以加载 CommonJS 模块,但是只能整体加载,不能只加载单一的输出项(即解构赋值的形式)。
  • 加载机制

    • ES6 模块是编译时输出接口。
    • ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
    • JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
  • 基本语法

    • // 暴露模块
      // math.js
      var basicNum = 0;
      var add = function (a, b) {
          return a + b;
      };
      export { basicNum, add };
      
      
      // 引入模块
      import { basicNum, add } from './math';
      function test(ele) {
          ele.textContent = add(99 + basicNum);
      }
      
  • package.json

    • type => module(ESM) / commonjs(CJS)
      • .mjs总是以ESM模块加载
      • .cjs总是CJS模块加载
      • 详情参见https://nodejs.org/api/packages.html#packages_type
  • Tips

    • exportsmodule.exports 的关系
      • exportsmodule.exports 的一个引用
      • exports = module.exports = {}