Giter Site home page Giter Site logo

blog's Introduction

Hi, I'm Stanley

A Frontend Development Engineer Live in China.

You can find me in my Website and Email.

📊 Monthly Coding Time

TypeScript       67 hrs 45 mins  🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟨⬜⬜⬜⬜⬜   78.73 %
JSON             7 hrs 11 mins   🟩🟩⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜   08.35 %
JavaScript       6 hrs 1 min     🟩🟩⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜   07.00 %
Other            1 hr 5 mins     🟨⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜   01.27 %
Bash             43 mins         ⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜   00.84 %

blog's People

Contributors

swiftwind0405 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

nthunt

blog's Issues

【JavaScript】垃圾回收机制

作为一种高级语言,JS 负责几个低级别的管理,比如内存管理。对于大多数编程语言来说,垃圾回收是一个常见的过程。通俗地说,垃圾回收就是简单地收集和释放,那些已经分配给对象,但目前又不被程序任一部分使用的内存。在像 C 这样的编程语言中,开发者必须使用 malloc() 和 dealloc() 函数来处理内存分配和回收。
Javascript 具有自动垃圾回收机制,会定期对那些我们不再使用的变量、对象所占用的内存进行释放。

Javascript 的垃圾回收机制

Javascript 会找出不再使用的变量,不再使用意味着这个变量生命周期的结束。Javascript 中存在两种变量——全局变量和局部变量,全部变量的声明周期会一直持续,直到页面卸载。而局部变量声明在函数中,它的声明周期从执行函数开始,直到函数执行结束。在这个过程中,局部变量会在堆或栈上被分配相应的空间以存储它们的值,函数执行结束,这些局部变量也不再被使用,它们所占用的空间也就被释放。

但是有一种情况的局部变量不会随着函数的结束而被回收,那就是局部变量被函数外部的变量所使用,其中一种情况就是闭包,因为在函数执行结束后,函数外部的变量依然指向函数内的局部变量,此时的局部变量依然在被使用,所以也就不能够被回收

可达性

JavaScript 中内存管理的主要概念是可达性。

简单地说,“可达性” 值就是那些以某种方式可访问或可用的值,它们被保证存储在内存中。

1. 有一组基本的固有可达值,由于显而易见的原因无法删除。例如:

  • 本地函数的局部变量和参数

  • 当前嵌套调用链上的其他函数的变量和参数

  • 全局变量

  • 还有一些其他的,内部的

这些值称为根。

2. 如果引用或引用链可以从根访问任何其他值,则认为该值是可访问的。

例如,如果局部变量中有对象,并且该对象具有引用另一个对象的属性,则该对象被视为可达性, 它引用的那些也是可以访问的。

JavaScript 引擎中有一个后台进程称为垃圾回收器,它监视所有对象,并删除那些不可访问的对象。

垃圾回收的算法

引用计数法

引用计数法就是引用对引用的次数进行计数。如果引用了增加就加 1,引用减少就减去 1。当引用等于 0 时将它清除。

引用计数有一个致命的问题就是循环引用,如果两个对象互相引用,尽管不再使用但是会进入一个无限循环,垃圾回收器不会对他进行回收。现在一般不会使用这个方法,但是 ie9 之前仍然还在用。

标记清除算法

标记清除算法是目前使用的较多的方法

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

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

例如,对象结构如下:

image

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

  • 第一步标记根:
    image
  • 然后标记他们的引用:
    image
  • 以及子孙代的引用:
    image
  • 现在进程中不能访问的对象被认为是不可访问的,将被删除:
    image

标记清除算法数据结构

标记清除法利用到了堆、链表结构 标记阶段:从根集合出发,将所有活动对象及其子对象打上标记 清除阶段:遍历堆,将非活动对象(未打上标记)的连接到空闲链表上

内存泄露

本质上讲,内存泄露就是不再被需要的内存,由于某种原因,无法被释放。

常见的内存泄露案例

  1. 全局变量造成内存泄露

    尽量不要使用全局变量。

  2. 未销毁的定时器和回调函数造成内存泄露

    使用玩及时清理定时器和回调函数。

  3. 闭包造成内存泄露
  4. DOM 引用造成内存泄露

    可以使用 WeakMap 或者 WeakSet 存储 DOM节点,DOM 被移除掉 WeakMap 或者 WeakSet 内部的 DOM 引用会被自动回收清除。

尽管垃圾回收是 JavaScript 自动执行的,但在某些情况下,它可能并不完美。在 JavaScript ES6 中,Map 和 Set 与它们的“weaker”兄弟元素一起被引入。“weaker”对应着 WeakMap 和 WeakSet,持有的是每个键对象的“弱引用”。它们允许对未引用的值进行垃圾收集,从而防止内存泄漏。

参考资料

【JavaScript】call / apply / bind

关于这块真的是基础的基础,曾经在遇到的时候一次又一次的跳过,包括用一些其他方式给解决了,但是若想深入,甚至看各种源码的时候,也是绕不开的知识点,必须好好学习。

区别

callapplybind 都可以改变函数的执行上下文,也就是函数运行时 this 的指向。

区别在于,callapply 改变了函数的执行上下文后会执行该函数,而 bind 不会执行该函数而是返回一个新函数。在参数的处理上,callapply 的第一个参数都是函数运行时的 this 值,区别在于,call 方法接受的是参数列表,apply 接受的是一个参数数组或类数组对象。

apply

apply 接受一个函数运行时的 this 值和一个参数数组或类数组对象,与 call 的区别在于对参数处理不同。

手写实现:

Function.prototype.apply = function(context, args) {
  // 对this的判断
  if (typeof this !== "function") {
    throw new TypeError("Error")
  }
  // 可对context的判断再做优化
  context = context || window
  // 可对第二参数进行容错,数组或者类数组
  args = args || []
  // 给context新增一个独一无二的属性以免覆盖原有属性
  const key = Symbol()
  context[key] = this
  const result = context[key](...args)
  // 带走产生的副作用
  delete context[key]
  return result
}

call

call 接受一个函数运行时的 this 值和一个参数列表,如果不指定第一个参数时,默认执行上下文为 window,改变 this 指向后,需要让新对象可以执行该函数,并能接受参数。

手写实现:

//传递参数从一个数组变成逐个传参了,不用...扩展运算符的也可以用arguments代替
Function.prototype.call = function (context, ...args) {
    //这里默认不传就是给window,也可以用es6给参数设置默认参数
    context = context || window;
    args = args ? args : [];
    //给context新增一个独一无二的属性以免覆盖原有属性
    const key = Symbol();
    context[key] = this;
    //通过隐式绑定的方式调用函数
    const result = context[key](...args);
    //删除添加的属性
    delete context[key];
    //返回函数调用的返回值
    return result;
}

bind

bind 接受一个函数运行时的 this 值和一个参数列表,返回一个新函数。

手写实现:

  • 实现一:
Function.prototype.bind = function (context, ...args) {
    const fn = this
    args = args ? args : []
    return function newFn(...newFnArgs) {
        if (this instanceof newFn) {
            return new fn(...args, ...newFnArgs)
        }
        return fn.apply(context, [...args,...newFnArgs])
    }
}
  • 实现二:
Function.prototype.bind = function (objThis, ...params) {
    const thisFn = this; // 存储源函数以及上方的params(函数参数)
    // 对返回的函数 secondParams 二次传参
    let fToBind = function (...secondParams) {
        const isNew = this instanceof fToBind // this是否是fToBind的实例 也就是返回的fToBind是否通过new调用
        const context = isNew ? this : Object(objThis) // new调用就绑定到this上,否则就绑定到传入的objThis上
        return thisFn.call(context, ...params, ...secondParams); // 用call调用源函数绑定this的指向并传递参数,返回执行结果
    };
    if (thisFn.prototype) {
        // 复制源函数的prototype给fToBind 一些情况下函数没有prototype,比如箭头函数
        fToBind.prototype = Object.create(thisFn.prototype);
    }
    return fToBind; // 返回拷贝的函数
};

参考资料

【Webpack】TypeScript 打包

可参考https://www.webpackjs.com/guides/typescript/或https://webpack.js.org/guides/typescript/

  • 安装 ts 依赖
npm install --save-dev typescript ts-loader
  • 增加 tsconfig.json 配置文件

tsconfig.json

{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "allowJs": true
  }
}
  • webpack.config.js 添加对 ts/tsx 语法支持(ts-loader)

webpack.config.js

const path = require("path");

module.exports = {
  entry: "./src/index.ts",
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader",
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"]
  },
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist")
  }
};

当从 npm 安装第三方库时,一定要牢记同时安装这个库的类型声明文件。可以从 TypeSearch 中找到并安装这些第三方库的类型声明文件。如:

npm install --save-dev @types/lodash

JavaScript 中的 this

image


什么是this

this 不是在编写时候绑定的,而是在运行时候绑定的上下文执行环境。this 绑定和函数申明无关,反而和函数被调用的方式有关系。

当一个函数被调用的时候,会建立一个活动记录,也成为执行环境。这个记录包含函数是从何处(call-stack)被调用的,函数是 如何 被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间将被使用的 this 引用。

四种绑定方式

函数调用

this 默认绑定

严格模式下,this 绑定到 underfined,否则绑定到全局对象。

方法调用

this 隐式绑定

当一个函数被调用的时候,会建立一个活动记录,也成为执行环境。这个记录包含函数是从何处(call-stack)被调用的,函数是 如何 被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间将被使用的 this 引用。

函数中的 this 是多变的,但是规则是不变的。一般情况下,this不是在编译的时候决定的,而是在运行的时候绑定的上下文执行环境this 与声明无关。

隐式绑定丢失

隐式丢失其实就是被隐式绑定的函数在特定的情况下会丢失绑定对象。

有两种情况容易发生隐式丢失问题:

  • 使用另一个变量来给函数取别名
  • 将函数作为参数传递时会被隐式赋值,回调函数丢失 this 绑定

如果把一个函数当成参数传递到另一个函数的时候,也会发生隐式丢失的问题,且与包裹着它的函数的 this 指向无关。在非严格模式下,会把该函数的 this 绑定到 window 上,严格模式下绑定到 undefined

构造函数调用

this 绑定的是新创建的对象

类通常包含一个 constructorthis 可以指向任何新创建的对象。

间接调用

强绑定,显示绑定,直接更改 this 的指向

  • this 永远指向最后调用它的那个对象
  • 匿名函数的 this 永远指向 window
  • 使用 .call() 或者 .apply() 的函数是会直接执行的
  • bind() 是创建一个新的函数,需要手动调用才会执行
  • 如果 callapplybind 接收到的第一个参数是空或者 nullundefined 的话,则会忽略这个参数
  • forEachmapfilter 函数的第二个参数也是能显式绑定 this

优先级:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

箭头函数

箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,thisundefined

此外,使用 callapplybind 等方法给 this 传值,箭头函数会忽略。箭头函数引用的是箭头函数在创建时设置的 this 值。

一些特殊情况下的this

DOM �事件函数

一般指向绑定事件的 DOM 元素,但有些情况绑定到全局对象,比如 IE6-8 的 attachEvent
例如:

document.body.addEventListener('click', fn, false);

function fn(e) {
	console.log(this === e.currentTarget);
}

参考资料

【JavaScript】数据类型转换

其它数据类型转布尔值

当我们在使用 Boolean() 来进行转换时,有如下转换规则:

参数类型 结果
false、undefined、null、+0、-0、NaN、"" false
除了上面的情况 true

另外需要注意的是,如果在使用Boolean()时不传参数结果也是为false

原始值转字符串

参数类型 结果
Undefined "undefined"
Null "null"
Boolean 如果参数是 true,返回 "true"。参数为 false,返回 "false"
Number 看下面题目
String 返回与之相等的值
Symbol "Symbol()"

数字转字符串

console.log(String(0)) // '0'
console.log(String(1)) // '1'
console.log(String(100)) // '100'
console.log(String(NaN)) // 'NaN'
console.log(String(10n)) // '10'
console.log(String(10n) === '10') // true

原始值转数字

参数类型 结果
Undefined NaN
Null +0
Boolean 如果参数是 true,返回 1。参数为 false,返回 +0
Number 返回与之相等的值
String 纯数字的字符串(包括小数和负数、各进制的数),会被转为相应的数字,否则为NaN
Symbol 使用Number()转会报错

记忆方法:

  • 纯数字的字符串(包括小数和负数、各进制的数),会被转为相应的数字
  • null 转为 0
  • Symbol 会报错
  • 其它的基本类型,包括非纯数字的字符串、NaNundefined 都会被转为 NaN

原始值转对象

console.log(new Object('1')) // String{'1'}
console.log(new Object(1)) // Number{1}
console.log(new Object(true)) // Boolean{true}
console.log(new Object(Symbol(1))) // Symbol{Symbol(1)}
console.log(new Object(10n)) // BigInt{10n}

console.log(new Object(null)) // {}
console.log(new Object(undefined)) // {}

传入的基本数据类型是什么类型的,那么最终的结果就会转为对应的包装类,但是对于 nullundefined 它们会被忽略,生成的会是一个空对象。

原始值转对象主要有以下总结:

  • StringNumberBoolean 有两种用法,配合 new 使用和不配合 new 使用,但是 ES6 规范不建议使用 new 来创建基本类型的包装类。
  • 现在更加推荐用 new Object() 来创建或转换为一个基本类型的包装类。

基本类型的包装对象的特点:

  • 使用 typeof 检测它,结果是 object,说明它是一个对象
  • 使用 toString() 调用的时候返回的是原始值的字符串

toPrimitive

ToPrimitive(input, PreferredType?)

参数:

  • 参数一:input,表示要处理的输入值
  • 参数二:PerferredType,期望转换的类型,可以看到语法后面有个问号,表示是非必填的。它只有两个可选值,NumberString

而它对于传入参数的处理是比较复杂的,看看这幅流程图:
image

数组转字符串主要是这样:

  • 空数组 [] 是被转换为空字符串 ""
  • 若是数组不为空的话,则将每一项转换为字符串然后再用","连接

配合着引用类型转字符串的图:
image

使用==比较时的类型转换

NaN 与其它类型的比较

NaN这个六亲不认的连它自己都不全等(也就是 NaN===NaN 的结果为false),只有用 Object.is(NaN, NaN) 才会被判断为true。

null/undefined 与其它类型的比较

若是一方为null、undefined,则另一方必须为null或者undefined才为true,也就是null == undefined为true或者null == null为true,因为undefined派生于null。

String 与 Number 的比较

会把String转成Number再来比较:

image

Boolean 与其它类型的比较

会将Boolean转为Number来比较,而通过上篇我们知道,Boolean转Number那是相当简单的,只有两种情况:

  • true => 1
  • false => 0

image

Object 与 String/Number/Symbol 的比较

会将对象执行类似ToNumber操作之后再进行比较的,但是又由于对象的valueOf()基本都是它本身,所以我们可以认为省略了这一步:

image

总结

当使用 == 进行比较的时候,会有以下转换规则(判断规则):

  1. 两边类型如果相同,值相等则相等,如 2 == 3 肯定是为 false
  2. 比较的双方都为基本数据类型:
    • 若是一方为 nullundefined,则另一方必须为 null 或者 undefined 才为 true,也就是 null == undefinedtrue 或者 null == nulltrue,因为 undefined 派生于 null
    • 其中一方为 String,是的话则把 String 转为 Number 再来比较
    • 其中一方为 Boolean,是的话则将 Boolean 转为 Number再来比较
  3. 比较的一方有引用类型:
    • 将引用类型遵循类似ToNumber的转换形式来进行比较(也就是toPrimitive(obj, 'defalut')
    • 两方都为引用类型,则判断它们是不是指向同一个对象

当一方有为对象的时候,实际是会将对象执行ToNumber操作之后再进行比较的,但是又由于对象的valueOf()基本都是它本身,所以我们可以认为省略了这一步。

可以对照下面的流程图看一下:

image

! 运算符的转换

当使用 ! 的时候,实际上会将 ! 后面的值转换为布尔类型来进行比较,而且这种转换是不会经过 valueOf 或者 toString 的,而是直接转换为了布尔值。

+-*/% 的类型转换

  1. -*/% 这四种都会把符号两边转成数字来进行运算
  2. + 由于不仅是数字运算符,还是字符串的连接符,所以分为两种情况:
    • 两端都是数字则进行数字计算
    • 有一端是字符串,就会把另一端也转换为字符串进行连接

对象的 + 号类型转换:

  • 对象在进行 + 号字符串连接的时候,toPrimitive 的参数 hintdefault,但是default 的执行顺序和number一样都是先判断有没有 valueOf,有的话执行 valueOf,然后判断 valueof 后的返回值,若是是引用类型则继续执行 toString
  • 日期在进行 + 号字符串连接的时候,优先调用 toString() 方法。
  • 一元正号是转换其他对象到数值的最快方法,也是最推荐的做法,因为它不会对数值执行任何多余操作

参考资料

【CSS】flex 布局

image

container容器的属性

  • flex-direction:决定主轴的方向(即项目的排列方向)
  • flex-wrap:决定容器内项目是否可换行
  • flex-flow:以上两个属性的简写形式
  • justify-content:指定主轴方向项目的对齐方式
  • align-items:指定交叉轴上项目的对齐方式
  • align-content:多根轴线指定对齐方式

item项目的属性

  • flex-grow:指定当前项如何扩展
  • flex-shrink:指定当前项如何收缩
  • flex-basis:指定分配剩余空间之前当前项的初始大小
  • flex:以上三个属性的简写形式
  • order:指定当前项在容器中出现的次序

最高优先级:flex-basic

在分配多余空间前,指定项目占据的主轴空间。浏览器根据这个属性,计算主轴是否有多余空间,默认值为auto,即项目的本来大小。 flex-items的width属性和flex-basis属性都在的情况下,容器的换行以flex-basis的值为标准

flex-basis 规定的是子元素的基准值。所以是否溢出的计算与此属性息息相关。flex-basis 规定的范围取决于 box-sizing。这里主要讨论以下 flex-basis 的取值情况:

  • auto:首先检索该子元素的主尺寸,如果主尺寸不为 auto,则使用值采取主尺寸之值;如果也是 auto,则使用值为 content。

  • content:指根据该子元素的内容自动布局。有的用户代理没有实现取 content 值,等效的替代方案是 flex-basis 和主尺寸都取 auto。

  • 百分比:根据其包含块(即伸缩父容器)的主尺寸计算。如果包含块的主尺寸未定义(即父容器的主尺寸取决于子元素),则计算结果和设为 auto 一样。

特例:flex-basis: 0

假设把所有可伸缩项的flex-basis都设置为0,那就意味着这些项不会参与第一次容器空间的分配。

什么意思呢?如前所述,flex-basis值决定了CSS如何确定各伸缩项在容器中的初始宽度。确定各项初始宽度是对容器空间的首次分配。

如果初次分配各项初始宽度为0( flex-basis: 0; ),那就是说容器的全部宽度都会进入二次分配环节。利用这一点,可以实现各伸缩项宽度的绝对平均化

flex-basis: 0flex-basis: 0% 的视觉效果和最终计算值是一样的,只不过是计算过程不同。百分比的计算值是以父类容器的宽度为基数计算的,而长度值 0 直接取值不用再计算,但是 0% 和 0 的最终计算值都是 0px。

项目最终宽度的计算方式

计算公式:
image

扩展:flex-grow

当前项可分得的剩余空间 = ( 当前项flex-grow值/所有项flex-grow值之和 ) * 剩余总宽度

计算demo可以看这个:flex-grow-calc-demo1

还有一种情况:
当所有元素的 flex-grow 之和小于 1 的时候(注意是 1,也就是说每个元素的 flex-grow 都是一个小数如 0.2 这样的),上面式子中的 sum 将会使用 1 来参与计算,而不论它们的和是多少。也就是说,当所有的元素的 flex-grow 之和小于 1 的时候,剩余空间不会全部分配给各个元素。

实际上用来分配的空间是 sum(flex-grow) / 1 * 剩余空间 ,这些用来分配的空间依然是按 flex-grow 的比例来分配。

计算demo可以看这个:flex-grow-calc-demo2

另外,flex-grow 还会受到 max-width 的影响。如果最终 grow 后的结果大于 max-width 指定的值,max-width 的值将会优先使用。同样会导致父元素有部分剩余空间没有分配。

收缩:flex-shrink

当前项收缩的宽度 = ( 当前项flex-shrink * 当前项flex-basic / 所有项flex-shrink  与各自flex-basic乘积之和 ) * 需收缩的总宽度

这里需要注意当前项的flex-basic的取值,可见上方的flex-basic内容,最后收缩完该项的width就是原本的width减去上面计算出来需要收缩的宽度。

计算demo可以看这个:flex-shrink-calc-demo1

同样,当所有元素的 flex-shrink 之和小于 1 时,计算方式也会有所不同:

此时,并不会收缩所有的空间,而只会收缩 flex-shrink 之和相对于 1 的比例的空间。

计算demo可以看这个:flex-shrink-calc-demo2

当然,类似 flex-grow,flex-shrink 也会受到 min-width 的影响。

总结:
虽然上面的公式看起来很复杂,其实计算过程还是比较简单的:如果所有元素的 flex-grow/shrink 之和大于等于 1,则所有子元素的尺寸一定会被调整到适应父元素的尺寸(在不考虑 max/min-width/height 的前提下),而如果 flex-grow/shrink 之和小于 1,则只会 grow 或 shrink 所有元素 flex-grow/shrink 之和相对于 1 的比例。grow 时的每个元素的权重即为元素的 flex-grow 的值;shrink 时每个元素的权重则为元素 flex-shrink 乘以 width 后的值。

那么为什么 grow 只用管各个项的 grow 值按比例分配,而 shrink 却额外还要考虑 flex-basic 的值呢?

侧轴:align-self

弹性布局默认不改变项目的宽度,但是它默认改变项目的高度。如果项目没有显式指定高度,就将占据容器的所有高度。而 align-self 属性可以改变这种行为。

align-self 属性可以取四个值:

  • flex-start:顶边对齐,高度不拉伸
  • flex-end:底边对齐,高度不拉伸
  • center:居中,高度不拉伸
  • stretch:默认值,高度自动拉伸

如果项目很多,一个个地设置 align-self 属性就很麻烦。这时,可以在容器元素(本例为表单)设置 align-items 属性,它的值被所有子项目的 align-self 属性继承。

关于 flex 的其它相关问题

  • flex 上下文中垂直 margin 不会合并
  • flex 主轴方面上子元素的 margin 如果设置为 auto,其该方向上的 margin 是会尽量大的,可以利用这个特性来做对齐
  • 设置了 flex 布局之后,子元素的 floatclearvertical-align 的属性将失效

工具

参考资料

  1. Flexbox,终于可以承认自己明白了
  2. Flex 布局教程:语法篇
  3. Flexbox 布局的最简单表单
  4. 重点来了,flex-shrink到底是如何计算的呢?
  5. 详解 flex-grow 与 flex-shrink

【100 issues】前端基础学习

每天学习,努力坚持,质变引起量变!
本来是说定的 100 天的计划,但是似乎每天在工作之余抽出的时间很难完成一个完整的主题,有时候一个主题需要学习好多天甚至更久,因此改为 100 个 issues 吧,Deadline 是 2020!
努力奔跑吧 💪💪💪 !

JavaScript

基础

进阶

异步

CSS

网络

React

Webpack

算法

前端

Tips

以上所有主题,99.99% 非原创,个人只做收集总结归纳而已,仅供个人参考使用。

【CSS】BFC 块级格式化上下文

image

BFC概念

BFC(Block Formatting Context)格式化上下文,是盒模型的一种渲染布局,简言之可以理解为 一个独立的容器不受外部影响不影响外部

如何触发 BFC

满足下列条件之一就可触发 BFC

  • 【1】根元素,即HTML元素
  • 【2】float 的值不为 none
  • 【3】overflow 的值不为 visible
  • 【4】display 的值为 inline-blocktable-celltable-caption
  • 【5】position 的值为 absolutefixed
     

BFC布局规则

  1. 内部的 Box 会在垂直方向,一个接一个地放置。
  2. Box 垂直方向的距离由 margin 决定。属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠
  3. 每个元素的 margin box 的左边, 与包含块 border box 的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。
  4. BFC 的区域不会与 float box 重叠。
  5. BFC 就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
  6. 计算 BFC 的高度时,浮动元素也参与计算

BFC 的作用

  1. 自适应两栏布局
  2. 可以阻止元素被浮动元素覆盖
  3. 可以包含浮动元素——清除内部浮动
  4. 分属于不同的 BFC 时可以阻止 margin 重叠

参考资料

【Webpack】Dev Tools 开发工具

webpack-dev-server

DevServer 会启动一个 HTTP 服务器用于服务网页请求,同时会帮助启动 Webpack ,并接收 Webpack 发出的文件更变信号,通过 WebSocket 协议自动刷新网页做到实时预览。

首先需要安装 DevServer:

npm i webpack-dev-server -D

DevServer 会把 Webpack 构建出的文件保存在内存中。

接口代理(请求转发)

如果你有单独的后端开发服务器 API,并且希望在同域名下发送 API 请求 ,那么代理某些 URL 会很有用。dev-server 使用了非常强大的 http-proxy-middleware 包。常用于接口请求转发。具体参考https://www.webpackjs.com/configuration/dev-server/#devserver-proxy

devServer: {
    contentBase: "./dist",
    open: true,
    hot: true,
    hotOnly: true,
    proxy: {
      "/api": {
        target: "https://other-server.example.com",
        pathRewrite: {"^/api" : ""},
        secure: false,
        bypass: function(req, res, proxyOptions) {
          if (req.headers.accept.indexOf("html") !== -1) {
            console.log("Skipping proxy for browser request.");
            return "/index.html";
          }
        }
      }
    }
  },

webpack-dev-middleware

webpack-dev-middleware 是一个容器(wrapper),它可以把 webpack 处理后的文件传递给一个服务器(server)。 webpack-dev-server 在内部使用了它,同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置来实现更多的需求

// server.js
// 使用webpack-dev-middleware
// https://www.webpackjs.com/guides/development/#%E4%BD%BF%E7%94%A8-webpack-dev-middleware
const express = require("express");
const webpack = require("webpack");
const webpackDevMiddleware = require("webpack-dev-middleware");
const config = require("./webpack.config.js");
const complier = webpack(config);

const app = express();

app.use(
  webpackDevMiddleware(complier, {
    publicPath: config.output.publicPath
  })
);

app.listen(3000, () => {
  console.log("server is running");
});

解决单页面路由问题

当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html
通过传入以下启用:

historyApiFallback: true;

复制代码通过传入一个对象,比如使用 rewrites 这个选项,此行为可进一步地控制:

historyApiFallback: {
  rewrites: [
    { from: /^\/$/, to: "/views/landing.html" },
    { from: /^\/subpage/, to: "/views/subpage.html" },
    { from: /./, to: "/views/404.html" }
  ];
}

Hot Module Replacement

模块热替换(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需进行完全刷新。

webpack.config.js

const webpack = require('webpack');

devServer: {
  contentBase: './dist',
  open: true,
  hot: true,
  hotOnly: true
},
plugins: [
  ...
  new webpack.HotModuleReplacementPlugin()
],

如果已经通过 HotModuleReplacementPlugin 启用了模块热替换(Hot Module Replacement),则它的接口将被暴露在 module.hot 属性下面。通常,用户先要检查这个接口是否可访问,然后再开始使用它。

index.js

// index.js
if (module.hot) {
  module.hot.accept("./library.js", function() {
    // 使用更新过的 library 模块执行某些操作...
  });
}

bundle 分析

借助一些官方推荐的可视化分析工具,可对打包后的模块进行分析以及优化

  • webpack-chart: webpack 数据交互饼图
  • webpack-visualizer: 可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的
  • webpack-bundle-analyzer: 一款分析 bundle 内容的插件及 CLI 工具,以便捷的、交互式、可缩放的树状图形式展现给用户

Preloading、Prefetching

prefetch:会等待核心代码加载完成后,页面带宽空闲后再去加载 prefectch 对应的文件;preload:和主文件一起去加载

  • 可以使用谷歌浏览器 Coverage 工具查看代码覆盖率(ctrl+shift+p > show coverage)
  • 使用异步引入 js 的方式可以提高 js 的使用率,所以 webpack 建议我们多使用异步引入的方式,这也是 splitChunks.chunks 的默认值是"async"的原因
  • 使用魔法注释 /_ webpackPrefetch: true _/ ,这样在主要 js 加载完,带宽有空闲时,会自动下载需要引入的 js
  • 使用魔法注释 /_ webpackPreload: true _/,区别是 webpackPrefetch 会等到主业务文件加载完,带宽有空闲时再去下载 js,而 preload 是和主业务文件一起加载的

Shimming

webpack 编译器(compiler)能够识别遵循 ES2015 模块语法、CommonJS 或 AMD 规范编写的模块。然而,一些第三方的库(library)可能会引用一些全局依赖(例如 jQuery 中的 $)。这些库也可能创建一些需要被导出的全局变量。这些“不符合规范的模块”就是 shimming 发挥作用的地方

  • shimming 全局变量(第三方库)(ProvidePlugin 相当于一个垫片)
const webpack = require("webpack");

module.exports = {
  plugins: [
    new webpack.ProvidePlugin({
      _: "lodash",
    }),
  ],
};
  • 细粒度 shimming(this 指向 window)(需要安装 imports-loader 依赖)
module.exports = {
  module: {
    rules: [
      {
        test: require.resolve("index.js"),
        use: "imports-loader?this=>window",
      },
    ],
  },
  plugins: [
    new webpack.ProvidePlugin({
      join: ["join"],
    }),
  ],
};

环境变量

在配置 Webpack 时,需要区分用于开发模式还是生产模式。比如我们只需要在生产模式时压缩 CSS;而在开发模式的时候,我们又希望生成 source map 便于调试,以及样式热更新。

详细可以看这篇文章,说的非常细致全面了:Webpack 设置环境变量的误区

参考文档

【JavaScript】原型与原型链

原型

每一个构造函数都有一个 prototype 属性,它指向构造函数的原型对象。 原型对象中有一个 constrcutor 属性,指回构造函数。而每一个实例对象都有一个 __proto__ 属性,当我们用构造函数创建实例时,实例的__proto__属性就会指向该构造函数的原型对象。而构造函数也是一种对象,其__proto__属性指向 Function.prototype

所有 Function 的实例都是函数对象,其他的均为普通对象,其中包括 Function 实例的实例。

image

Function.prototype__proto__属性又指向 Object.prototype(鸡生蛋,蛋生鸡),而Object.prototype__proto__属性最终指向 null

JavaScript 中万物皆对象,而对象皆出自构造(构造函数)。

经典神图。必须理解到位:
image

原型链

当我们试图访问一个对象的属性或方法时,不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索:
image

  • 原型链查找会一直持续,直到找到同名的属性或方法,或者到达原型链的末尾 null
  • 原型链查找会遵循 属性遮蔽 原则,位于底层的属性或方法会被优先找到

每个对象拥有一个原型对象,通过 __proto__ 指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层向上,最终指向 null,这就是原型链。

hasOwnProperty

hasOwnPropertyObject.prototype 的一个方法,他能判断一个对象是否包含自定义属性而不是原型链上的属性,因为 hasOwnProperty 是 JavaScript 中唯一一个处理属性但是不查找原型链的函数。

in

prop in object

如果指定的属性在指定的对象或其原型链中,则 in 运算符返回 true

对被删除或值为 undefined 的属性使用in:

  • 如果使用 delete 运算符删除了一个属性,则 in 运算符对所删除属性返回 false
  • 如果只是将一个属性的值赋值为 undefined,而没有删除它,则 in 运算仍然会返回 true

instanceof

object instanceof Constructor

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

即通过下面的操作来判断:

object.__proto__ === Constructor.prototype ?
object.__proto__.__proto__ === Constructor.prototype ?
object.__proto__.__proto__....__proto__ === Constructor.prototype

当左边的值是 null 时,会停止查找,返回 false。实际是检测 Constructor.prototype 是否存在于参数 object 的原型链上。

手写实现 instanceof

function _instanceOf(left, right) {
  if (right === null || right === undefined) {
    throw new TypeError(`Right-hand side of ' instanceof ' is not an object`)
  }
  const rightPrototype = right.prototype
  left = Object.getPrototypeOf(left)

  while (left !== null) {
    if (left === rightPrototype) return true
    left = Object.getPrototypeOf(left)
  }

  return false
}

Object.prototype.isPrototypeOf()

prototypeObj.isPrototypeOf(object)

isPrototypeOf() 方法用于测试一个对象是否存在于另一个对象的原型链上。

Object.getPrototypeOf

Object.getPrototypeOf(object)

Object.getPrototypeOf() 方法返回指定对象的原型(内部 [[Prototype]] 属性的值)。如果没有继承属性,则返回 null

手写实现 getPrototypeOf

Object.getPrototypeOf = function(obj) {
  if (obj === null || obj === undefined) {
    throw new Error('Cannot convert undefined or null to object')
  }
  if (typeof obj === 'boolean' || typeof obj === 'number' || typeof obj === 'string') return Object(obj).__proto__
  return obj.__proto__
}

Object.setPrototypeOf

Object.setPrototypeOf(obj, prototype)

Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即内部 [[Prototype]] 属性)到另一个对象或 null

如果 prototype 参数不是一个对象或者 null (例如,数字,字符串,boolean,或者 undefined),则会报错。该方法将 obj 的 [[Prototype]] 修改为新的值。

如果不指定对应的属性描述符,则默认都是 false。描述符有以下几个:

  • enumerable 可枚举,默认 false
  • configurable 可删除,默认 false`
  • writable 可赋值,默认 false`
  • value 属性的值

手写实现 setPrototypeOf

Object.create = function(proto, propertiesObject) {
  const res = {}
  // proto 只能为 null 或者 type 为 object 的数据类型
  if (!(proto === null || typeof proto === 'object')) {
    throw new TypeError('Object prototype may only be an Object or null')
  }
  Object.setPrototypeOf(res, proto)

  if (propertiesObject === null) {
    throw new TypeError('Cannot convert undefined or null to object')
  }
  if (propertiesObject) {
    Object.defineProperties(res, propertiesObject)
  }

  return res
}

参考链接:

【Webpack】Library 库打包

除了打包应用程序代码,webpack 还可以用于打包 JavaScript library 用户应该能够通过以下方式访问 library:

  • ES2015 模块。例如 import library from 'library'
  • CommonJS 模块。例如 require('library')
  • 全局变量,当通过 script 脚本引入时

我们打包的 library 中可能会用到一些第三方库,诸如 lodash。现在,如果执行 webpack,你会发现创建了一个非常巨大的文件。如果你查看这个文件,会看到 lodash 也被打包到代码中。在这种场景中,我们更倾向于把 lodash 当作 peerDependency。也就是说,用户应该已经将 lodash 安装好。因此,你可以放弃对外部 library 的控制,而是将控制权让给使用 library 的用户。这可以使用 externals 配置来完成:

  // webpack.config.js
  var path = require('path');

  module.exports = {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'webpack-numbers.js'
-   }
+   },
+   externals: {
+     lodash: {
+       commonjs: 'lodash',
+       commonjs2: 'lodash',
+       amd: 'lodash',
+       root: '_'
+     }
+   }
  };

对于用途广泛的 library,我们希望它能够兼容不同的环境,例如 CommonJS,AMD,Node.js 或者作为一个全局变量。为了让你的 library 能够在各种用户环境(consumption)中可用,需要在 output 中添加 library 属性:

  // webpack.config.js
  var path = require('path');

  module.exports = {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
-     filename: 'library.js'
+     filename: 'library.js',
+     library: 'library'
    },
    externals: {
      lodash: {
        commonjs: 'lodash',
        commonjs2: 'lodash',
        amd: 'lodash',
        root: '_'
      }
    }
  };

当你在 import 引入模块时,这可以将你的 library bundle 暴露为名为 webpackNumbers 的全局变量。为了让 library 和其他环境兼容,还需要在配置文件中添加 libraryTarget 属性。这是可以控制 library 如何以不同方式暴露的选项。

  var path = require('path');

  module.exports = {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'library.js',
+     library: 'library',
+     libraryTarget: 'umd'
    },
    externals: {
      lodash: {
        commonjs: 'lodash',
        commonjs2: 'lodash',
        amd: 'lodash',
        root: '_'
      }
    }
  };

我们还需要通过设置 package.json 中的 main 字段,添加生成 bundle 的文件路径。

// package.json
{
  ...
  "main": "dist/library.js",
  ...
}

【Webpack】Code Spliting 代码分割

Code Splitting 的核心是把很大的文件,分离成更小的块,让浏览器进行并行加载。

常见的代码分割有三种形式:

  • 手动进行分割:例如项目如果用到 lodash,则把 lodash 单独打包成一个文件。
  • 同步导入的代码:使用 Webpack 配置进行代码分割。
  • 异步导入的代码:通过模块中的内联函数调用来分割代码。

手动进行分割的意思是在 entry 上配置多个入口。
比如配置两个 entry,它就会输出两个模块,也能在一定程度上进行代码分割,不过这种分割是十分脆弱的,如果两个模块共同引用了第三个模块,那么第三个模块会被同时打包进这两个入口文件中,而不是分离出来。
所以我们常见的做法是关心最后两种代码分割方法,无论是同步代码还是异步代码,都需要在 webpack.common.js 中配置 splitChunks 属性,像下面这样子:

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
	  // async:此值为默认值,只有异步导入的代码才会进行代码分割
	  // initial:与async相对,只有同步引入的代码才会进行代码分割。
	  // all:表示无论是同步代码还是异步代码都会进行代码分割。
    }
  }
}

【JavaScript】Promise 基础

基础

Promise 必须为以下三种状态之一:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。一旦 Promiseresolvereject,不能再迁移至其他任何状态(即状态 immutable)。

image

基本过程:

  1. 初始化 Promise 状态(pending
  2. 执行 then(..) 注册回调处理数组(then 方法可被同一个 promise 调用多次)
  3. 立即执行 Promise 中传入的 fn 函数,将 Promise 内部 resolvereject 函数作为参数传递给 fn ,按事件机制时机处理
  4. Promise 中要保证,then 方法传入的参数 onFulfilledonRejected,必须在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。

Promise 最大的意义在于给予我们使用异步时丢失的 returnthrow 和栈。

Promise 中,返回任意一个非 promise 的值都会被包裹成 promise 对象,例如 return 2 会被包装为 return Promise.resolve(2)

方法

Promise.prototype.then()

Promise 实例具有 then 方法,即 then 方法是定义在原型对象 Promise.prototype 上的,作用是给 Promise 实例添加状态改变时的回调函数。then 方法的第一个参数是 Resolve 状态的回调函数,第二个是 Rejected(可选)状态的回调函数。

Promise.prototype.catch()

Promise.prototype.catch的方法是.then(null,rejection)的别名,用于指定发生错误的回调函数。

Promise.prototype.finally()

  • .finally() 方法不管 Promise 对象最后的状态如何都会执行
  • .finally() 方法的回调函数不接受任何的参数,也就是说在 .finally() 函数中是没法知道 Promise 最终的状态是 resolved 还是 rejected
  • 它最终返回的默认会是一个上一次的 Promise 对象值,不过如果抛出的是一个异常则返回异常的 Promise对象

Promise.all()

Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。

Promise.race()

Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝,而其余的 promise 虽然依旧在执行,但是其结果会被抛弃。

使用场景:可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作。

allrace 传入的数组中如果有会抛出异常的异步任务,那么只有最先抛出的错误会被捕获,并且是被 then 的第二个参数或者后面的 catch 捕获;但并不会影响数组中其它的异步任务的执行。

Promise.allSettled()

Promise.allSettled() 方法返回一个 promise,该 promise 在所有给定的 promise 已被解析或被拒绝后解析,并且每个对象都描述每个 promise 的结果。

Promise.resolve()

返回一个 fulfilledPromise 实例,或原始 Promise 实例。

  • 参数为空,返回一个状态为 fulfilledPromise 实例
  • 参数是一个跟 Promise 无关的值,返回一个状态为 fulfilledPromise 实例,同时 fulfilled 响应函数会得到喝过参数
  • 参数为 Promise 实例,则返回该实例,不做任何修改。
  • 参数为 thenable,立刻执行它的 .then()

Promise.reject()

返回一个 rejectedPromise 实例。

Promise.resolve() 大体类似,除了 Promise.reject() 不认 thenable

总结:

  • .then.catch 都会返回一个新的 Promise
  • catch 不管被连接到哪里,都能捕获上层未捕捉过的错误
  • Promise.then 或者 .catch 可以被调用多次, 但如果 Promise 内部的状态一经改变,并且有了一个值,那么后续每次调用 .then 或者 .catch 的时候都会直接拿到该值
  • .then 或者 .catchreturn 一个 error 对象并不会抛出错误,所以不会被后续的 .catch 捕获
  • .then.catch 返回的值不能是 promise 本身,否则会造成死循环
  • .then 或者 .catch 的参数期望是函数,传入非函数则会发生值透传
  • .then 方法是能接收两个参数的,第一个是处理成功的函数,第二个是处理失败的函数,再某些时候你可以认为 catch.then 第二个参数的简便写法
  • .finally 方法也是返回一个 Promise,他在 Promise 结束的时候,无论结果为 resolved 还是rejected,都会执行里面的回调函数

链式调用

Promise 能够链式调用的原理,即:promisethen/catch 方法执行后会也返回一个 promise

实现 Ajax

let getJSON=function(url){
    let promise=new Promise(function(){
        let client=new XMLHttpRequest()
        client.open('GET',url)
        client.onreadystatechange=hander
        cilent.responseType='json'
        client.setRequestHeader('Accept',"application/json")
        client.send()
        function hander(){
            if(this.readystate!=4){
                return
            }
            if(this.statues===200){
                resolve(this.response)
            }else{
                reject(new Error(this.stautsText))
            }
        }
    })
    return promise
}

//使用
getJSON('/xxx/xx.json').then((json)=>{
    console.log('contents'+json)
},(error)=>{
    console.log("请求出错"+error)
})

参考资料

【React】React Hooks

概念

什么是 React Hooks:

  • React Hooks是将React.Component的特性添加到函数组件的一种方式。
  • Hooks能够让开发者不使用class来使用React的特性
  • React 组件加载、渲染、卸载过程中的拦截处理程序。

为什么要用 React Hooks:

  • 组件内状态逻辑的简化。比如:状态管理的简化,生命周期的简化,这些下面会通过三大基础 hook 来体现。
  • 组件间状态逻辑的复用,用自定义 hook 就可以体现出来。

Hooks 组件的目标并不是取代 class component 组件,而是增加函数式组件的使用率,明确通用工具函数与业务工具函数的边界,鼓励开发者将业务通用的逻辑封装成 React Hooks 而不是工具函数。

钩子

钩子 用法 作用
useState const [state, changeState] = useState(initialValue) 用于生成状态以及改变状态的方法
useEffect useEffect(fn, [...relativeState]) 用于生成与状态绑定的副作用
useCallback useCallback(fn, [...relativeState]) 用于生成与状态绑定的回调函数
useMemo useMemo(fn, [...relativeState]) 用于生成与状态绑定的组件/计算结果
useRef const newRef = useRef(initialValue) 用于 获取节点实例 / 数据保存

Hooks 的生命周期

image

自定义 Hooks 案例

useDebounce(防抖)

// 防抖
function debounce(func, ms = 30) {
    let timeout;
    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout);
        
        timeout = setTimeout(() => {
            func.apply(context, args)
        }, ms);
    }
}

// 自定义Hooks
const useDebounce = (fn, ms = 30, deps = []) => {
    let timeout = useRef()
    useEffect(() => {
        if (timeout.current) clearTimeout(timeout.current)
        timeout.current = setTimeout(() => fn(), ms)
    }, deps)

    const cancel = () => {
        clearTimeout(timeout.current)
        timeout = null
    }
  
    return [cancel]
  }

useThrottle(节流)

// 节流
function throttle(func, ms) {
    let previous = 0;
    return function() {
        let now = Date.now();
        let context = this;
        let args = arguments;
        if (now - previous > ms) {
            func.apply(context, args);
            previous = now;
        }
    }
}

// 自定义Hooks
const useThrottle = (fn, ms = 30, deps = []) => {
    let previous = useRef(0)
    let [time, setTime] = useState(ms)
    useEffect(() => {
        let now = Date.now();
        if (now - previous.current > time) {
            fn();
            previous.current = now;
        }
    }, deps)

    const cancel = () => {
        setTime(0)
    }
  
    return [cancel]
  }

useUpdate

const useUpdate = () => {
    const [, setFlag] = useState()
    const update = () => {
        setFlag(Date.now())
    }
  
    return update
}

参考文档

【网络】HTTP和HTTPS

image


HTTP(HyperText Transfer Protocol),超文本传输协议,是一个基于TCP实现的应用层协议。

image

HTTP的请求方法

GET: 获取URL指定的资源;
POST:传输实体信息
PUT:上传文件
DELETE:删除文件
HEAD:获取报文首部,与GET相比,不返回报文主体部分
OPTIONS:询问支持的方法
TRACE:追踪请求的路径;
CONNECT:要求在与代理服务器通信时建立隧道,使用隧道进行TCP通信。主要使用SSL和TLS将数据加密后通过网络隧道进行传输。

HTTP应答状态码

状态码 类别 描述
1xx Informational(信息性状态码) 请求正在被处理
2xx Success(成功状态码) 请求处理成功
3xx Redirection(重定向状态码) 需要进行重定向
4xx Client Error(客户端状态码) 服务器无法处理请求
5xx Server Error(服务端状态码) 服务器处理请求时出错

image

HTTP2

优势:

  • 多路复用:在一个TCP连接中将所有请求并发完成。
  • 服务端推送:服务端可以在客户端确认需要资源(js css)之前把资源推送到客户端,而不是客户端解析了HTML之后再做请求
  • 数据流优先(Stream priority):HTTP/2采用二进制格式传输数据,而不是HTTP/1.x的那种文本格式。
  • 报头压缩:HTTP/2对报头采用 HPACK 算法进行压缩后传输,节省流量。
  • 强制加密:虽然加密不是强制的,但是多数浏览器都是通过TLS(HTTPS)实现的HTTP/2

二进制分帧层 (Binary Framing Layer)

在应用层与传输层之间增加一个二进制分帧层,以此达到在不改动 HTTP 的语义,HTTP 方法、状态码、URI 及首部字段的情况下,突破HTTP1.1 的性能限制,改进传输性能,实现低延迟和高吞吐量。在二进制分帧层上,HTTP2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码,其中 HTTP1.x 的首部信息会被封装到 Headers 帧,而我们的 request body 则封装到 Data 帧里面。
image

多路复用

对于 HTTP/1.x,即使开启了长连接,请求的发送也是串行发送的,在带宽足够的情况下,对带宽的利用率不够,HTTP/2.0 采用了多路复用的方式,可以并行发送多个请求,提高对带宽的利用率。
image

HTTPS

HTTP协议采用明文传输信息,存在信息窃听、信息篡改和信息劫持的风险,而协议TLS/SSL具有身份验证、信息加密和完整性校验的功能,可以避免此类问题发生。
TLS/SSL全称安全传输层协议Transport Layer Security,是介于TCP和HTTP之间的一层安全协议,不影响原有的TCP协议和HTTP协议,所以使用HTTPS基本上不需要对HTTP页面进行太多的改造。

image

详细的工作流程:

image

图解HTTPS基本原理
image

参考资料

【JavaScript】面向对象之继承

image

类(构造函数)

构造函数创建实例对象的过程和工厂模式类似

function createPerson(name) {
  var person = new Object();
  person.name = name;
  person.getName = function() {
    return this.name;
  }
  return person;
}

let person1 = createPerson('ziyi1');
let person2 = createPerson('ziyi2');

console.log(person1.getName === person2.getName);  // => false

工厂模式虽然抽象了创建具体对象的过程,解决了创建多个相似对象的问题,但是没有解决对象的识别问题,即如何知道对象的类型,而类(构造函数)创建的实例对象可以识别实例对象对应哪个原型对象(需要注意原型对象是类的唯一标识,当且仅当两个对象继承自同一个原型对象,才属于同一个类的实例,而构造函数并不能作为类的唯一标识)。

构造函数的创建过程:

  • 创建一个新对象
  • 将构造函数的作用域赋给新对象(this 新对象)
  • 执行构造函数中的代码
  • 返回新对象(最终返回的就是 new 出来的实例对象,因此 this 指向实例对象)

原型链继承

基本思路:
利用原型让一个引用类型继承另一个引用类型的属性和方法。即重写原型对象,代之以一个新类型的实例。

function SuperType() {
    this.name = 'Tom';
    this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.getName = function () {
    return this.name;
}
function SubType() {
    this.age = 22;
}
SubType.prototype = new SuperType();
SubType.prototype.getAge = function() {
    return this.age;
}
SubType.prototype.constructor = SubType;
let instance1 = new SubType();
instance1.colors.push('yellow');
console.log(instance1.getName()); //'Tom'
console.log(instance1.colors);//[ 'pink', 'blue', 'green', 'yellow' ]

let instance2 = new SubType();
console.log(instance2.colors);//[ 'pink', 'blue', 'green', 'yellow' ]

缺点:

  1. 通过原型来实现继承时,原型会变成另一个类型的实例,原先的实例属性变成了现在的原型属性,该原型的引用类型属性会被所有的实例共享。
  2. 在创建子类型的实例时,没有办法在不影响所有对象实例的情况下给超类型的构造函数中传递参数。

构造函数继承

基本思路:
在子类型的构造函数中调用超类型构造函数。

function SuperType(name) {
  this.name = name;
  this.colors = ["pink", "blue", "green"];
}
function SubType(name) {
  SuperType.call(this, name);
}
let instance1 = new SubType("Tom");
instance1.colors.push("yellow");
console.log(instance1.colors); // => ['pink', 'blue', 'green', yellow]

let instance2 = new SubType("Jack");
console.log(instance2.colors); // => ['pink', 'blue', 'green']

优点:

  1. 可以向超类传递参数
  2. 解决了原型中包含引用类型值被所有实例共享的问题

缺点:

  1. 方法都在构造函数中定义,函数复用无从谈起,另外超类型原型中定义的方法对于子类型而言都是不可见的。
  2. 父类的方法没有被共享,造成内存浪费

组合继承(原型链+构造函数)

组合继承指的是将原型链和借用构造函数技术组合到一块,从而发挥二者之长的一种继承模式。

基本思路:
使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承,既通过在原型上定义方法来实现了函数复用,又保证了每个实例都有自己的属性。

image

function SuperType(name) {
    this.name = name;
    this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
    console.log(this.name);
}
function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
    console.log(this.age);
}
let instance1 = new SubType('Tom', 20);
instance1.colors.push('yellow');
console.log(instance1.colors); // => [ 'pink', 'blue', 'green', 'yellow' ]
instance1.sayName(); // => Tom

let instance2 = new SubType('Jack', 22);
console.log(instance2.colors); // => [ 'pink', 'blue', 'green' ]
instance2.sayName();// => Jack

优点:

  1. 可以向超类传递参数
  2. 每个实例都有自己的属性
  3. 实现了函数复用

缺点:

  1. 无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
  2. 子类的原型对象中还存在父类实例对象的实例属性。只是因为子类实例中有同名属性,所以没有继续往子类的原型对象上去找属性。

原型式继承

基本**:
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

function objectCreate(o) { 
	function F() { } 
	F.prototype = o; 

	return new F();
}

在 objectCreate() 函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例,从本质上讲,objectCreate() 对传入的对象执行了一次浅拷贝。

ECMAScript5 通过新增 Object.create(proto[, propertiesObject])方法规范了原型式继承。这个方法接收两个参数:

  • proto:新创建对象的原型对象
  • propertiesObject:可选,新对象定义额外属性的对象(可以覆盖原型对象上的同名属性)

在传入一个参数的情况下,Object.create() 和 objectCreate() 方法的行为相同。

var person = {
    name: 'Tom',
    hobbies: ['reading', 'photography']
}
var person1 = Object.create(person);
person1.name = 'Jack';
person1.hobbies.push('coding');
var person2 = Object.create(person);
person2.name = 'Echo';
person2.hobbies.push('running');

console.log(person.hobbies); // => [ 'reading', 'photography', 'coding', 'running']
console.log(person1.hobbies);// => [ 'reading', 'photography', 'coding','running']

优点
在没有必要创建构造函数,仅让一个对象与另一个对象保持相似的情况下,原型式继承是可以胜任的。

缺点:
同原型链实现继承一样,包含引用类型值的属性会被所有实例共享。

寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

function createAnother(original) {
    var clone = Object.create(original);//通过调用函数创建一个新对象
    clone.sayHi = function () {         //以某种方式增强这个对象
        console.log('hi');
    };
    return clone;                       //返回这个对象
}
var person = {
    name: 'Tom',
    hobbies: ['reading', 'photography']
};

var person2 = createAnother(person);
person2.sayHi(); //hi

基于 person 返回了一个新对象 -—— person2,新对象不仅具有 person 的所有属性和方法,而且还有自己的 sayHi() 方法。在考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。

缺点:

  • 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而效率低下。
  • 同原型链实现继承一样,包含引用类型值的属性会被所有实例共享。

寄生组合式继承

image

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,基本思路:

不必为了指定子类型的原型而调用超类型的构造函数,我们需要的仅是超类型原型的一个副本,本质上就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示:

function inheritPrototype(subType, superType) {
    var prototype = objectCreate(superType.prototype); // 创建对象
    prototype.constructor = subType;                   // 增强对象
    subType.prototype = prototype;                     // 指定对象
}
  • 第一步:创建超类型原型的一个副本
  • 第二步:为创建的副本添加 constructor 属性
  • 第三步:将新创建的对象赋值给子类型的原型

至此,我们就可以通过调用 inheritPrototype 来替换为子类型原型赋值的语句:

function SuperType(name) {
  this.name = name;
  this.colors = ['pink', 'blue', 'green'];
}

SuperType.prototype.sayName = function () {console.log(this.nmae)}
function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}

SubType.prototype = new SubType();
inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function () {console.log(this.age)}

优点:
只调用了一次超类构造函数,效率更高。避免在SubType.prototype上面创建不必要的、多余的属性,与其同时,原型链还能保持不变。

寄生组合继承是引用类型最理性的继承范式

class继承(es6)

ES6 中的类只是 ES5 封装后的语法糖而已。

// ES6中实现的Person类
class Es6Person {
  constructor(name, age) {
    // 实例对象的属性
    this.name = name
    this.age = age
    // 实例对象的方法
    this.getName = () => {
      return this.name
    }
  }

  // Person类原型对象的方法
  getAge() {
    return this.age
  }
}


// ES5中实现的Person类
function Es5Person (name, age) {
  // 实例对象的属性
  this.name = name
  this.age = age
  // 实例对象的方法
  this.getName = () => {
    return this.name
  }
}

// Person类原型对象的方法
Es5Person.prototype.getAge = function() {
  return this.age
}

在 ES5 中类的原型对象的方法是可枚举的,但是 ES6 中不可枚举:

console.log(Object.keys(Es6Person.prototype));   // => []
console.log(Object.keys(Es5Person.prototype));   // => ["getAge"]

在 ES5 中如果不用 newthis 指向 window 全局变量,在 ES6 如果不用 new 关键字则会报错处理:

// Uncaught TypeError: Class constructor Person cannot be invoked without 'new'
let person2 = Es6Person('ziyi2', 111)

ES6 中的类是不会声明提升的,ES5 可以:

console.log(Es5Person);     // => Es5Person {}
let es6 = new Es6Person();  // => Uncaught ReferenceError: Es6Person is not defined
console.log(es6);

class Es6Person {}
function Es5Person {}

在 ES6 中如果不写构造方法:

class Es6Person {}

// 等同于
class Es6Person {
  constructor() {}
}

在 ES6 中类的属性名可以采用表达式:

const getAge = Symbol('getAge')

class Es6Person {
  constructor(name, age) {
    this.name = name
    this.age = age
    this.getName = () => {
      return this.name
    }
  }
  
  // 表达式
  [getAge]() {
    return this.age
  }
}

let es6Person = new Es6Person('ziyi2', 28)
es6Person[getAge]()

ES5 的继承使用借助构造函数实现,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面。ES6 的继承机制完全不同,实质是先创造父类的实例对象 this(所以必须先调用 super 方法),然后再用子类的构造函数修改 this

子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super 方法,子类就得不到 this 对象。

ES6 extends 继承做了什么操作

看这个例子:

// ES6
class Parent{
    constructor(name){
        this.name = name;
    }
    static sayHello(){
        console.log('hello');
    }
    sayName(){
        console.log('my name is ' + this.name);
        return this.name;
    }
}
class Child extends Parent{
    constructor(name, age){
        super(name);
        this.age = age;
    }
    sayAge(){
        console.log('my age is ' + this.age);
        return this.age;
    }
}
let parent = new Parent('Parent');
let child = new Child('Child', 18);

用原型图表示:
image

结合代码和图可以知道。
ES6 extends 继承,主要就是:

  1. 把子类构造函数(Child)的原型(__proto__)指向了父类构造函数(Parent)
  2. 把子类实例(child)的原型对象(Child.prototype) 的原型(__proto__)指向了父类(parent)的原型对象(Parent.prototype)。

上面这两点也就是图中用不同颜色标记的两条线。

  1. 子类构造函数(Child)继承了父类构造函数(Preant)的里的属性。使用 super 调用的(ES5 则用 call 或者 apply 调用传参)。也就是图中用不同颜色标记的两条线。

把上面的例子转化为 ES5(也即是寄生组合式继承的方法):

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

Parent.sayHello = function() {
  console.log("hello");
}

Parent.prototype.sayName = function() {
  console.log("my name is " + this.name);
  return this.name;
}

function Child(name, age) {
  // 相当于super
  Parent.call(this, name);
  this.age = age;
}

function _inherits(Child, Parent) {
  // Object.create
  Child.prototype = Object.create(Parent.prototype);
  // __proto__
  // Child.prototype.__proto__ = Parent.prototype;
  Child.prototype.constructor = Child;
  // ES6
  // Object.setPrototypeOf(Child, Parent);
  // __proto__
  Child.__proto__ = Parent;
}
_inherits(Child, Parent);

Child.prototype.sayAge = function() {
  console.log("my age is " + this.age);
  return this.age;
}

let parent = new Parent("Parent");
let child = new Child("Child", 18);

参考资料

【CSS】水平垂直居中

子元素不确定大小的情况

方法一:flex

.parent {
	display: flex;
    justify-content: center; // 主轴居中
    align-items: center;     // 交叉轴居中
}

缺点:flex 布局只支持现代浏览器。

方法二:absoulte + transform

.parent {
    position: relative;
}

.child {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

缺点:CSS3 属性只支持现代浏览器。

方法三:table-cell

.parent {
    display: table-cell;
    text-align: center;
    vertical-align: middle;
}

.child {
    display: inline-block; // 必须是行内元素
}

缺点:子元素必须是行内元素。不然只会垂直居中。table-cell 布局会引起一些不必要的麻烦。

【浏览器】从输入一个URL到网页展示的过程

image


浏览器请求与服务器响应

image

DNS 域名解析

在网络世界,你肯定记得住网站的名称,但是很难记住网站的 IP 地址,因而也需要一个地址簿,就是 DNS 服务器。DNS 服务器是高可用、高并发和分布式的,它是树状结构,如图:

image

  • 根 DNS 服务器 :返回顶级域 DNS 服务器的 IP 地址
  • 顶级域 DNS 服务器:返回权威 DNS 服务器的 IP 地址
  • 权威 DNS 服务器 :返回相应主机的 IP 地址

DNS的域名查找,在客户端和浏览器,本地DNS之间的查询方式是递归查询;在本地DNS服务器与根域及其子域之间的查询方式是迭代查询;

递归过程:

image

在客户端输入 URL 后,会有一个递归查找的过程,从浏览器缓存中查找->本地的hosts文件查找->找本地DNS解析器缓存查找->本地DNS服务器查找,这个过程中任何一步找到了都会结束查找流程。

结合起来的过程,可以用一个图表示:
image

建立TCP连接

首先,判断是不是https的,如果是,则HTTPS其实是HTTP + SSL / TLS 两部分组成,也就是在HTTP上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据都是加密后的数据。

浏览器渲染过程

image

浏览器渲染过程:

  1. 解析HTML,生成DOM树,解析CSS,生成CSSOM树
  2. 将DOM树和CSSOM树结合,生成渲染树(Render Tree)
  3. Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
  4. Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
  5. Display:将像素发送给GPU,展示在页面上。(这一步其实还有很多内容,比如会在GPU将多个合成层合并为同一个层,并展示在页面中,而css3硬件加速的原理则是新建合成层)

生成渲染树

image

为了构建渲染树,浏览器主要完成了以下工作:

  1. 从DOM树的根节点开始遍历每个可见节点。
  2. 对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。
  3. 根据每个可见节点以及其对应的样式,组合生成渲染树。

第一步中,既然说到了要遍历可见的节点,那么我们得先知道,什么节点是不可见的。不可见的节点包括:

  • 一些不会渲染输出的节点,比如 scriptmetalink等。
  • 一些通过css进行隐藏的节点。比如display: none。注意,利用 visibility opacity 隐藏的节点,还是会显示在渲染树上的。只有 display: none 的节点才不会显示在渲染树上。

注意:渲染树只包含可见的节点

浏览器加载JavaScript脚本

正常加载流程

浏览器加载JavaScript脚本,主要通过<script>元素完成。其正常流程如下

  1. 浏览器的呈现引擎持有渲染的控制权,它正常解析HTML页面
  2. 解析遇到<script>标签,呈现引擎移交控制权给Javascript引擎(例如chrome的V8)
  3. 如果<script>标签引用了外部脚本那就先下载再执行,否则直接执行代码
  4. JavaScript引擎执行完毕移交控制权给呈现引擎,呈现引擎继续解析

加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后,再继续渲染。原因是 JavaScript 代码可以修改 DOM,所以必须把控制权让给它,否则会导致复杂的线程竞赛的问题。

defer属性

浏览器解析到包含defer属性的<script>元素时,其运行流程如下

  1. 浏览器的呈现引擎持有渲染的控制权,它正常解析HTML页面
  2. 解析遇到包含defer属性的<script>标签,继续解析HTML,同时并行下载外链脚本
  3. 解析完成,文档处于交互状态时开始解析处于deferred模式的脚本
  4. 脚本解析完毕后,将文档状态设置为完成,DOMContentLoaded事件随之触发

使用defer属性时需要注意的点:

  • defer属性下载的脚本文件在DOMContentLoaded事件触发前执行(即刚刚读取完</html>标签)
  • defer属性可以保证执行顺序就是它们在页面上出现的顺序
  • 对于内置而不是加载外部脚本的script标签,以及动态生成的script标签,defer属性不起作用
  • 使用defer加载的外部脚本不应该使用document.write方法

async属性

浏览器解析到包含async属性的<script>元素时,其运行流程如下

  1. 浏览器的呈现引擎持有渲染的控制权,它正常解析HTML页面
  2. 解析遇到包含async属性的<script>标签,继续解析HTML,让另一进程同时并行下载外链脚本
  3. 脚本下载完成,浏览器暂停解析HTML,开始执行下载的脚本
  4. 脚本执行完毕,浏览器恢复解析HTML

使用async属性时需要注意的点:

  • async属性可以保证脚本下载的同时,浏览器继续渲染
  • async属性无法保证脚本的执行顺序,哪个先下载结束就先执行哪一个
  • 包含async属性的脚本不应该使用document.write方法
  • 如果同时使用async和defer属性,后者不起作用,浏览器行为由async属性决定

async、defer与不加的区别

一图胜千言: 原图地址

image

脚本的动态加载

<script>元素还可以动态生成,生成后再插入页面,从而实现脚本的动态加载。动态生成的script标签不会阻塞页面渲染,也就不会造成浏览器假死。但是问题在于,这种方法无法保证脚本的执行顺序,哪个脚本文件先下载完成,就先执行哪个。如果想避免这个问题,可以设置async属性为false。还可以监听脚本的onload事件来为脚本指定回调。

CSS阻塞JS加载

因为JS脚本可能会引用DOM的样式做计算,所以为了保证脚本计算的正确性,Firefox浏览器会等到脚本前面的所有样式表,都下载并解析完,再执行脚本;Webkit则是一旦发现脚本引用了样式,就会暂停执行脚本,等到样式表下载并解析完,再恢复执行。

此外,对于来自同一个域名的资源,比如脚本文件、样式表文件、图片文件等,浏览器一般有限制,同时最多下载6~20个资源,即最多同时打开的 TCP 连接有限制,这是为了防止对服务器造成太大压力。如果是来自不同域名的资源,就没有这个限制。所以,通常把静态文件放在不同的域名之下,以加快下载速度。

DOMContentLoaded 与 load 的区别

  • 当 DOMContentLoaded 事件触发时,仅当 DOM 解析完成后,不包括样式表、图片。我们前面提到 CSS 加载会阻塞 Dom 的渲染和后面 js 的执行,js 会阻塞 Dom 解析,所以我们可以得到结论:当文档中没有脚本时,浏览器解析完文档便能触发 DOMContentLoaded 事件。如果文档中包含脚本,则脚本会阻塞文档的解析,而脚本需要等 CSSOM 构建完成才能执行。在任何情况下,DOMContentLoaded 的触发不需要等待图片等其他资源加载完成。
  • 当 onload 事件触发时,页面上所有的 DOM、样式表、脚本、图片等资源已经加载完毕。
  • DOMContentLoaded -> load。

参考资料

【JavaScript】数据类型基础

image


JavaScript 中的数据类型

image

  • 基本类型:string / number / boolean / null / undefined / symbol(es6) / bigInt(es10)
  • 引用类型:object / array / function

堆和栈

  • 程序运行的时候,需要内存空间存放数据。一般来说,系统会划分出两种不同的内存空间:一种叫做堆(heap),另一种叫做栈(stack)
  • 堆(heap)是没有结构的,数据可以任意存放,它是用于存放复杂数据类型(引用类型)的,例如数组对象、object对象等。
  • 栈(stack)是有结构的,每个区块按照一定次序存放(后进先出),栈中主要存放的是基本类型的变量的值以及指向堆中的数组或者对象的地址,存在栈中的数据大小与生存期必须是确定的。除此之外,还可以明确知道每个区块的大小,因此,stack 的寻址速度要快于 heap。
  • 栈创建的时候,大小是确定的,如果超出了浏览器规定的栈限制,就会报 stack overflow 错误,而堆的大小是不确定的,需要的话可以不断增加。

基本数据类型与引用数据类型

基本数据类型

  1. 值是不可变的

  2. 存放在栈区中

  3. 值的比较

    == : 只进行值的比较,会进行数据类型的转换。
    === : 不仅进行值得比较,还要进行数据类型的比较。

引用数据类型

  1. 值是可变的

  2. 同时保存在栈内存和堆内存
    image

  3. 比较是引用的比较

第六种基本数据类型:Symbol

ES6新增,表示独一无二的值。这个知识点以后再深入学习。

第七种基本数据类型:BigInt

BigInt数据类型的目的是比 Number 数据类型支持的范围更大的整数值。在对大整数执行数学运算时,以任意精度表示整数的能力尤为重要。使用 BigInt,整数溢出将不再是问题。

JS 中的 Number 类型只能安全地表示 -9007199254740991 (-(2^53-1))9007199254740991(2^53-1) 之间的整数,任何超出此范围的整数值都可能失去精度。

console.log(9999999999999999);            // → 10000000000000000
    
// 注意最后一位的数字
9007199254740992 === 9007199254740993;    // → true

目前的开发中,此数据类型比较少用,更多详情参考此篇文章:JS最新基本数据类型:BigInt

undefinednull 的区别

相同:

  • 都是基本类型
  • 都是虚值: Boolean(value)或者 !!value转换为布尔值时为 false

不同:

  • undefined 是未指定特定值的变量的默认值,或者没有显式返回值的函数
  • null 是“不代表任何值的值”
  • 在比较 nullundefined 时,我们使用 == 时得到 true,使用 === 时得到 false:
      console.log(null == undefined); // true
      console.log(null === undefined); // false

类型转换

显示转换与隐式转换

显示类型转换

尽管JavaScript可以自动做许多类型转换,但有时仍需要做显式转换,或者为了使代码变得清晰易读而做显式转换。

做显式类型转换最简单的方法就是使用 Boolean()Number()String()Object() 函 数。

Number( "3“ )  // 3
String(false)  // "false" 或使用false.toString()
Object(3) // Number(3)
Boolean([])  // true
Boolean('')  //  false

ECMA中,Boolean([])Boolean('') 的解释:

image

Boolean() 用的是 ToBoolean 方法;
参数为 String 中,string 长度为 0,也就是空字符串 ”” 时,Boolean 显式转换为 false,非空字符串为 true
而参数只要是 Object 类型,都被 Boolean 显示转换成 true,而众所周知,[] 数组属于 Object 类型的一种。

+ 是将字符串转换为数字的最快方法,因为如果值已经是数字,它不会执行任何操作。

隐式类型转换

JavaScript中的某些运算符会做隐式的类型转换:

x+"" // 等价于 String(x)  如88 + '6' => ’886’
+x // 等价于 Number(x).也可以写成x-0  如+'886' => number类型的886
!!x // 等价于Boolean(x) 如 !!'886' => true

检验数据类型

1. typeof

适用场景

typeof 操作符可以准确判断一个变量是否为下面几个原始类型:

typeof 'ConardLi'  // string
typeof 123  // number
typeof true  // boolean
typeof Symbol()  // symbol
typeof undefined  // undefined

还可以用它来判断函数类型:

typeof function(){}  // function

不适用场景

当用 typeof 来判断引用类型时似乎显得有些乏力了:

typeof [] // object
typeof {} // object
typeof new Date() // object
typeof /^\d*$/; // object

代码除函数外所有的引用类型都会被判定为object。
另外typeof null === 'object'也会让人感到头痛,这是在JavaScript初版就流传下来的bug,后面由于修改会造成大量的兼容问题就一直没有被修复。

2. instanceof

instanceof 操作符可以帮助我们判断引用类型具体是什么类型的对象:

[] instanceof Array // true
new Date() instanceof Date // true
new RegExp() instanceof RegExp // true

[] instanceof Array 实际上是判断 Array.prototype 是否在 [] 的原型链上。所以,使用 instanceof 来检测数据类型,不会很准确,这不是它设计的初衷:

[] instanceof Object // true
function(){}  instanceof Object // true

另外,使用 instanceof 也不能检测基本数据类型,所以 instanceof 并不是一个很好的选择。

3. toString

Object.prototype.toString.call() 最准确最常用的方式。首先获取 Object 原型上的 toString 方法,让方法执行,让 toString 方法中的 this 指向第一个参数的值。

每一个引用类型都有 toString 方法,默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中 type 是对象的类型。事实上,大部分引用类型比如Array、Date、RegExp等都重写了toString方法。因此可以直接调用 Object 原型上未被覆盖的 toString() 方法,使用 call 来改变 this 指向来达到我们想要的效果。

Object.prototype.toString.call('') ;   // [object String]
Object.prototype.toString.call(1) ;    // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window是全局对象global的引用

四种相等性算法

  1. 严格相等比较 ===, indexOf, lastIndexOf 认为 NaNNaN 不等,+0-0 相等
  2. 非严格相等比较 ==
  3. 同值 Set,Map,include等 认为 NaNNaN 相等,+0-0 相等
  4. 同值 Object.is 认为 NaNNaN 相等,+0-0 不等

参考资料

【Webpack】常用 loaders 与配置

常用 loaders

  • 加载文件
    • raw-loader:把文本文件的内容加载到代码中去。
    • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件。
    • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去。
    • svg-inline-loader:把压缩后的 SVG 内容注入到代码中。
    • image-loader:加载并且压缩图片文件。
    • json-loader:加载 JSON 文件。
  • 转换脚本语言
  • 转换样式文件
    • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性。
    • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
    • sass-loader:把 SCSS/SASS 代码转换成 CSS。
    • postcss-loader:扩展 CSS 语法,使用下一代 CSS。
    • less-loader:把 Less 代码转换成 CSS 代码。
  • 检查代码
  • 其它
    • vue-loader:加载 Vue.js 单文件组件。
    • i18n-loader:加载多语言版本,支持国际化。
    • ignore-loader:忽略掉部分文件。
    • ui-component-loader:按需加载 UI 组件库,例如在使用 antd UI 组件库时,不会因为只用到了 Button 组件而打包进所有的组件。

loader 的使用

loader 可以配置以下参数:

  • test: 匹配处理文件的扩展名的正则表达式
  • use: loader 名称
  • include/exclude: 手动指定必须处理的文件夹或屏蔽不需要处理的文件夹
  • query: 为 loader 提供额外的设置选项

loader 是链式调用,所以执行顺序是从右到左的。

转换脚本语言

babel-loader

presets 属性告诉 Babel 要转换的源码使用了哪些新的语法特性,一个 Presets 对一组新语法特性提供支持,多个 Presets 可以叠加。 Presets 其实是一组 Plugins 的集合,每一个 Plugin 完成一个新语法的转换工作。Presets 是按照 ECMAScript 草案来组织的。

preset description
@babel/preset-env env 包含当前所有 ECMAScript 标准里的最新特性
@babel/preset-react 支持 React 开发中的 JSX 语法

需要安装:

//安装插件 --@babel/core是babel核心库
npm install babel-loader @babel/core -D

//同时要按装,babel-loader只是打通webpack的一个桥梁,并不会转义代码,需要借助@babel/preset-env 来做语法的转换
npm install @babel/preset-env -D

//还有继续安装@babel/polyfill -- 作用是帮助低版本的浏览器弥补缺失的变量以及函数,同时可以在options配置,根据业务代码来做低版本的缺失弥补,这样打包代码可以做到精简,注意的是,这个插件只适合做业务代码带包,因为会污染全局
npm install @babel/polyfill -D

1.配置 .babelrc

weback.config.js

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                use: 'babel-loader'
            }
        ]
    }
};

项目根目录下增加 .babelrc 配置文件
.babelrc

{
    "presets": ["@babel/preset-env","@babel/preset-react"]
}

2. 直接配置在 babel-loader 中

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        "presets": ["@babel/preset-env", "@babel/preset-react"]
                    }
                }
            }
        ]
    }
};

样式文件的转换

css最基本的编译要依靠于style-loader、css-loader这两个加载器,所谓最基本,就是在不考虑使用css预处理器以及css后处理器的情况。预处理器包括less、scss、sass、stylus,后处理器如postcss的autoprefixer等。首先我们先弄明白各加载器的作用:

  • style-loader,将所有处理好的css样式以行内元素style标签的格式动态注入到界面的head标签中去。
  • css-loader,用来处理css样式,例如把css中类似@import()或者url()这样的引用资源进行引入或者处理。
  • autoprefixer,postcss提供用来自动处理css在不同浏览器之间前缀等问题,从这里我们也可以看出,使用autoprefixer需要先引入postcss。
  • 预处理-loader,所有预处理器都是以css为终点生成文件的一种专门的编程语言,为css增加了一些编程特性。

css-loader

  • 只负责加载 css 模块,不会将加载的 css 样式应用到 html
  • importLoaders 用于指定在 css-loader 前应用的 loader 的数量
  • 查询参数 modules 会启用 CSS 模块规范

webpack.config.js

module: {
  rules: [
    {
      test: /\.css$/,
      use: ["style-loader", "css-loader"]
    }
  ];
}

style-loader

  • 负责将 css-loader 加载到的 css 样式动态的添加到 html-head-style 标签中
  • 一般建议将 style-loader 与 css-loader 结合使用

postcss-loader

autoprefixer 是 css 后处理器 postcs s提供的一个对 css3 中个别属性在不同浏览器下需要添加浏览器前缀的样式处理工具,因此在使用 autoprefixer 之前,我们需要安装 postcss-loader 来加载它。

webpack.config.js

module: {
        rules: [{
            test: /\.css$/,
            use: [
                { loader: "style-loader" },
                { loader: "css-loader",options: { importLoaders: 1 } },//importLoaders解决由于css-loader处理文件导入的方式导致postcss-loader不能正常使用的问题
                { loader: "postcss-loader" } //指定postcss加载器
            ],
            exclude: /node_modules/
        }]
    }

根目录下新建 postcss.config.js 配置文件:

module.exports = {
    plugins: [
        require("autoprefixer")({overrideBrowserslist:'last 5 version'}) // overrideBrowserslist最终生成的css兼容最近的N个版本
    ]
}

需要注意,由于css-loader处理文件导入的方式,因此加载器postcss-loader不能与CSS模块一起使用。 为了使它们正常工作,可以添加 css-loader 的importLoaders选项。

也可以直接写在loader里:

{ loader: 'postcss-loader', options: { plugins: loader => [ require('autoprefixer')({overrideBrowserslist: "last 5 version"}) ] } }
module.exports = {
    module: {
        rules: [
            {
                test: /\.less/,
                use: ['style-loader', 'css-loader', 'less-loader']
            }
        ]
    }
};

loader配置的顺序不可调换,因为 less-loader 是把 less 转换成 css,而 css-loader 是解析处理 css 里的 url 路径,并将 css 文件转换成一个模块,style-loader 是将 css 文件变成 style 标签插入到 head 中。

如果要转换 less 或者 sass,那么除了 loader,还需要安装 less 或者 sass:

npm install less less-loader -D

分离 css 文件

安装 mini-css-extract-plugin,将 'style-loader' 修改为 {loader: MiniCssExtractPlugin.loader}

webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    module: {
        rules: [
            {
                test: /\.less/,
                use: [{loader: MiniCssExtractPlugin.loader}, 'css-loader', 'less-loader']
            }
        ]
    },
	plugins: [
		new MiniCssExtractPlugin({
            filename: 'css/[name].css'
        })
	]
};

此插件为每个包含 CSS 的 JS 文件创建一个单独的 CSS 文件,并支持 CSS 和 SourceMap 的按需加载。
注意:这里说的每个包含 CSS 的 JS 文件,并不是说组件对应的 JS 文件,而是打包之后的 JS 文件.

加载文件

file-loader

  • file-loader 可以解析项目中的 url 引入(不仅限于 css),根据我们的配置,将图片拷贝到相应的路径,再根据我们的配置,修改打包后文件引用路径,使之指向正确的文件。
  • 默认情况下,生成的文件的文件名就是文件内容的 MD5 哈希值并会保留所引用资源的原始扩展名。

webpack.config.js

module.exports = {
    module: {
        rules: [
            {
                test: /.(png|jpg)$/,  // 图片或者字体等文件
                use: [
                    {
                          loader: "file-loader",
					      options: {
					        name: "[name]_[hash].[ext]",
					        outputPath: "images/"
					      }
                        }
                    }
                ]
            }
        ]
    }
};

url-loader

  • url-loader 功能类似于 file-loader,但是在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL。
  • url-loader 把资源文件转换为 URL,file-loader 也是一样的功能。不同之处在于 url-loader 更加灵活,它可以把小文件转换为 base64 格式的 URL,从而减少网络请求次数。url-loader 依赖 file-loade

webpack.config.js

module.exports = {
    module: {
        rules: [
            {
                test: /.(png|jpg)$/,  // 图片或者字体等文件
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 10240,    // 处理成 base64 的边界值,单位是字节
							outputPath: 'images' //如果希望图片存放在单独的目录下,那么需要指定outputPath
                        }
                    }
                ]
            }
        ]
    }
};

参考文档

【JavaScript】作用域与作用域链

作用域

所谓作用域,指的是变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。

作用域就是一套规则,用于确定在何处以及如何查找变量(标识符)。

JS 中的作用域是词法作用域,是由函数声明时所在的位置决定的。词法作用域是指在编译阶段就产生的,一整套函数标识符的访问规则。说到底 JS 的作用域只是一个“空地盘”,其中并没有真实的变量,但是却定义了变量如何访问的规则。(词法作用域是在编译阶段就确认的,区别于词法作用域,动态作用域是在函数执行的时候确认的,JS 没有动态作用域,但 JS 的 this 很像动态作用域。语言也分为静态语言和动态语言,静态语言是指数据类型在编译阶段就确定的语言如 Java,动态语言是指在运行阶段才确定数据类型的语言如 JS。)

LHS 和 RHS

引擎查找变量的过程会进行两种类型的查询:LHS 查询和 RHS 查询,分别代表赋值操作的左侧和右侧查询。

  • LHS 查询:查询的目的是对变量进行赋值(或写入内存)
  • RHS 查询:查询的目的是获取变量的值(或从内存中读取)

两者的特性:

  • 都会在所有作用域中查询
  • 严格模式下,找不到所需的变量时,引擎都会抛出 ReferenceError 异常。
  • 非严格模式下,LHR 稍微比较特殊: 会自动创建一个全局变量
  • 查询成功时,如果对变量的值进行不合理的操作,比如:对一个非函数类型的值进行函数调用,引擎会抛出TypeError异常

JavaScript是如何执行的

image

  • 核心重点:变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
  • 函数运行的瞬间,创建一个AO (Active Object 活动对象)运行载体。

执行上下文

JavaScript 的可执行代码(executable code)的类型就三种,全局代码、函数代码、eval代码。

当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。

对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

变量对象

变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
在函数上下文中,用活动对象(activation object, AO)来表示变量对象。

函数声明特点:AO上如果有与函数名同名的属性,则会被此函数覆盖。

  • 全局上下文的变量对象初始化是全局对象
  • 函数上下文的变量对象初始化只包括 Arguments 对象
  • 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
  • 在代码执行阶段,会再次修改变量对象的属性值

执行上下文栈

JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。

作用域链

函数对象和其它对象一样,拥有可以通过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是[[Scope]],由ECMA-262标准第三版定义,该内部属性包含了函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问。

JavaScript 引擎在查找变量时,会先从当前执行上下文的变量对象中去找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中去找,一直找到全局上下文的变量对象,也就是全局对象,由多个执行上下文的变量对象构成的一个链表就叫作用域链。

也就是说,作用域链查找是从作用域链最底层开始查找,当遇到同名变量时,查找到的是距离当前执行上下文最近的变量。

var name = 'Tom'
function () {
  var name = 'Jack'
  console.log(name) // Jack
}

ReferenceError 和 TypeError 异常

ReferenceError 与作用域判别失败有关,例如:

  • RHS查询失败时,即查询所有嵌套作用域都找不到变量时
  • 严格模式下,LHS查询失败时也会抛出 ReferenceError 异常,而在非严格模式下,会导致隐式地创建一个全局变量

TypeError 代表作用域判别成功了,但是对结果操作非法或不合理,例如:

  • 对一个非函数类型的值进行调用
  • 引用 nullundefined 中的属性

参考链接

【浏览器】JS Event 事件机制

javaScript 与 HTML 之间的交互是通过事件实现的。

事件捕获和事件冒泡

定义:
事件冒泡和事件捕获,这两个概念都是为了解决页面中事件流(事件发生顺序)的问题。

先看一张W3的图:
https://www.w3.org/TR/DOM-Level-3-Events/images/eventflow.svg

绿色的流程就是事件冒泡的过程:事件会从最内层的元素开始发生,一直向上传播,直到document对象。
红色的流程就是事件捕获的过程:事件从最外层开始发生,直到最具体的元素。

image
1-5是捕获过程,5-6是目标阶段,6-10是冒泡阶段。

事件代理

可以查看这个 demo,理解下为什么需要使用事件代理

使用事件代理的好处不仅在于将多个事件处理函数减为一个,而且对于不同的元素可以有不同的处理方法。

主流浏览器默认为事件冒泡

对于事件代理来说,在事件捕获或者事件冒泡阶段处理并没有明显的优劣之分。目前主流浏览器都是默认为事件冒泡,因此除非特殊需求,也尽量以事件冒泡来处理。

阻止事件冒泡

event.stopPropagation()

终止事件在传播过程的捕获、目标处理或起泡阶段进一步传播。
该方法将停止事件的传播,阻止它被分派到其他 Document 节点。在事件传播的任何阶段都可以调用它。注意,虽然该方法不能阻止同一个 Document 节点上的其他事件句柄被调用,但是它可以阻止把事件分派到其他节点。

event.target==event.currentTarget

让触发事件的元素等于绑定事件的元素,也可以阻止事件冒泡

阻止默认事件

event.preventDefault()

取消事件的默认动作。
该方法将通知 Web 浏览器不要执行与事件关联的默认动作(如果存在这样的动作)。

事件处理程序

vHTML事件处理程序

某个元素支持的每种事件,都可以使用一个与相应事件处理程序同名的 HTML 特性来指定。

例如,要在按钮被单击时执行一些 JavaScript,可以像下面 这样编写代码:

<input type="button" value="Click Me" onclick="alert(&quot;Clicked&quot;)" />

缺点:

  • 存在一个时差问题。因为用户可能会在 HTML 元素一出现在页面上就触发相应的事件,但当时的事件处理程序有可能尚不具备执行条件。
  • 这样扩展事件处理程序的作用域链在不同浏览器中会导致不同结果。
  • HTML 与 JavaScript 代码紧密耦合。如果要更换事 件处理程序,就要改动两个地方:HTML 代码和 JavaScript 代码。

DOM0 级事件处理程序

简单来说就是取得一 个要操作的对象的引用,然后为它指定了某事件处理程序(如onclick)。使用 DOM0 级方法指定的事件处理程序被认为是元素的方法,以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。

var btn = document.getElementById("myBtn"); 
btn.onclick = function(){ 
    alert(this.id); //"myBtn" 
};

btn.onclick = null;//删除事件处理程序

将事件处理程序设置为 null 之后,再单击按钮将不会有任何动作发生。

DOM2 级事件处理程序

“DOM2 级事件”定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener() 和 removeEventListener()。

onclick 事件会被覆盖,addevenlistener 事件先后执行。

同时支持了事件捕获阶段和事件冒泡阶段,因此我们可以自己控制事件处理函数在哪一个阶段被使用。

addEventListener 方法用来为一个特定的元素绑定一个事件处理函数。
addEventListener 有三个参数:

 element.addEventListener(event, function, useCapture)
参数 描述
event 必须。字符串,指定事件名。
注意: 不要使用 on 前缀。 例如,使用 click,而不是使用 onclick
提示: 所有 HTML DOM 事件,可以查看HTML DOM Event 对象参考手册
function 必须。指定要事件触发时执行的函数。
当事件对象会作为第一个参数传入函数。 事件对象的类型取决于特定的事件。例如, click 事件属于 MouseEvent(鼠标事件) 对象。
useCapture 可选。布尔值,指定事件是否在捕获或冒泡阶段执行。
true:事件句柄在捕获阶段执行(即在事件捕获阶段调用处理函数)
false (默认):事件句柄在冒泡阶段执行(即表示在事件冒泡的阶段调用事件处理函数)

事件类型

Web 浏览器中可能发生的事件有很多类型。如前所述,不同的事件类型具有不同的信息,而“DOM3 级事件”规定了以下几类事件。

  • UI(User Interface,用户界面)事件,当用户与页面上的元素交互时触发; 如resize,scroll
  • 焦点事件,当元素获得或失去焦点时触发; 如blur (在元素失去焦点时触发,这个事件不会冒泡)
  • 鼠标事件,当用户通过鼠标在页面上执行操作时触发;如click
  • 滚轮事件,当使用鼠标滚轮(或类似设备)时触发;
  • 文本事件,当在文档中输入文本时触发;
  • 键盘事件,当用户通过键盘在页面上执行操作时触发;
  • 合成事件,当为 IME(Input Method Editor,输入法编辑器)输入字符时触发;
  • 变动(mutation)事件,当底层 DOM 结构发生变化时触发。

其它知识

  • 不是所有的事件都能冒泡。以下事件不冒泡:blurfocusloadunload
  • 阻止冒泡并不能阻止对象默认行为。比如submit按钮被点击后会提交表单数据,这种行为无须我们写程序定制。
  • event.target 和 event.currentTarget 的区别:currentTarget 表示的,标识是当事件沿着 DOM 触发时事件的当前目标。它总是指向事件绑定的元素,而 event.target 则是事件触发的元素。

参考资料

【React】React 生命周期

为了更好的支持异步渲染(Async Rendering),解决一些生命周期滥用可能导致的问题,React 从 V16.3 开始,对生命周期进行渐进式调整,同时在官方文档也提供了使用的最佳实践。
可利用这个网站 React Lifecycle Methods diagram,查看各版本的生命周期图。

React 的生命周期大致分为:

  • 组件装载(Mount)组件第一次渲染到 Dom 树
  • 组件更新(update)组件 state,props 变化引发的重新渲染
  • 组件卸载(Unmount)组件从 Dom 树删除

从 React v16 开始,还对生命周期加入了错误处理(Error Handling)。

V16.3.0之前

创建阶段(Mounting)

  1. constructor(props, context)
  2. componentWillMount()
  3. render()
  4. componentDidMount()

更新阶段(Updating)

props发生变化时

  1. componentWillReceiveProps(nextProps, nextContext)
  2. shouldComponentUpdate(nextProps, nextState, nextContext)
  3. componentWillUpdate(nextProps, nextState, nextContext)
  4. render
  5. componentDidUpdate(prevProps, prevState, snapshot)

state发生变化时

  1. shouldComponentUpdate(nextProps, nextState, nextContext)
  2. componentWillUpdate(nextProps, nextState, nextContext)
  3. render
  4. componentDidUpdate(prevProps, prevState, snapshot)

卸载阶段(Unmounting)

  1. componentWillUnmount()

图示:
image

V16.3.0之后

创建阶段(Mounting)

  1. constructor(props, context)
  2. static getDerivedStateFromProps(props, status)
  3. render()
  4. componentDidMount()

更新阶段(Updating)

props / state 发生变化时

  1. static getDerivedStateFromProps(props, status)
  2. shouldComponentUpdate(nextProps, nextState, nextContext)
  3. render
  4. getSnapshotBeforeUpdate(prevProps, prevState)
  5. componentDidUpdate(prevProps, prevState, snapshot)

卸载阶段(Unmounting)

  1. componentWillUnmount()

图示:
image

删除生命周期

  1. componentWillMount
  2. componentWillReceiveProps
  3. componentWillUpdate

所有被删除的生命周期函数,目前还凑合着用,但是只要用了,开发模式下会有红色警告,在下一个大版本(也就是React v17)更新时会彻底废弃。会保留另外一种形式:

  1. UNSAFE_componentWillMount()
  2. UNSAFE_componentWillReceiveProps
  3. UNSAFE_componentWillUpdate

注意: getDerivedStateFromProps/getSnapshotBeforeUpdate 和 componentWillMount/componentWillReceiveProps/componentWillUpdate 如果同时存在,React会在控制台给出警告信息,且仅执行 getDerivedStateFromProps/getSnapshotBeforeUpdate 【[email protected]

React-Hooks 的生命周期

image

生命周期详解

constructor()

constructor(props)

构造函数通常用于:

  • 使用 this.state 来初始化 state
  • 给事件处理函数绑定 this

注意:ES6 子类的构造函数必须执行一次 super()。React 如果构造函数中要使用 this.props,必须先执行 super(props)。

static getDerivedStateFromProps()

static getDerivedStateFromProps()

当创建时、接收新的 props 时、setState 时、forceUpdate 时会执行这个方法。

这是一个静态方法,参数 nextProps 是新接收的 propsprevState 是当前的 state。返回值(对象)将用于更新 state,如果不需要更新则需要返回 null

下面是官方文档给出的例子

class ExampleComponent extends React.Component {
  // Initialize state in constructor,
  // Or with a property initializer.
  state = {
    isScrollingDown: false,
    lastRow: null,
  };

  static getDerivedStateFromProps(props, state) {
    if (props.currentRow !== state.lastRow) {
      return {
        isScrollingDown: props.currentRow > state.lastRow,
        lastRow: props.currentRow,
      };
    }

    // Return null to indicate no change to state.
    return null;
  }
}

这个方法的常用作用也很明显了:父组件传入新的 props 时,用来和当前的 state 对比,判断是否需要更新 state。以前一般使用 componentWillReceiveProps 做这个操作。

这个方法在建议尽量少用,只在必要的场景中使用,一般使用场景如下:

  1. 无条件的根据 props 更新 state
  2. propsstate 的不匹配情况更新 state

详情可以参考官方文档的最佳实践 You Probably Don’t Need Derived State

render

render()

每个类组件中,render() 是唯一必须的方法。

render() 正如其名,作为渲染用,可以返回下面几种类型:

  • React 元素(React elements)
  • 数组(Arrays)
  • 片段(fragments)
  • 插槽(Portals)
  • 字符串或数字(String and numbers)
  • 布尔值或 null(Booleans or null)

里面不应该包含副作用,应该作为纯函数。不能使用 setState。

componentDidMount()

componentDidMount()

组件完成装载(已经插入 DOM 树)时,触发该方法。这个阶段已经获取到真实的 DOM。

一般用于下面的场景:

  • 异步请求 ajax
  • 添加事件绑定(注意在 componentWillUnmount 中取消,以免造成内存泄漏)

可以使用 setState,触发re-render,影响性能。

componentWillUnmount()

componentWillUnmount()

在组件卸载或者销毁前调用。这个方法主要用来做一些清理工作,例如:

  • 取消定时器
  • 取消事件绑定
  • 取消网络请求

注意:不能使用 setState

componentDidCatch()

componentDidCatch(err, info)

任何子组件在渲染期间,生命周期方法中或者构造函数 constructor 发生错误时调用。

错误边界不会捕获下面的错误:

  • 事件处理 (Event handlers) (因为事件处理不发生在 React 渲染时,报错不影响渲染)
  • 异步代码 (Asynchronous code) (e.g. setTimeout or requestAnimationFrame callbacks)
  • 服务端渲染 (Server side rendering)
  • 错误边界本身(而不是子组件)抛出的错误

参考资料

【Webpack】PWA 打包

渐进式网络应用程序(Progressive Web Application - PWA),是一种可以提供类似于原生应用程序(native app)体验的网络应用程序(web app)。PWA 可以用来做很多事。其中最重要的是,在离线(offline)时应用程序能够继续运行功能。这是通过使用名为 Service Workers 的网络技术来实现的
添加 workbox-webpack-plugin 插件,并调整 webpack.config.js 文件:

npm install workbox-webpack-plugin --save-dev

webpack.config.js

 const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
  const CleanWebpackPlugin = require('clean-webpack-plugin');
+ const WorkboxPlugin = require('workbox-webpack-plugin');

  module.exports = {
    entry: {
      app: './src/index.js',
      print: './src/print.js'
    },
  plugins: [
    new CleanWebpackPlugin(['dist']),
    new HtmlWebpackPlugin({
-     title: 'Output Management'
+     title: 'Progressive Web Application'
-   })
+   }),
+   new WorkboxPlugin.GenerateSW({
+     // 这些选项帮助 ServiceWorkers 快速启用
+     // 不允许遗留任何“旧的” ServiceWorkers
+     clientsClaim: true,
+     skipWaiting: true
+   })
  ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };

注册 Service Worker

  import _ from 'lodash';
  import printMe from './print.js';

+ if ('serviceWorker' in navigator) {
+   window.addEventListener('load', () => {
+     navigator.serviceWorker.register('/sw.js').then(registration => {
+       console.log('SW registered: ', registration);
+     }).catch(registrationError => {
+       console.log('SW registration failed: ', registrationError);
+     });
+   });
+ }

现在来进行测试。停止服务器并刷新页面。如果浏览器能够支持 Service Worker,你应该可以看到你的应用程序还在正常运行。然而,服务器已经停止了服务,此刻是 Service Worker 在提供服务。

【JavaScript】防抖和节流

概念

函数节流(throttle)与 函数防抖(debounce)都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。

两者的区别是:防抖是虽然事件持续触发,但只有等事件停止触发后 n 秒才执行函数,节流是持续触发的时候,每 n 秒执行一次函数。

防抖,即短时间内大量触发同一事件,只会执行一次函数,实现原理为设置一个定时器,约定在xx毫秒后再触发事件处理,每次触发事件都会重新设置计时器,直到xx毫秒内无第二次操作,防抖常用于搜索框/滚动条的监听事件处理,如果不做防抖,每输入一个字/滚动屏幕,都会触发事件处理,造成性能浪费。

防抖是延迟执行,而节流是间隔执行,函数节流即每隔一段时间就执行一次,实现原理为设置一个定时器,约定xx毫秒后执行事件,如果时间到了,那么执行函数并重置定时器,和防抖的区别在于,防抖每次触发事件都重置定时器,而节流在定时器到时间后再清空定时器

防抖(debounce)

防止抖动,以免把一次事件误认为多次。防抖重在清零 clearTimeout(timer)

简单实现:

function debounce(fn, wait) {
	let timer;
	return () => {
		clearTimeout(timer)
		timer = setTimeout(() => fn(arguments), wait)
	};
}

防抖函数

抖函数的作用就是控制函数在一定时间内的执行次数。防抖意味着N秒内函数只会被执行一次,如果N秒内再次被触发,则重新计算延迟时间。

实现:

  1. 事件第一次触发时,timer 是 null,调用 later(),若 immediate 为true,那么立即调用 func.apply(this, params);如果 immediate 为 false,那么过 wait 之后,调用 func.apply(this, params)
  2. 事件第二次触发时,如果 timer 已经重置为 null(即 setTimeout 的倒计时结束),那么流程与第一次触发时一样,若 timer 不为 null(即 setTimeout 的倒计时未结束),那么清空定时器,重新开始计时。
// immediate 为 true 时,表示函数在每个等待时延的开始被调用。immediate 为 false 时,表示函数在每个等待时延的结束被调用
function debounce(func, wait, immediate = true) {
    let timeout, result;
    // 延迟执行函数
    const later = (context, args) => setTimeout(() => {
        timeout = null;// 倒计时结束
        if (!immediate) {
            //执行回调
            result = func.apply(context, args);
            context = args = null;
        }
    }, wait);
    let debounced = function (...params) {
        if (!timeout) {
            timeout = later(this, params);
            if (immediate) {
                //立即执行
                result = func.apply(this, params);
            }
        } else {
            clearTimeout(timeout);
            //函数在每个等待时延的结束被调用
            timeout = later(this, params);
        }
        return result;
    }
    //提供在外部清空定时器的方法
    debounced.cancel = function () {
        clearTimeout(timer);
        timer = null;
    };
    return debounced;
};

防抖的应用场景

  1. 搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。
  2. 表单验证
  3. 按钮提交事件。
  4. 浏览器窗口缩放,resize事件(如窗口停止改变大小之后重新计算布局)等。
  5. 登录、发短信等按钮避免用户点击太快,以致于发送了多次请求。
  6. 文本编辑器实时保存,当无任何更改操作一秒后进行保存。

节流(throttle)

控制事件发生的频率。节流重在开关锁 timer = null

简单实现:

function throttle(fn, wait) {
	let timer;
	return () => {
		if(timer) return;
		timer = setTimeout(() => {
			fn(arguments);
			timer = null;
		}, wait)
	};
}

节流函数

节流函数的作用是规定一个单位时间,在这个单位时间内最多只能触发一次函数执行,如果这个单位时间内多次触发函数,只能有一次生效。

// 禁用第一次首先执行,传递 {leading: false} ;想禁用最后一次执行,传递 {trailing: false}
function throttle(func, wait, options = {}) {
    var timeout, context, args, result;
    var previous = 0;
    var later = function () {
        previous = options.leading === false ? 0 : (Date.now() || new Date().getTime());
        timeout = null;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
    };

    var throttled = function () {
        var now = Date.now() || new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        //remaining 为距离下次执行 func 的时间
        //remaining > wait,表示客户端系统时间被调整过
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        //remaining 小于等于0,表示事件触发的间隔时间大于设置的 wait
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                //清空定时器
                clearTimeout(timeout);
                timeout = null;
            }
            //重置 previous
            previous = now;
            //执行函数
            result = func.apply(context, args); 
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
        }
        return result;
    };

    throttled.cancel = function () {
        clearTimeout(timeout);
        previous = 0;
        timeout = context = args = null;
    };

    return throttled;
}

节流的应用场景

  1. 按钮点击事件
  2. 拖拽事件
  3. onScoll
  4. 计算鼠标移动的距离(mousemove)

参考资料

JavaScript 中的闭包

闭包的定义

闭包是指有权访问另一个函数作用域中的变量的函数,但是闭包不只是函数,还应该包括函数可访问的词法作用域(因为作用域链)

闭包有三个作用域范围:

  1. 有权访问自己的作用域:自己的大括号中定义的变量
  2. 有权访问外部函数的变量
  3. 有权访问全局变量

创建函数的父级上下文的数据是保存在函数的内部属性 [[Scope]] 中的。如果对 [[Scope]] 和作用域链的知识完全理解了的话,那对闭包也就完全理解了。

根据函数创建的算法,我们看到 在 ECMAScript 中,所有的函数都是闭包,因为它们都是在创建的时候就保存了上层上下文的作用域链(除开异常的情况) (不管这个函数后续是否会激活 —— [[Scope]] 在函数创建的时候就有了)。

所有对象都引用一个[[Scope]]:
这里还要注意的是:在 ECMAScript 中,同一个父上下文中创建的闭包是共用一个 [[Scope]] 属性的。也就是说,某个闭包对其中 [[Scope]] 的变量做修改会影响到其他闭包对其变量的读取。
这就是说:所有的内部函数都共享同一个父作用域。

因为作用域链,使得所有的函数都是闭包(与函数类型无关: 匿名函数,FE函数声明,NFE命名函数表达式,FD函数表达式都是闭包)。

这里只有一类函数除外,那就是通过 Function 构造器创建的函数,因为其[[Scope]]只包含全局对象。
为了更好的澄清该问题,我们对 ECMAScript 中的闭包给出2个正确的版本定义:

ECMAScript中,闭包指的是:

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  2. 从实践角度:以下函数才算是闭包:
    1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    2. 在代码中引用了自由变量

闭包是如何产生的

以下过程引用自极客时间的《浏览器工作原理与实践》的第12章
先看以下代码:

function foo() {
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    var innerBar = { 
        setName:function(newName){
            myName = newName
        },
        getName:function(){
            console.log(test1)
            return myName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())

当 foo 函数的执行上下文销毁时,由于 foo 函数产生了闭包,所以变量 myName 和 test1 并没有被销毁,而是保存在内存中,那么应该如何解释这个现象呢?
要解释这个现象,我们就得站在内存模型的角度来分析这段代码的执行流程:

  1. 当 JavaScript 引擎执行到 foo 函数时,首先会编译,并创建一个空执行上下文。
  2. 在编译过程中,遇到内部函数 setName,JavaScript 引擎还要对内部函数做一次快速的词法扫描,发现该内部函数引用了 foo 函数中的 myName 变量,由于是内部函数引用了外部函数的变量,所以 JavaScript 引擎判断这是一个闭包,于是在堆空间创建换一个“closure(foo)”的对象(这是一个内部对象,JavaScript 是无法访问的),用来保存 myName 变量。
  3. 接着继续扫描到 getName 方法时,发现该函数内部还引用变量 test1,于是 JavaScript 引擎又将 test1 添加到“closure(foo)”对象中。这时候堆中的“closure(foo)”对象中就包含了 myName 和 test1 两个变量了。
  4. 由于 test2 并没有被内部函数引用,所以 test2 依然保存在调用栈中。

通过上面的分析,我们可以画出执行到 foo 函数中“return innerBar”语句时的调用栈状态,如下图所示:

image

从上图你可以清晰地看出,当执行到 foo 函数时,闭包就产生了;当 foo 函数执行结束之后,返回的 getName 和 setName 方法都引用“clourse(foo)”对象,所以即使 foo 函数退出了,“clourse(foo)”依然被其内部的 getName 和 setName 方法引用。所以在下次调用bar.setName或者bar.getName时,创建的执行上下文中就包含了“clourse(foo)”。

总的来说,产生闭包的核心有两步:第一步是需要预扫描内部函数;第二步是把内部函数引用的外部变量保存到堆中

闭包的作用

  1. 能够访问函数定义时所在的词法作用域(阻止其被回收)。

  2. 私有化变量

function base() {
  let x = 10 // 私有变量
  return {
    getX: function() {
      return x
    },
  }
}

let obj = base()
obj.getX() // ==> 10
  1. 模拟块级作用域
var a = []
for (var i = 0; i < 10; i++) {
  a[i] = (function(j) {
    return function() {
      console.log(j)
    }
  })(i)
}

a[6]() // ==> 6
  1. 创建模块
function coolModule() {
  let name = "Mike"
  let age = 20
  function sayName() {
    console.log(name)
  }
  function sayAge() {
    console.log(age)
  }
  return {
    sayName,
    sayAge,
  }
}

let info = coolModule()
info.sayName() // ==> Mike

模块模式具有两个必备的条件(来自《你不知道的JavaScript》):

  • 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)
  • 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

参考文章

【JavaScript】赋值与深浅拷贝

image


数据类型

先回忆一下JavaScript中的数据类型:

数据分为基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和引用数据类型。

  • 基本数据类型的特点:直接存储在栈(stack)中的数据
  • 引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里

引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

image

赋值(Copy)

赋值是将某一数值或对象赋给某个变量的过程,分为下面 2 部分:

  • 基本数据类型:赋值,赋值之后两个变量互不影响
  • 引用数据类型:赋址,两个变量具有相同的引用,指向同一个对象,相互之间有影响

浅拷贝(Shallow Copy)

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

浅拷贝使用场景:

  • Object.assign()
  • 展开语法 Spread
  • Array.prototype.slice() / Array.prototype.concat()

深拷贝(Deep Copy)

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

深拷贝使用场景:

  • JSON.parse(JSON.stringify(object))
    该方法有以下几个问题:
    • 会忽略 undefined
    • 会忽略 symbol
    • 不能序列化函数
    • 不能解决循环引用的对象
    • 不能正确处理 new Date()
    • 不能处理正则
  • 手写递归方法(比如 jQuery 中的 extend()lodash 中的 cloneDeep()

三者的区别

-- 和原数据是否指向同一对象 第一层数据为基本数据类型 原数据中包含子对象
赋值 改变会使原数据一同改变 改变会使原数据一同改变
浅拷贝 改变不会使原数据一同改变 改变会使原数据一同改变
深拷贝 改变不会使原数据一同改变 改变不会使原数据一同改变

手动实现 Object.assign()

实现一个 Object.assign 大致思路如下:

  1. 判断原生 Object 是否支持该函数,如果不存在的话创建一个函数 assign,并使用 Object.defineProperty 将该函数绑定到 Object 上。
  2. 判断参数是否正确(目标对象不能为空,我们可以直接设置 {} 传递进去,但必须设置值)。
  3. 使用 Object() 转成对象,并保存为 to,最后返回这个对象 to
  4. 使用 for..in 循环遍历出所有可枚举的自有属性。并复制给新的目标对象(使用 hasOwnProperty 获取自有属性,即非原型链上的属性)。
if (typeof Object.assign2 != 'function') {
  // Attention 1
  //原生情况下挂载在 Object 上的属性是不可枚举的,但是直接在 Object 上挂载属性 a 之后是可枚举的,所以这里必须使用 Object.defineProperty
  Object.defineProperty(Object, "assign2", {
    value: function (target) {
	   // JS 对于不可写的属性值的修改静默失败(silently failed),在严格模式下才会提示错误
      'use strict';
      if (target == null) { // Attention 2
        throw new TypeError('Cannot convert undefined or null to object');
      }

      // Attention 3
      var to = Object(target);
        
      for (var index = 1; index < arguments.length; index++) {
        var nextSource = arguments[index];

        if (nextSource != null) {  // Attention 2
          // Attention 4
		  // 使用 for..in 遍历对象 nextSource 获取属性值
		  // 此处会同时检查其原型链上的属性
          for (var nextKey in nextSource) {
		    // 有的对象可能没有连接到 Object.prototype 上(比如通过 Object.create(null) 来创建),这种情况下,使用 object.hasOwnProperty(..) 就会失败,因此用 call
            if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
              to[nextKey] = nextSource[nextKey];
            }
          }
        }
      }
      return to;
    },
	// 默认值是 false,即 enumerable: false
    writable: true,
    configurable: true
  });
}

参考资料

JavaScript 中异步的初步了解

JavaScript中有很多异步编程方式:

  • callback
  • promise
  • generator
  • async await
  • RxJS

callback

当一个函数传入“另外一个函数”作为参数时, 我们就可以把这个函数叫做 Callback 函数。 而这里的“另外一个函数”也有一个常见的名字: 高阶函数(Hight order function)。

Callback 并非都是异步执行的。 比如, 在我们常用的 Array.prototype.map() 中,其第一个参数也是一个回调函数,但它是同步执行的。

Callback 存在着以下2个问题而饱受诟病:

  • 控制反转:即 callback 函数的调用在一定程序上是不受我们控制的,我们缺少可靠的机制确保回调函数能按照预期被执行
  • 难以理解:即令人生畏的回调地狱

Promise

所谓Promise,简单来说就是一个容器,里面保存着某个未来才会结束的事情(通常是一个异步操作)。从语法上说,Promise是一个对象,从他可以获取异步操作的消息。

Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。

  • resolve函数的作用是,将promise对象的状态从“未完成”变为“成功”(即从Pending变为Resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
  • reject函数的作用是,将promise对象的状态从“未完成”变为“失败”(即从Pending变为Rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

promise对象生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数。then方法可以接受两个回调函数作为参数。第一个回调函数是promise对象的状态变为Resolved时调用,第二个回调函数是promise对象的状态变为Reject时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受promise对象传出的值作为参数。

使用 Promise 构造函数时,必须调用 resolve()reject() 回调。

当链接 .then.catch 时,将它们视为一系列步骤会很有帮助。每个 .then 都接收前一个 .then 返回的值作为其参数。但是,如果 “step” 遇到错误,则任何后续的 .then “ steps” 都将被跳过,直到遇到 .catch。如果要覆盖错误,要做的就是返回一个非错误值。可以通过任何随后的 .then 访问。

var p = new Promise((resolve, reject) => {
    reject(Error('The Fails!'))
  })
  .catch(error => console.log(error))   // The Fails!
  .then(error => console.log(error))    // undefined

Promise 也有一些缺陷被人诟病,主要体现在以下两个方面:

  • 一旦开始执行就没办法手动终止;在满足一些条件时我们可能会希望不再执行后续的then,这在Promise中就很难优雅的做到;
  • 我们无法完全捕获可能的错误。比如说 .catch 中的错误就难以再被捕获;

async/await

async/await其实是 Promise 的语法糖,它能实现的效果都能用 then 链来实现,它是为优化 then 链而开发出来的。async 声明 function 是异步的,await等待某个操作完成。语法上强制规定 await 只能出现在 asnyc 函数中。

async 函数返回的是一个 Promise 对象。
await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。

async 函数的一些缺陷如下:

  • await 关键字只能结合 Promise 控制异步;
  • 无法在外界取消一个正在运行中的 async 函数;

我们应当明确,async 函数并非一种让 generator 更便于使用的语法糖。async 函数只有在结束时,才会返回的是一个 Promise。我们无法控制其中间状态,而 generator 返回的是迭代器,迭代器让你有充分的控制权。

参考资料

【JavaScript】对象基础

JavaScript 对象的属性类型

在 JavaScript 中有两种属性类型,分别是数据属性和访问器属性。

数据属性

数据属性包含一个数据值的位置。在这个位置可以读取和写入值。其包含四个特性:

  • [[Configurable]]:表示能否通过 delete 操作符删除对象中的属性,能否修改属性的特性,能否把属性修改为访问器特性,默认为 true
  • [[Enumberable]]:表示是否可以通过 for-in 循环返回属性。默认为 true
  • [[Writable]]:表示是否可以修改属性的值,默认为 true
  • [[value]]:表示该属性的值,读取的时候从这里读取,修改的时候保存在这里,默认为 undefined

在这里需要注意到一个地方,如果想要修改属性的默认特性,必须要通过 Object.defineProperty() 方法进行修改,比如: Object.defineProperty(person,"name",{writable:false})person 中的 name 属性修改为不可改变值。按照道理来说,这四个属性的特性是可以随意修改的,那么重点来了,当把 Configurable 属性修改为 false 之后,无论修改四个属性中任何一个,都会报错。

访问器属性

访问器属性不包含数据值,他包含一对 gettersetter 函数(个人认为这两个函数是 Vue 等 MVVM 框架的双向数据绑定原理核心所在,虽然这两个属性在访问器属性中并不是必须的)。在读取访问器属性的时候,会调用 getter 函数,在写入访问器属性的时候,会调用 setter 函数。访问器属性具有以下四个特性:

  • [[Configurable]]: 表示能否通过 delete 操作符删除对象中的属性,能否修改属性的特性,能否把属性修改为访问器特性,默认为 true
  • [[Enumberable]]: 表示是否可以通过 for-in 循环返回属性。默认为 true
  • [[get]]: 在读取属性的时候调用,默认为 undefined
  • [[set]]: 在写入属性的时候调用,默认为 undefined

这里也是有一个地方需要注意的,访问器属性是不能直接进行定义的,只能通过 Object.defineProperty() 方法进行定义,在这里引用红宝书上的一个例子来进行说明:

let book = {
	_year:2004,
	edition:1
};
Object.defineProperty(book,"year",{
	get:function(){
		return this._year;
	},
	// set的默认参数是新值
	set:function(newValue){
		if(newValue > 2004){
			this._year = newValue;
			this.eidition += newValue -2004;
		}
	}
});
book.year = 2005;
console.log(book.edition); //2

这里指定了一个访问器属性 year,因为对 year 属性进行修改会触发 setter 函数,所以 edition 会变成 2
当然,不一定需要同时具备 gettersetter 函数,只有 getter 表示只能读不能写,反之只能写不能读。

定义多个属性

在 ES5 中有一个方法可以同时定义很多个属性,这个方法是 Object.defineProperties(),这个方法的参数有两个,第一个参数是要添加和修改其对象的属性,第二个参数是对象的属性与第一个对象中要添加获修改的属性。

读取属性的特性

在 ES5 中可以使用 Object.getOwnPropertyDescription() 方法取得给定属性的描述符,参数为:属性所在的对象和要读取其描述符的属性的名称。

参考资料

  • 红宝书

【JavaScript】class (es6)

JavaScript 类用构造函数初始化实例,定义字段和方法。甚至可以使用 static 关键字在类本身上附加字段和方法。

继承是使用 extends 关键字实现的:可以轻松地从父类创建子类,super 关键字用于从子类访问父类。

要利用封装,将字段和方法设为私有以隐藏类的内部细节,私有字段和方法名必须以 # 开头。

初始化: constructor()

constructor(param1, param2, ...) 是用于初始化实例的类主体中的一种特殊方法。可以设置字段的初始值或进行任何类型的对象设置。

字段

类字段是保存信息的变量,字段可以附加到两个实体:

  1. 类实例上的字段
  2. 类本身的字段(也称为静态字段)

字段有两种级别可访问性:

  1. public:该字段可以在任何地方访问
  2. private:字段只能在类的主体中访问

公共实例字段

    class User {
      name;
    
      constructor(name) {
        this.name = name;
      }
    }
    
    const user = new User('前端小智');
    user.name; // => '前端小智'

私有实例字段

私有字段只能在类的主体中访问。

class User {
  #name;

  constructor (name) {
    this.#name = name;
  }

  getName() {
    return this.#name;
  }
}

const user = new User('前端小智')
user.getName() // => '前端小智'

user.#name  // 抛出语法错误

公共静态字段

定义类常量或存储特定于该类的信息

class User {
  static TYPE_ADMIN = 'admin';
  static TYPE_REGULAR = 'regular';

  name;
  type;

  constructor(name, type) {
    this.name = name;
    this.type = type;
  }
}

const admin = new User('前端小智', User.TYPE_ADMIN);
admin.type === User.TYPE_ADMIN; // => true

私有静态字段

隐藏静态字段的实现细节

class User {
  static #MAX_INSTANCES = 2;
  static #instances = 0;

  name;

  constructor(name) {
    User.#instances++;
    if (User.#instances > User.#MAX_INSTANCES) {
      throw new Error('Unable to create User instance');
    }
    this.name = name;
  }
}

new User('张三');
new User('李四');
new User('王五'); // throws Error

方法

gettersetter 模仿常规字段,但是对如何访问和更改字段具有更多控制。在尝试获取字段值时执行 getter ,而在尝试设置值时使用 setter

静态方法是直接附加到类的函数,它们持有与类相关的逻辑,而不是类的实例。

继承: extends

JavaScript 中的类使用extends关键字支持单继承。在class Child extends Parent { }表达式中,Child类从Parent继承构造函数,字段和方法。

父构造函数:constructor()中的super()

如果希望在子类中调用父构造函数,则需要使用子构造函数中可用的 super() 特殊函数。

父实例:方法中的super

如果希望在子方法中访问父方法,可以使用特殊的快捷方式 super

参考资料

Babel 基础入门

什么是 Babel

Babel 是一个 JavaScript 编译器,用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前版本和旧版本的浏览器或其他环境中。简单来说 Babel 的工作就是:

  • 语法转换
  • 通过 Polyfill 的方式在目标环境中添加缺失的特性(@babel/polyfill 模块)
  • JS 源码转换(codemods)

Babel 三个编译阶段

Babel 的编译过程和大多数其他语言的编译器相似,可以分为三个阶段:

  • 解析(Parsing):将代码字符串解析成抽象语法树。
  • 转换(Transformation):对抽象语法树进行转换操作。
  • 生成(Code Generation): 根据变换后的抽象语法树再生成代码字符串。

image

Babel 常用 API

@babel/core

Babel 的编译器,核心 API 都在这里面,比如常见的 transformparse

@babel/cli

cli 是命令行工具, 安装了 @babel/cli 就能够在命令行中使用 babel 命令来编译文件。当然我们一般不会用到,打包工具已经帮我们做好了。

@babel/node

直接在 node 环境中,运行 ES6 的代码

babylon

Babel 的解析器

babel-traverse

用于对 AST 的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点。

babel-types

用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法,对编写处理 AST 逻辑非常有用。

babel-generator

Babel 的代码生成器,它读取 AST 并将其转换为代码和源码映射(sourcemaps)

插件 Plugins

Babel 构建在插件之上,使用现有的或者自己编写的插件可以组成一个转换通道,Babel 的插件分为两种: 语法插件和转换插件。

语法插件

这些插件只允许 Babel 解析(parse) 特定类型的语法(不是转换),可以在 AST 转换时使用,以支持解析新语法,例如:

import * as babel from "@babel/core";
const code = babel.transformFromAstSync(ast, {    
	//支持可选链    
	plugins["@babel/plugin-proposal-optional-chaining"],    
	babelrcfalse
}).code;

转换插件

转换插件会启用相应的语法插件(因此不需要同时指定这两种插件),这点很容易理解,如果不启用相应的语法插件,意味着无法解析,连解析都不能解析,又何谈转换呢?

@babel/plugin-transform-runtime

@babel/plugin-transform-runtime 是一个可以重复使用 Babel 注入的帮助程序,以节省代码大小的插件

预设 Preset

我们假设 ES6 有 10 个新特性,那为了把 ES6 转换成 ES5 就需要安装 10 个转换插件,配置文件很长不说,npm install 的时间也会很长。为了解决这个问题,babel 还提供了一组插件的集合,就叫 presets。

通过使用或创建一个 preset 即可轻松使用一组插件。

presets 是这么分类的

  1. es201X, latest,这些是已经纳入到标准规范的语法。例如 es2015 包含 const let 等新语法,es2017 包含 syntax-trailing-function-commas。latest 是一个每年更新的 preset,目的是包含所有 es201x。但也是因为更加灵活的 env 的出现,已经废弃。
  2. stage-x,这里面包含的都是当年最新规范的草案,每年更新。 这里面还细分为 Stage 0(想法),Stage 1(提案),Stage 2(初稿),Stage 3(候选),Stage 4(完成)。低一级的 stage 会包含所有高级 stage 的内容,例如 stage-1 会包含 stage-2, stage-3 的所有内容。
  3. env, react等。

官方 Preset

  • @babel/preset-env
  • @babel/preset-flow
  • @babel/preset-react
  • @babel/preset-typescript

@babel/preset-env

@babel/preset-env 主要作用是对我们所使用的并且目标浏览器中缺失的功能进行代码转换和加载 polyfill,在不进行任何配置的情况下,@babel/preset-env 所包含的插件将支持所有最新的 JS 特性(ES2015,ES2016 等,不包含 stage 阶段),将其转换成 ES5 代码。例如,如果你的代码中使用了可选链(目前,仍在 stage 阶段),那么只配置 @babel/preset-env,转换时会抛出错误,需要另外安装相应的插件。

需要说明的是,@babel/preset-env 会根据你配置的目标环境,生成插件列表来编译。对于基于浏览器或 Electron 的项目,官方推荐使用 .browserslistrc 文件来指定目标环境。默认情况下,如果你没有在 Babel 配置文件中(如 .babelrc)设置 targetsignoreBrowserslistConfig@babel/preset-env 会使用 browserslist 配置源。

Polyfill

babel 对一些新的 API 是无法转换,比如 GeneratorSetProxyPromise 等全局对象,以及新增的一些方法:includesArray.form 等。所以这个时候就需要一些工具来为浏览器做这个兼容。

官网的定义:babel-polyfill 是为了模拟一个完整的 ES6+ 环境,旨在用于应用程序而不是库/工具。

babel-polyfill 主要有两个缺点:

  • 使用 babel-polyfill 会导致打出来的包非常大,很多其实没有用到,对资源来说是一种浪费。

  • babel-polyfill 可能会污染全局变量,给很多类的原型链上都作了修改,这就有不可控的因素存在。

因为上面两个问题,所以在 Babel7 中增加了 babel-preset-env,我们设置 "useBuiltIns":"usage" 这个参数值就可以实现按需加载 babel-polyfill

插件/预设补充知识

顺序

如果两个转换插件都将处理“程序(Program)”的某个代码片段,则将根据转换插件或 preset 的排列顺序依次执行。

  • 插件在 Presets 前运行。
  • 插件顺序从前往后排列。
  • Preset 顺序是颠倒的(从后往前)。

例如:

{
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-syntax-dynamic-import"
  ]
}

先执行 @babel/plugin-proposal-class-properties,后执行 @babel/plugin-syntax-dynamic-import

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

preset 的执行顺序是颠倒的,先执行 @babel/preset-react, 后执行 @babel/preset-env

插件参数

插件和 preset 都可以接受参数,参数由插件名和参数对象组成一个数组。preset 设置参数也是这种格式。

如:

{
  "plugins": [["@babel/plugin-proposal-class-properties", { "loose": true }]]
}

插件的短名称

如果插件名称为 @babel/plugin-XXX,可以使用短名称 @babel/XXX :

{
  "plugins": [
    "@babel/transform-arrow-functions" // 同 "@babel/plugin-transform-arrow-functions"
  ]
}

如果插件名称为 babel-plugin-XXX,可以使用短名称 XXX,该规则同样适用于带有 scope 的插件:

{
  "plugins": [
    "newPlugin", // 同"babel-plugin-newPlugin"
    "@scp/myPlugin" // 同 "@scp/babel-plugin-myPlugin"
  ]
}

创建 Preset

可以简单的返回一个插件数组

module.exports = function () {
  return {
    plugins: ["A", "B", "C"],
  };
};

preset 中也可以包含其他的 preset,以及带有参数的插件。

module.exports = function () {
  return {
    presets: [require("@babel/preset-env")],
    plugins: [
      [require("@babel/plugin-proposal-class-properties"), { loose: true }],
      require("@babel/plugin-proposal-object-rest-spread"),
    ],
  };
};

最佳实践

    1. 如果实现自己的应用,推荐使用 presets-env,原因是 presets-env 可以根据配置的 browerlist 来进行编译,特别是目标浏览器比较高的时候,生成的包会非常小,而且自己的应用也不存在谁污染谁的问题,就算要兼容比较旧的浏览器,最差的情况和 runtime 方式差不多。
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false,
        "useBuiltIns": "usage",
        "corejs": {
          "version": 3,
          "proposals": true
        }
      }
    ]
  ]
}
    1. 如果是做公共的 npm 包,建议使用 runtime 的方式,因为这样可以避免全局污染,你永远无法想象包的调用方会在代码里写了什么,还是谨慎为上。
{
    "presets": [
        [
            "@babel/env",
            {
                "modules": false
            }
        ]
    ],
    "plugins": [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": {
                    "version": 3,
                    "proposals": true
                },
                "useESModules": true
            }
        ]
    ]
}

注意,这里需要配置一下 corejs 的版本号,不配置编译的时候会报警告。
useBuiltIns 的机构参数:

  • false:此时不对Polyfill 做操作,如果引入 @babel/polyfill 则不会按需加载,会将所有代码引入
  • usage:会根据配置的浏览器兼容性,以及你代码中使用到的 API 来进行 Polyfill ,实现按需加载
  • entry:会根据配置的浏览器兼容性,以及你代码中使用到的 API 来进行 Polyfill ,实现按需加载,不过需要在入口文件中手动加上 import ' @babel/polyfill'

参考资料

【浏览器】缓存

image

各类缓存技术优缺点

1、cookie

优点:对于传输部分少量不敏感数据,非常简明有效

缺点:容量小(4K),不安全(cookie被拦截,很可能暴露session);原生接口不够友好,需要自己封装;需要指定作用域,不可以跨域调用

2、Web Storage

容量稍大一点(5M),localStorage可做持久化数据存储支持事件通知机制,可以将数据更新的通知发送给监听者

缺点:本地储存数据都容易被篡改,容易受到XSS攻击

缓存读取需要依靠js的执行,所以前提条件就是能够读取到html及js代码段,其次文件的版本更新控制会带来更多的代码层面的维护成本,所以LocalStorage更适合关键的业务数据而非静态资源

Cookie的作用是与服务器进行交互,作为HTTP规范的一部分而存在 ,而Web Storage仅仅是为了在本地“存储”数据而生

3、indexDB

IndexedDb提供了一个结构化的、事务型的、高性能的NoSQL类型的数据库,包含了一组同步/异步API,这部分不好判断优缺点,主要看使用者。

4、Manifest(已经被web标准废除)

优点

  • 可以离线运行
  • 可以减少资源请求
  • 可以更新资源

缺点

  • 更新的资源,需要二次刷新才会被页面采用
  • 不支持增量更新,只有manifest发生变化,所有资源全部重新下载一次
  • 缺乏足够容错机制,当清单中任意资源文件出现加载异常,都会导致整个manifest策略运行异常

Manifest被移除是技术发展的必然,请拥抱Service Worker吧

5、PWA(Service Worker)

这位目前是最炙手可热的缓存明星,是官方建议替代Application Cache(Manifest)的方案作为一个独立的线程,是一段在后台运行的脚本,可使web app也具有类似原生App的离线使用、消息推送、后台自动更新等能力

目前有三个限制(不能明说是缺点)

  • 不能访问 DOM
  • 不能使用同步 API
  • 需要HTTPS协议

localStorage,sessionStorage和cookie的区别

  • 共同点:都是保存在浏览器端、且同源的
  • 区别:
    • cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递,而sessionStorage和localStorage不会自动把数据发送给服务器,仅在本地保存。cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下
    • 存储大小限制也不同,cookie数据不能超过4K,同时因为每次http请求都会携带cookie、所以cookie只适合保存很小的数据,如会话标识。sessionStorage和localStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大
    • 数据有效期不同,sessionStorage:仅在当前浏览器窗口关闭之前有效;localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie:只在设置的cookie过期时间之前有效,即使窗口关闭或浏览器关闭
    • 作用域不同,sessionStorage不在不同的浏览器窗口**享,即使是同一个页面;localstorage在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的
    • web Storage支持事件通知机制,可以将数据更新的通知发送给监听者
    • web Storage的api接口使用更方便

浏览器缓存

浏览器处理缓存的策略流程图:
image

通常浏览器缓存策略分为两种:强缓存和协商缓存

image

基本原理:

  • 1)浏览器在加载资源时,根据请求头的 expirescache-control 判断是否命中强缓存,是则直接从缓存读取资源,不会发请求到服务器。
  • 2)如果没有命中强缓存,浏览器一定会发送一个请求到服务器,通过 last-modifiedetag 验证资源是否命中协商缓存,如果命中,服务器会将这个请求返回,但是不会返回这个资源的数据,依然是从缓存中读取资源
  • 3)如果前面两者都没有命中,直接从服务器加载资源

强缓存

Expires

ExpiresHTTP/1.0 控制网页缓存的字段。其值为服务器返回该请求结果缓存的到期时间,即如果发生时间在 Expires 之前,那么本地缓存始终有效,否则就会发送请求到服务器来获取资源;是绝对时间

Cache-Control

Cache-ControlHTTP/1.1 新增的规则,用于控制网页缓存的字段。
image

Expires & Cache-Control

Cache-ControlExpires 同时存在的话(如下图),Cache-Control 优先级高于 Expires
因为 Expires 时间返回的是服务器绝对时间,而客户端本地时间是可以修改的(时区不同等),造成服务器与客户端时间发生误差,强缓存会直接失效。而 Cache-Control 是相对时间,每次参照客户端第一次请求时间计算而来的,故不会受到影响;毕竟 Cache-ControlHTTP/1.1 新增的规范

协商缓存

Last-Modified

Last-Modified: Wed, 21 Nov 2018 05:46:58 GMT
If-Modified-Since: Wed, 21 Nov 2018 05:46:58 GMT

ETag

Etag 是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成)。

ETag: "d5d-55b192d5e0640"
If-None-Match: "d5d-55b192d5e0640"

Last-Modified & Etag

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

ETag 可以解决 Last-Modified 存在的一些问题,既生 Last-Modified 何生 ETag ?

  • 文件内容不更改,但修改时间发生改变,这时候不希望客户端认为这个文件修改了。
  • 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说 1S 内修改了 N 次),If-Modified-Since 能检查到的粒度是 S 级的,这种修改无法判断;
  • 某些服务器不能精确的得到文件的最后修改时间。

参考资料

【Webpack】基础知识与简单配置

构建就是把源代码转换成发布到线上的可执行 JavaScrip、CSS、HTML 代码,包括如下内容:

  • 代码转换:TypeScript 编译成 JavaScript、SCSS 编译成 CSS 等。
  • 文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片等。
  • 代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载。
  • 模块合并:在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件。
  • 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器。
  • 代码校验:在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过。
  • 自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统。
  • 构建其实是工程化、自动化**在前端开发中的体现,把一系列流程用代码去实现,让代码自动化地执行这- - 一系列复杂的流程。 构建给前端开发注入了更大的活力,解放了我们的生产力。

Webpack 是一个打包模块化 JavaScript 的工具,在 Webpack 里一切文件皆模块,通过 Loader 转换文件,通过 Plugin 注入钩子,最后输出由多个模块组合成的文件。Webpack 专注于构建模块化项目。

其官网的首页图很形象的画出了 Webpack 是什么,如下:

image

一切文件:JavaScript、CSS、SCSS、图片、模板,在 Webpack 眼中都是一个个模块,这样的好处是能清晰的描述出各个模块之间的依赖关系,以方便 Webpack 对模块进行组合和打包。 经过 Webpack 的处理,最终会输出浏览器能使用的静态资源。

概念

Webpack 有以下几个核心概念。

  • Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。比如单页面应用只有一个入口文件,而多页面应用则有多个入口文件。
  • Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
  • Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
  • Loader:模块转换器,用于把模块原内容按照需求转换成新内容。webpack 原生只支持 jsjson 两种文件,通过 Loaders 去支持其它文件类型并把它们转化为有效的模块,并且可以添加到依赖图中。
  • Plugin:扩展插件,webpack 的重要组成部分,其 webpack 源码的很多核心功能也都是插件实现的,由于webpack提供了在不同的生命周期内提供了很多不同的hooks钩子函数,插件的工作原理就是在 webpack 构建流程中的不同的(hooks)时机注入扩展逻辑来改变构建结果或做个性化操作。
  • Output:输出结果,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果。
  • mode:指定当前的构建环境是 productiondevelopment 还是 none。设置 mode 可以使用 webpack 内置的函数,默认值为 production

source-map

Webpack 支持为转换生成的代码输出对应的 Source Map 文件,以方便在浏览器中能通过源码调试。 控制 Source Map 输出的 Webpack 配置项是 devtool,它有很多选项

devtool 取值其实可以由 source-mapevalinlinehiddencheapmodule 这六个关键字随意组合而成。 这六个关键字每个都代表一种特性,它们的含义分别是:

  • eval:用 eval 语句包裹需要安装的模块;
  • source-map:生成独立的 Source Map 文件;
  • hidden:不在 JavaScript 文件中指出 Source Map 文件所在,这样浏览器就不会自动加载 Source Map;
  • inline:把生成的 Source Map 转换成 base64 格式内嵌在 JavaScript 文件中;
  • cheap:生成的 Source Map 中不会包含列信息,这样计算量更小,输出的 Source Map 文件更小;同时 Loader 输出的 Source Map 不会被采用;
  • module:来自 Loader 的 Source Map 被简单处理成每行一个模块;

webpack.config.js

module.exports = {
    devtool: "source-map"
    //可以提示比较全面的错误信息,具体到哪一行哪一列第几个字符,并且映射文件会打包到打包文件里面
}
  • 生产环境:默认为 null ,一般不设置( none )或 nosources-source-map

  • 开发环境:默认为 eval ,一般设置为 evalcheap-eval-source-mapcheap-module-eval-source-map

策略为:

  • 使用 cheap 模式可以大幅提高 source map 生成的效率。 没有列信息(会映射到转换后的代码,而不是映射到原始代码),通常我们调试并不关心列信息,而且就算 source map 没有列,有些浏览器引擎(例如 v8) 也会给出列信息。

  • 使用 eval 方式可大幅提高持续构建效率。参考官方文档提供的速度对比表格可以看到 eval 模式的编译速度很快。

  • 使用 module 可支持 babel 这种预编译工具(在 webpack 里做为 loader 使用)。

如果默认的 webpack minimizer 已经被重定义(例如 terser-webpack-plugin ),你必须提供 sourceMap:true 选项来启用 source map 支持。

浏览器缓存与 hash 值

对于我们开发的每一个应用,浏览器都会对静态资源进行缓存,如果我们更新了静态资源,而没有更新静态资源名称(或路径),浏览器就可能因为缓存的问题获取不到更新的资源。在我们使用 webpack 进行打包的时候,webpack 提供了 hash 的概念,所以我们可以使用 hash 来打包。

  • hash
    build-specific, 哈希值对应每一次构建( Compilation ),即每次编译都不同,即使文件内容都没有改变,并且所有的资源都共享这一个哈希值,此时,浏览器缓存就没有用了,可以用在开发环境,生产环境不适用。

  • chunkhash
    chunk-specific, 哈希值对应于 webpack 每个入口点,每个入口都有自己的哈希值。如果在某一入口文件创建的关系依赖图上存在文件内容发生了变化,那么相应入口文件的 chunkhash 才会发生变化,适用于生产环境

  • contenthash
    content-specific,根据包内容计算出的哈希值,只要包内容不变,contenthash 就不变,适用于生产环境。但我们会发现,有时内容没有变更,打包时 [contenthash] 反而变更了的问题,

webpack 也允许哈希的切片。如果你写 [hash:8] ,那么它会获取哈希值的前 8 位。

注意

  • 尽量在生产环境使用哈希

  • 按需加载的块不受 filename 影响,受 chunkFilename 影响

  • 使用 hash/chunkhash/contenthash 一般会配合 html-webpack-plugin (创建 html ,并捆绑相应的打包文件) 、clean-webpack-plugin (清除原有打包文件) 一起使用

如果只是单纯地将所有内容打包成同一个文件,那么 hash 就能够满足了,如果项目涉及到拆包,分模块进行加载等等,那么机需要用 chunkhash,来保证每次更新之后只有相关的文件 hash 值发生改变。
chunkhash 无法在启用 HotModuleReplacementPlugin 的时候使用,只能用 hash

整体配置说明

const path = require("path");

module.exports = {
    // entry 表示 入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
    // 类型可以是 string | object | array
    entry: "./app/entry", // 只有1个入口,入口只有1个文件
    entry: ["./app/entry1", "./app/entry2"], // 只有1个入口,入口有2个文件
    entry: {
        // 有2个入口
        a: "./app/entry-a",
        b: ["./app/entry-b1", "./app/entry-b2"],
    },

    // 如何输出结果:在 Webpack 经过一系列处理后,如何输出最终想要的代码。
    output: {
        // 输出文件存放的目录,必须是 string 类型的绝对路径。
        path: path.resolve(__dirname, "dist"),

        // 输出文件的名称
        filename: "bundle.js", // 完整的名称
        filename: "[name].js", // 当配置了多个 entry 时,通过名称模版为不同的 entry 生成不同的文件名称
        filename: "[chunkhash].js", // 根据文件内容 hash 值生成文件名称,用于浏览器长时间缓存文件

        // 发布到线上的所有资源的 URL 前缀,string 类型
        publicPath: "/assets/", // 放到指定目录下
        publicPath: "", // 放到根目录下
        publicPath: "https://cdn.example.com/", // 放到 CDN 上去

        // 导出库的名称,string 类型
        // 不填它时,默认输出格式是匿名的立即执行函数
        library: "MyLibrary",

        // 导出库的类型,枚举类型,默认是 var
        // 可以是 umd | umd2 | commonjs2 | commonjs | amd | this | var | assign | window | global | jsonp ,
        libraryTarget: "umd",

        // 是否包含有用的文件路径信息到生成的代码里去,boolean 类型
        pathinfo: true,

        // 附加 Chunk 的文件名称
        chunkFilename: "[id].js",
        chunkFilename: "[chunkhash].js",

        // JSONP 异步加载资源时的回调函数名称,需要和服务端搭配使用
        jsonpFunction: "myWebpackJsonp",

        // 生成的 Source Map 文件名称
        sourceMapFilename: "[file].map",

        // 浏览器开发者工具里显示的源码模块名称
        devtoolModuleFilenameTemplate: "webpack:///[resource-path]",

        // 异步加载跨域的资源时使用的方式
        crossOriginLoading: "use-credentials",
        crossOriginLoading: "anonymous",
        crossOriginLoading: false,
    },

    // 配置模块相关
    module: {
        rules: [
            // 配置 Loader
            {
                test: /\.jsx?$/, // 正则匹配命中要使用 Loader 的文件
                include: [
                    // 只会命中这里面的文件
                    path.resolve(__dirname, "app"),
                ],
                exclude: [
                    // 忽略这里面的文件
                    path.resolve(__dirname, "app/demo-files"),
                ],
                use: [
                    // 使用那些 Loader,有先后次序,从后往前执行
                    "style-loader", // 直接使用 Loader 的名称
                    {
                        loader: "css-loader",
                        options: {
                            // 给 html-loader 传一些参数
                        },
                    },
                ],
            },
        ],
        noParse: [
            // 不用解析和处理的模块
            /special-library\.js$/, // 用正则匹配
        ],
    },

    // 配置插件
    plugins: [],

    // 配置寻找模块的规则
    resolve: {
        modules: [
            // 寻找模块的根目录,array 类型,默认以 node_modules 为根目录
            "node_modules",
            path.resolve(__dirname, "app"),
        ],
        extensions: [".js", ".json", ".jsx", ".css"], // 模块的后缀名
        alias: {
            // 模块别名配置,用于映射模块
            // 把 'module' 映射 'new-module',同样的 'module/path/file' 也会被映射成 'new-module/path/file'
            module: "new-module",
            // 使用结尾符号 $ 后,把 'only-module' 映射成 'new-module',
            // 但是不像上面的,'module/path/file' 不会被映射成 'new-module/path/file'
            "only-module$": "new-module",
        },
        alias: [
            // alias 还支持使用数组来更详细的配置
            {
                name: "module", // 老的模块
                alias: "new-module", // 新的模块
                // 是否是只映射模块,如果是 true 只有 'module' 会被映射,如果是 false 'module/inner/path' 也会被映射
                onlyModule: true,
            },
        ],
        symlinks: true, // 是否跟随文件软链接去搜寻模块的路径
        descriptionFiles: ["package.json"], // 模块的描述文件
        mainFields: ["main"], // 模块的描述文件里的描述入口的文件的字段名称
        enforceExtension: false, // 是否强制导入语句必须要写明文件后缀
    },

    // 输出文件性能检查配置
    performance: {
        hints: "warning", // 有性能问题时输出警告
        hints: "error", // 有性能问题时输出错误
        hints: false, // 关闭性能检查
        maxAssetSize: 200000, // 最大文件大小 (单位 bytes)
        maxEntrypointSize: 400000, // 最大入口文件大小 (单位 bytes)
        assetFilter: function (assetFilename) {
            // 过滤要检查的文件
            return (
                assetFilename.endsWith(".css") || assetFilename.endsWith(".js")
            );
        },
    },

    devtool: "source-map", // 配置 source-map 类型

    context: __dirname, // Webpack 使用的根目录,string 类型必须是绝对路径

    // 配置输出代码的运行环境
    target: "web", // 浏览器,默认
    target: "webworker", // WebWorker
    target: "node", // Node.js,使用 `require` 语句加载 Chunk 代码
    target: "async-node", // Node.js,异步加载 Chunk 代码
    target: "node-webkit", // nw.js
    target: "electron-main", // electron, 主线程
    target: "electron-renderer", // electron, 渲染线程

    externals: {
        // 使用来自 JavaScript 运行环境提供的全局变量
        jquery: "jQuery",
    },

    stats: {
        // 控制台输出日志控制
        assets: true,
        colors: true,
        errors: true,
        errorDetails: true,
        hash: true,
    },

    devServer: {
        // DevServer 相关的配置
        proxy: {
            // 代理到后端服务接口
            "/api": "http://localhost:3000",
        },
        contentBase: path.join(__dirname, "public"), // 配置 DevServer HTTP 服务器的文件根目录,推荐使用一个绝对路径
        compress: true, // 是否开启 gzip 压缩
        historyApiFallback: true, // 是否开发 HTML5 History API 网页
        hot: true, // 是否开启模块热替换功能
        https: false, // 是否开启 HTTPS 模式
		before: function(app, server) {
			res.json({custom: 'response'});
		},              //在服务内部的所有其他中间件之前, 提供执行自定义中间件的功能。 这可以用来配置自定义处理程序
		disableHostCheck: true, // 设置为 true 时,此选项绕过主机检查
		quiet: true      // 启用 `devServer.quiet` 后,除了初始启动信息之外的任何内容都不会被打印到控制台。这也意味着来自 webpack 的错误或警告在控制台不可见。
    },

    profile: true, // 是否捕捉 Webpack 构建的性能信息,用于分析什么原因导致构建性能不佳

    cache: false, // 是否启用缓存提升构建速度

    watch: true, // 是否开始
    watchOptions: {
        // 监听模式选项
        // 不监听的文件或文件夹,支持正则匹配。默认为空
        ignored: /node_modules/,
        // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
        // 默认为300ms
        aggregateTimeout: 300,
        // 判断文件是否发生变化是不停的去询问系统指定文件有没有变化,通过传递 true 开启 polling,或者指定毫秒为单位进行轮询
        poll: 1000,
    },
};

通常你可用如下经验去判断如何配置 Webpack:

  • 想让源文件加入到构建流程中去被 Webpack 控制,配置 entry
  • 想自定义输出文件的位置和名称,配置 output
  • 想自定义寻找依赖模块时的策略,配置 resolve
  • 想自定义解析和转换文件的策略,配置 module,通常是配置 module.rules 里的 Loader
  • 其它的大部分需求可能要通过 Plugin 去实现,配置 plugin

参考文档

【JavaScript】offset、scroll、client

image

image

offset 偏移量

offset 指偏移,包括这个元素在文档中占用的所有显示宽度,包括滚动条、 padding、 border,不包括 overflow 隐藏的部分

  • offsetParent:返回一个对象的引用,这个对象是距离调用 offsetParent 的父级元素中最近的(在包含层次中最靠近的),并且是已进行过 CSS 定位的容器元素。如果这个容器元素未进行 CSS 定位, 则 offsetParent 属性的取值为根元素的引用。

    • 如果当前元素的父级元素中没有进行 CSS 定位(positionabsolute/relative), offsetParentbody

    • 如果当前元素的父级元素中有 CSS 定位( positionabsolute/relative), offsetParent 取父级中最近的元素

  • obj.offsetWidth:指 obj 控件自身的绝对宽度,不包括因 overflow 而未显示的部分,也就是其实际占据的宽度,整型,单位:像素。

     offsetWidth = border-width*2 + padding-left + width + padding-right
  • obj.offsetHeight:指 obj 控件自身的绝对高度,不包括因 overflow 而未显示的部分,也就是其实际占据的高度,整型,单位:像素。

     offsetHeight = border-width*2 + padding-top + height + padding-bottom
  • obj.offsetTop:指 obj 相对于版面或由 offsetParent 属性指定的父坐标的计算上侧位置,整型,单位:像素。

     offsetTop = offsetParent的padding-top + 中间元素的offsetHeight + 当前元素的margin-top
  • obj.offsetLeft: 指 obj 相对于版面或由 offsetParent 属性指定的父坐标的计算左侧位置,整型,单位:像素。

     offsetLeft = offsetParent的padding-left + 中间元素的offsetWidth + 当前元素的margin-left

image

所有这些偏移量属性都是只读的,而且每次访问它们都需要重新计算。因此,应 该尽量避免重复访问这些属性;如果需要重复使用其中某些属性的值,可以将它们保 存在局部变量中,以提高性能。

client 客户区大小

client 指元素本身的可视内容,不包括 overflow 被折叠起来的部分,不包括滚动条、 border,包括 padding。

  • clientWidth: 对象可见的宽度,不包括滚动条等边线,会随窗口的显示大小改变

  • clientHeight: 对象可见的高度

  • clientTopclientLeft: 这两个返回的是元素周围边框的厚度,一般它的值就是 0。因为滚动条不会出现在顶部或者左侧

image

scroll 滚动大小

scroll 指滚动,包括这个元素没显示出来的实际宽度,包括 padding,不包括滚动条、 border。

  • scrollHeight: 获取对象的滚动高度,对象的实际高度;

  • scrollLeft: 设置或获取位于对象左边界和窗口中目前可见内容的最左端之间的距离

  • scrollTop: 设置或获取位于对象最顶端和窗口中可见内容的最顶端之间的距离

  • scrollWidth: 获取对象的滚动宽度

image

确定元素大小

浏览器给每个元素都提供了一个 getBoundingClientRect() 方法。这个方法返回会一个矩形对象,包含 4 个属性:left、top、right 和 bottom。这些属性给出了元素在页面中相对于视口的位置。

Array in JavaScript

非连续内存

本质上JavaScript数组的特殊之处在于JavaScript的数组不一定是连续内存。
而维基百科关于数组的定义:

在计算机科学中,数组数据结构(英语:array data structure),简称数组(英语:Array),是由相同类型的元素(element)的集合所组成的数据结构,分配一块连续的内存来存储。

JavaScript的数组是否分配连续内存取决于数组成员的类型,如果统一是单一类型的数组那么会分配连续内存,如果数组内包括了各种各样的不同类型,那么则是非连续内存。

非连续内存的数组用的是类似哈希映射的方式存在,比如声明了一个数组,他被分配给了1001、2011、1088、1077四个非连续的内存地址,通过指针连接起来形成一个线性结构,那么当我们查询某元素的时候其实是需要遍历这个线性链表结构的,这十分消耗性能。

image

而线性储存的数组只需要遵循这个寻址公式,进行数学上的计算就可以找到对应元素的内存地址。

a[k]_address = base_address + k * type_size

非线性储存的数组其速度比线性储存的数组要慢得多。

数组类型的判断

ES6之前:

var a = [];
// 1.基于instanceof
a instanceof Array;
// 2.基于constructor
a.constructor === Array;
// 3.基于Object.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(a);
// 4.基于getPrototypeOf
Object.getPrototypeOf(a) === Array.prototype;
// 5.基于Object.prototype.toString
Object.prototype.toString.apply(a) === '[object Array]';

以上,除了 Object.prototype.toString 外,其它方法都不能正确判断变量的类型。原因是只要手动指定了某个对象的 __proto__ 属性为 Array.prototype,便会导致了该对象继承了 Array 对象,这种毫不负责任的继承方式,使得基于继承的判断方案瞬间土崩瓦解。

因此 ES6 中有了更好判断数组类型的方法Array.isArray()

Array.isArray([]); // true
Array.isArray({0: 'a', length: 1}); // false

方法

数组原型提供的方法非常之多,主要分为三种,一种是会改变自身值的,一种是不会改变自身值的,另外一种是遍历方法。

改变自身值的方法(9个)

基于ES6,改变自身值的方法一共有9个,分别为pop、push、reverse、shift、sort、splice、unshift,以及两个ES6新增的方法copyWithin 和 fill。
对于能改变自身值的数组方法,日常开发中需要特别注意,尽量避免在循环遍历中去改变原数组的项

不会改变自身的方法(9个)

基于ES7,不会改变自身的方法一共有9个,分别为concat、join、slice、toString、toLocateString、indexOf、lastIndexOf、未标准的toSource以及ES7新增的方法includes。

遍历方法(12个)

基于ES6,不会改变自身的方法一共有12个,分别为forEach、every、some、filter、map、reduce、reduceRight 以及ES6新增的方法entries、find、findIndex、keys、values。

以上方法具体不再赘述,详情可参考:【深度长文】JavaScript数组所有API全解密以及Array - JavaScript | MDN

这些方法之间存在很多共性。比如:

  • 所有插入元素的方法, 比如 push、unshift,一律返回数组新的长度;
  • 所有删除元素的方法,比如 pop、shift、splice 一律返回删除的元素,或者返回删除的多个元素组成的数组;
  • 部分遍历方法,比如 forEach、every、some、filter、map、find、findIndex,它们都包含 function(value,index,array){}thisArg 这样两个形参。

Array.prototype 的所有方法均具有鸭式辨型这种神奇的特性。它们不止可以用来处理数组对象,还可以处理类数组对象。

例如 javascript 中一个纯天然的类数组对象字符串(String),像join方法(不改变当前对象自身)就完全适用,可惜的是 Array.prototype 中很多方法均会去试图修改当前对象的 length 属性,比如说 pop、push、shift, unshift 方法,操作 String 对象时,由于String对象的长度本身不可更改,这将导致抛出TypeError错误。

推荐一个查询方法是不是会造成mutate的网站:Does it mutate?

最佳实践

  • Replacing Array.indexOf with Array.includes
    要获取 index 就用 Array.indexOf,不然就用 Array.includes 获取布尔值
  • Using Array.find instead of Array.filter
    Array.filter 会遍历整个数组,并且返回多个值。Array.find 只返回满足这个回调第一个元素的值
  • Replacing Array.find with Array.some
    Array.some 返回一个需要的布尔值
  • Using Array.reduce instead of chaining Array.filter and Array.map
    后者会遍历两遍数组,推荐使用 Array.reduce 作为数组累加器

参考资料

【Webpack】Optimize 优化

保持最新的版本

使用最新版本,带来的优化是最显著的,开发者们会经常进行性能优化。所以第一步先升级 Webpack 以及 所有构建相关 devDependencies 的版本。

保持最新的 Node.js 也能够保证性能。除此之外,保证包管理工具 (例如 npm 或者 yarn ) 为最新也能保证性能。较新的版本能够建立更高效的模块树以及提高解析速度。

量化

必须要有一个量化指标才可以看出前后对比。

speed-measure-webpack-plugin

speed-measure-webpack-plugin 插件可以测量各个插件和 loader 所花费的时间,使用之后,构建时,会得到类似下面这样的信息:

image

对比前后的信息,来确定优化的效果。
speed-measure-webpack-plugin 的使用很简单,可以直接用来包裹 Webpack 的配置:
webpack.config.js

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

const config = {
    //...webpack配置
}

module.exports = smp.wrap(config);

Webpack 自带时间统计

上面的 speed-measure-webpack-plugin 主要是测量插件以及 loader 花费的时间(主要是 loader),其自身也是耗时间的,因此在对其分析并相应的做出优化之后,应该去除 speed-measure-webpack-plugin后根据 Webpack 每次打包给出时间作为最后的量化指标。

缩小文件搜索范围(更快命中)

优化 loader 配置

通过排除 node_modules 下的文件 从而缩小了 loader 加载搜索范围 高概率命中文件。为了尽可能少的让文件被 loader 处理,可以通过 include 去命中只有哪些文件需要被处理

优化 resolve.modules 配置

resolve.modules 用于配置 Webpack 去哪些目录下寻找第三方模块。resolve.modules 的默认值是 ['node modules'],含义是先去当前目录的 ./node modules 目录下去找我们想找的模块,如果没找到,就去上一级目录 ../node modules 中找,再没有就去 ../.. /node modules 中找,以此类推,这和 Node.js 的模块寻找机制很相似。当安装的第三方模块都放在项目根目录的 ./node modules 目录下时,就没有必要按照默认的方式去一层层地寻找,可以指明存放第三方模块的绝对路径,以减少寻找。

优化后配置:

resolve: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
modules: [path.resolve(__dirname,'node_modules')]
},

注意:如果配置了上述的 resolve.moudles ,可能会出现问题,例如,依赖中还存在 node_modules 目录,那么就会出现,对应的文件明明在,但是却提示找不到。因此呢,不推荐配置这个。如果其他人不熟悉这个配置,遇到这个问题时,会摸不着头脑。

优化 resolve.alias 配置

创建 import 或 require 的路径别名,来确保模块引入变得更简单。配置项通过别名来把原导入路径映射成一个新的导入路径 此优化方法会影响使用 Tree-Shaking 去除无效代码

优化 resolve.extensions 配置

当引入模块时不带文件后缀 webpack会根据此配置自动解析确定的文件后缀。

  • 后缀列表尽可能小
  • 频率最高的往前放
  • 导出语句尽可能带上后缀
resolve: {
    extensions: ['.js']
}

优化 module.noParse 配置

用了 noParse 的模块将不会被 loaders 解析,所以当我们使用的库如果太大,并且其中不包含 import require define 的调用,我们就可以使用这项配置来提升性能, 让 Webpack 忽略对部分没采用模块化的文件的递归解析处理。

// 忽略对jquery lodash的进行递归解析
module: {
    // noParse: /jquery|lodash/

    // 从 webpack 3.0.0 开始
    noParse: function(content) {
        return /jquery|lodash/.test(content)
    }
}

优化 resolve.mainFields 配置

在安装的第三方模块中都会有一个 package.json 文件,用于描述这个模块的属性,其中可以存在多个字段描述入口文件,原因是某些模块可以同时用于多个环境中,针对不同的运行环境需要使用不同的代码。

resolve.mainFields 的默认值和当前的 target 配置有关系,对应的关系如下。

  • targetweb 或者 webworker 时,值是 ['browser','module','main']
  • target 为其他情况时,值是 ['module','main']

target 等于 web 为例, Webpack 会先采用第三方模块中的 browser 字段去寻找模块的入口文件,如果不存在,就采用 module 字段,以此类推。

为了减少搜索步骤,在明确第三方模块的入口文件描述字段时,可以将它设置得尽量少。由于大多数第三方模块都采用 main 字段去描述入口文件的位置,所以可以这样配置:

module.exports = {
        resolve: {
        //只采用 main 字段作为入口文件的描述字段,以减少搜索步骤
        mainFields: ['main']
        }
    }

优化解析时间(开启多进程打包)

thread-loader

thread-loader 放置在其它 loader 之前,那么放置在这个 loader 之后的 loader 就会在一个单独的 worker 池中运行。

在 worker 池(worker pool)中运行的 loader 是受到限制的。例如:

  • 这些 loader 不能产生新的文件。
  • 这些 loader 不能使用定制的 loader API(也就是说,通过插件)。
  • 这些 loader 无法获取 webpack 的选项设置。

thread-loader 使用起来也非常简单,只要把 thread-loader 放置在其他 loader 之前, 那 thread-loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行。

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        // 创建一个 js worker 池
        use: [ 
          'thread-loader',
          'babel-loader'
        ] 
      },
      {
        test: /\.s?css$/,
        exclude: /node_modules/,
        // 创建一个 css worker 池
        use: [
          'style-loader',
          'thread-loader',
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localIdentName: '[name]__[local]--[hash:base64:5]',
              importLoaders: 1
            }
          },
          'postcss-loader'
        ]
      }
      // ...
    ]
    // ...
  }
  // ...
}

官方上说每个 worker 大概都要花费 600ms ,所以官方为了防止启动 worker 时的高延迟,提供了对 worker 池的优化:预热。

// ...
const threadLoader = require('thread-loader');

const jsWorkerPool = {
  // options
  
  // 产生的 worker 的数量,默认是 (cpu 核心数 - 1)
  // 当 require('os').cpus() 是 undefined 时,则为 1
  workers: 2,
  
  // 闲置时定时删除 worker 进程
  // 默认为 500ms
  // 可以设置为无穷大, 这样在监视模式(--watch)下可以保持 worker 持续存在
  poolTimeout: 2000
};

const cssWorkerPool = {
  // 一个 worker 进程中并行执行工作的数量
  // 默认为 20
  workerParallelJobs: 2,
  poolTimeout: 2000
};

threadLoader.warmup(jsWorkerPool, ['babel-loader']);
threadLoader.warmup(cssWorkerPool, ['css-loader', 'postcss-loader']);


module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'thread-loader',
            options: jsWorkerPool
          },
          'babel-loader'
        ]
      },
      {
        test: /\.s?css$/,
        exclude: /node_modules/,
        use: [
          'style-loader',
          {
            loader: 'thread-loader',
            options: cssWorkerPool
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localIdentName: '[name]__[local]--[hash:base64:5]',
              importLoaders: 1
            }
          },
          'postcss-loader'
        ]
      }
      // ...
    ]
    // ...
  }
  // ...
}

注意:

  • thread-loader 放在了 style-loader 之后,这是因为 thread-loader 后的 loader 没法存取文件也没法获取 webpack 的选项设置。
  • 请仅在耗时的 loader 上使用。

HappyPack(已不维护,不推荐 Webpack4 使用)

HappyPack 是让 webpack 对 loader 的执行过程,从单一进程形式扩展为多进程模式,也就是将任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程,从而加速代码构建。与 DLL 动态链接库结合来使用更佳。

合理利用缓存(缩短连续构建时间,增加初始构建时间)

开启缓存之后,在第一构建之后,会产生缓存文件,一般默认缓存目录:node_modules/.cache

cache-loader

将结果缓存到磁盘里,显著提升二次构建速度。

module.exports = {
  module: {
    rules: [
      {
        test: /\.ext$/,
        use: ['cache-loader', ...loaders],
        include: path.resolve('src'),
      },
    ],
  },
};

注意:

  • 保存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader 使用此 loader。
  • thread-loadercache-loader 要一起使用的話,先放 cache-loader,接着是 thread-loader,最后才是其它的 heavy-loader,这样的順序才可以有最好的效能。
  • 如果只打算给 babel-loader 配置 cache 的话,也可以不使用 cache-loader,给 babel-loader 增加选项 cacheDirectorytrue

HardSourceWebpackPlugin

HardSourceWebpackPlugin 为模块提供中间缓存,缓存默认的存放路径是: node_modules/.cache/hard-source

配置 hard-source-webpack-plugin,首次构建时间没有太大变化,但是第二次开始,构建时间大约可以节约 80%。

DllPlugin

在一个动态链接库中可以包含其他模块调用的函数和数据,动态链接库只需被编译一次,在之后的构建过程中被动态链接库包含的模块将不会被重新编译,而是直接使用动态链接库中的代码。

  • 将web应用依赖的基础模块抽离出来,打包到单独的动态链接库中。一个链接库可以包含多个模块。
  • 当需要导入的模块存在于动态链接库,模块不会再次打包,而是去动态链接库中去获取。
  • 页面依赖的所有动态链接库都需要被加载。

如何使用不记录了,因为现在已经不推荐使用 DllPlugin 了,在最新的 webpack 下带来的性能提升有限。

TerserPlugin

Webpack4 默认内置使用 terser-webpack-plugin 插件压缩优化代码,而该插件使用 terser 来缩小 JavaScript。所谓 terser,官方给出的定义是用于 ES6+ 的 JavaScript 解析器、mangler/compressor(压缩器)工具包。

使用多进程并行运行来提高构建速度。并发运行的默认数量为 os.cpus().length - 1

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true,
      }),
    ],
  },
};

可以显著加快构建速度,因此强烈推荐开启多进程。

ParallelUglifyPlugin()

Webpack4 已废弃,不推荐 Webpack4 使用。

CSS 代码的压缩

通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件,通过 css-loader 的 minimize 选项开启 cssnano 压缩 CSS。

控制包文件大小

减少编译的整体大小,以提高构建性能。尽量保持 chunks 小巧。

结合stats.json分析打包结果(bundle analyze)

提取页面公共资源

基础包分离:

  • 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中
  • 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件

tree-shaking

打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码
purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)。

scope-hosting

构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法,为了充分发挥 Scope hoisting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法

变量提升,可以减少一些变量声明。在生产环境下,默认开启。

另外,大家测试的时候注意一下,speed-measure-webpack-plugin 和 HotModuleReplacementPlugin 不能同时使用,否则会报错。

babel 配置优化

在不配置 @babel/plugin-transform-runtime 时,babel 会使用很小的辅助函数来实现类似 _createClass 等公共方法。默认情况下,它将被注入(inject)到需要它的每个文件中。但是这样的结果就是导致构建出来的JS体积变大。

我们也并不需要在每个 js 中注入辅助函数,因此我们可以使用 @babel/plugin-transform-runtime@babel/plugin-transform-runtime 是一个可以重复使用 Babel 注入的帮助程序,以节省代码大小的插件。

因此我们可以在 .babelrc 中增加 @babel/plugin-transform-runtime 的配置。

{
    "presets": [],
    "plugins": [
        [
            "@babel/plugin-transform-runtime"
        ]
    ]
}

图片压缩

开发环境的优化

增量编译

使用 webpack 的监听模式。不要使用其他工具来监听你的文件和调用 webpack 。在监听模式下构建会记录时间戳并将信息传递给编译让缓存失效。

在某些设置中,监听会回退到轮询模式。有许多监听文件会导致 CPU 大量负载。在这些情况下,你可以使用 watchOptions.poll 来增加轮询的间隔。

Devtool

需要注意的是不同的 devtool 的设置,会导致不同的性能差异。

  • "eval" 具有最好的性能,但并不能帮助你转译代码。
  • 如果你能接受稍差一些的 mapping 质量,可以使用 cheap-source-map 选项来提高性能
  • 使用 eval-source-map 配置进行增量编译。

=> 在大多数情况下,cheap-module-eval-source-map 是最好的选择。

避免在生产环境下才会用到的工具

某些实用工具, plugins 和 loaders 都只能在构建生产环境时才有用。例如,在开发时使用 UglifyJsPlugin 来压缩和修改代码是没有意义的。以下这些工具在开发中通常被排除在外:

  • UglifyJsPlugin
  • ExtractTextPlugin
  • [hash] / [chunkhash]
  • AggressiveSplittingPlugin
  • AggressiveMergingPlugin
  • ModuleConcatenationPlugin

在内存中编译

以下几个实用工具通过在内存中进行代码的编译和资源的提供,但并不写入磁盘来提高性能:

  • webpack-dev-server
  • webpack-hot-middleware
  • webpack-dev-middleware

参考文档

【浏览器】回流与重绘

reflow(回流/重构)

当渲染树中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建, 这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。

repaint/redraw(重绘)

当盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来之后,浏览器便把这些原色都按照各自的特性绘制一遍,将内容呈现在页面上。重绘是指一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。
重绘发生在元素的可见的外观被改变,但并没有影响到布局的时候。比如,仅修改DOM元素的字体颜色(只有Repaint,因为不需要调整布局)

何时发生回流重绘

回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流。比如以下情况:

  • 添加或删除可见的DOM元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
  • 页面一开始渲染的时候(这肯定避免不了)
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

注意:回流一定会触发重绘,而重绘不一定会回流

根据改变的范围和程度,渲染树中或大或小的部分需要重新计算,有些改变会触发整个页面的重排,比如,滚动条出现的时候或者修改了根节点。

回流的优化

对树的局部甚至全局重新生成是非常耗性能的,所以要避免频繁触发回流

  • 现代浏览器已经帮我们做了优化,采用队列存储多次的回流操作,然后批量执行,但获取布局信息例外,因为要获取到实时的数值,浏览器就必须要清空队列,立即执行回流。
  • 编码上,避免连续多次修改,可通过合并修改,一次触发
  • 对于大量不同的 DOM 修改,可以先将其脱离文档流,比如使用绝对定位,或者 display:none ,在文档流外修改完成后再放回文档里中
  • 通过节流和防抖控制触发频率
  • CSS3 硬件加速,transformopacityfilters,开启后,会新建渲染层

浏览器的优化机制

现代的浏览器都是很聪明的,由于每次重绘都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重绘过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。但是!当你获取布局信息的操作的时候,会强制队列刷新,比如当你访问以下属性或者使用以下方法:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • getComputedStyle()
  • getBoundingClientRect

具体可以访问这篇文章:what-forces-layout

以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新渲染队列。如果要使用它们,最好将值缓存起来。

开启GPU加速的方法

开启后,会将 dom 元素提升为独立的渲染层,它的变化不会再影响文档流中的布局。

  • transform: translateZ(0)
  • opacity
  • filters
  • Will-change

最小化重绘和重排

由于重绘和重排可能代价比较昂贵,因此最好就是可以减少它的发生次数。为了减少发生次数,我们可以合并多次对DOM和样式的修改,然后一次处理掉。

参考资料

【JavaScript】Reducer

reducer 是什么

要理解 reducer 的第一点也是最重要的一点是它永远返回一个值,这个值可以是数字、字符串、数组或对象,但它始终只能是一个。reducer 对于很多场景都很适用,但是它们对于将一种逻辑应用到一组值中并最终得到一个单一结果的情况特别适用。

另外需要说明:reducer 本质上不会改变你的初始值;相反,它们会返回一些其他的东西。

语法:

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

示例

有多少个 X

假设您有一个数字数组,并且希望返回一个报告这些数字在数组中出现的次数的对象。请注意,这同样适用于字符串。

const nums = [3, 5, 6, 82, 1, 4, 3, 5, 82];

const result = nums.reduce((tally, amt) => {
    tally[amt] ? tally[amt]++ : tally[amt] = 1;
    return tally;
}, {});

console.log(result);
//{ '1': 1, '3': 2, '4': 1, '5': 2, '6': 1, '82': 2 }

最初,我们有一个数组和将要放入其中的对象。在 reducer 中,我们首先判断这个item是否存在于累加器中,如果是存在,加1。如果不存在,添加这一项并设置为1。最后,请返回每一项出现的次数。然后,我们运行reduce函数,同时传递 reducer 和初始值。

获取一个数组并将其转换为显示某些条件的对象

假设我们有一个数组,我们希望基于一组条件创建一个对象。reduce 在这里非常适用!现在,我们希望从数组中任意一个数字项创建一个对象,并同时显示该数字的奇数和偶数版本。

const nums = [3, 5, 6, 82, 1, 4, 3, 5, 82];

// we're going to make an object from an even and odd
// version of each instance of a number
const result = nums.reduce((acc, item) => {
  acc[item] = {
    odd: item % 2 ? item : item - 1,
    even: item % 2 ? item + 1 : item
  }
  return acc;
}, {});

console.log(result);

控制台输出结果:

{ 
	'1': { odd: 1, even: 2 },
	'3': { odd: 3, even: 4 },
	'4': { odd: 3, even: 4 },
	'5': { odd: 5, even: 6 },
	'6': { odd: 5, even: 6 },
	'82': { odd: 81, even: 82 } 
}

当我们遍历数组中的每一项时,我们为偶数和奇数创建一个属性,并且基于一个带模数运算符的内联条件,我们要么存储该数字,要么将其递增1。模算符非常适合这样做,因为它可以快速检查偶数或奇数 —— 如果它可以被2整除,它是偶数,如果不是,它是奇数。

找最大值

const vals = [5, 4, 9, 2, 1];
const biggest = vals.reduce((acc, val) => {
  if (val > acc) {
    acc = val;
  }
  return acc;
});
console.log(biggest);

优化精简一下:

const vals = [5, 4, 9, 2, 1];
const biggest = vals.reduce((a, b) => a > b ? a : b);
console.log(biggest);

.reduce() 代替 .filter() .map()

const numbers = [-5, 6, 2, 0,];
const doubledPositiveNumbers = numbers.reduce((accumulator, currentValue) => {
  if (currentValue > 0) {
    const doubled = currentValue * 2;
    accumulator.push(doubled);
  }
  return accumulator;
}, []);
console.log(doubledPositiveNumbers); // [12, 4]

数组去重

const array = ['a', 'b', 'a', 'b', 'c', 'e', 'e', 'c', 'd', 'd', 'd', 'd']
const orderedArray = array.reduce((acc, cur) => acc.indexOf(cur) === -1 ? acc.push(cur) : acc, [])
console.log(orderedArray);

二维数组拍平

const flattened = [[0 ,1], [2, 3], [4, 5]].reduce((acc, cur) => acc.concat(cur), []);

用 reduce 实现 map

if (!Array.prototype.mapUsingReduce) {
  Array.prototype.mapUsingReduce = function(callback, thisArg) {
    return this.reduce(function(mappedArray, currentValue, index, array) {
      mappedArray[index] = callback.call(thisArg, currentValue, index, array)
      return mappedArray
    }, [])
  }
}

[1, 2, , 3].mapUsingReduce(
  (currentValue, index, array) => currentValue + index + array.length
) // [5, 7, , 10]

参考文档

【浏览器】跨域

image


同源策略

浏览器有同源策略,只有当“协议”、“域名”、“端口号”都相同时,才能称之为是同源,其中有一个不同,即是跨域。

image

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

同源策略限制内容有:

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM 节点
  • AJAX 请求发送后,结果被浏览器拦截了

但是这里有个例外,所有带 src 属性的标签都可以跨域加载资源,不受同源策略的限制,同时这个特性也会被用作 CSRF 攻击。

跨域

不同域之间相互请求资源,就算作“跨域”。

跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。

跨域的方法

1. jsonp

尽管浏览器有同源策略,但是 <script> 标签的 src 属性不会被同源策略所约束,可以获取任意服务器上的脚本并执行。jsonp 通过插入 script 标签的方式来实现跨域,参数只能通过url传入,仅能支持get请求。

原理:利用<script>标签不受跨域限制,将回调函数名作为参数附带在请求中,服务器接受到请求后,进行特殊处理:把接收到的函数名和需要给它的数据拼接成一个字符串返回,客户端会调用相应声明的函数,对返回的数据进行处理。
image

优点:

  1. 它不像XMLHttpRequest 对象实现 Ajax 请求那样受到同源策略的限制
  2. 兼容性很好,在古老的浏览器也能很好的运行
  3. 不需要 XMLHttpRequest 或 ActiveX 的支持;并且在请求完毕后可以通过调用 callback 的方式回传结果。

缺点:

  1. 它支持 GET 请求而不支持 POST 等其它类行的 HTTP 请求。
  2. 它只支持跨域 HTTP 请求这种情况,不能解决不同域的两个页面或 iframe 之间进行数据通信的问题
  3. 无法捕获 Jsonp 请求时的连接异常,只能通过超时进行处理

2. CORS

CORS 是一个 W3C 标准,全称是"跨域资源共享"(Cross-origin resource sharing)它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 ajax 只能同源使用的限制。
CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。

先了解一下,请求分简单请求与复杂请求,复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为 预检 请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

cors请求流程图:
image

优缺点:

  1. 使用简单方便,更为安全
  2. 支持 POST 请求方式
  3. CORS 是一种新型的跨域问题的解决方案,存在兼容问题,仅支持 IE 10 以上

3. postMessage

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

页面和其打开的新窗口的数据传递
多窗口之间消息传递
页面与嵌套的iframe消息传递
上面三个场景的跨域数据传递

postMessage() 方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。

4. websocket

Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。

5.Node中间件代理(两次跨域)

实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。 代理服务器,需要做以下几个步骤:

  1. 接受客户端请求
  2. 将请求 转发给服务器
  3. 拿到服务器 响应 数据
  4. 将 响应 转发给客户端

image

6. nginx反向代理

实现原理类似于Node中间件代理,需要搭建一个中转nginx服务器,用于转发请求。
使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。

原理:
image

7. window.name + iframe

window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

8. location.hash + iframe

实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。

9. document.domain + iframe

该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。
只需要给页面添加 document.domain ='test.com' 表示二级域名都相同就可以实现跨域。
实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

跨域时cookie处理

客户端处理

  1. JSONP默认能带上cookie,利用这个特性可以用做跨站请求伪造(CSRF)
  2. ajax默认不带cookie,需要设置相应属性:withCredentials
  3. axios设置:axios.defaults.withCredentials=true

服务端处理

nginx配置:

  1. Access-Control-Allow-Credentials:可选字段。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为 true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为 true,如果服务器不要浏览器发送 Cookie,删除该字段即可。
  2. 对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin 的值为 *。这是因为请求的首部中携带了Cookie信息,如果 Access-Control-Allow-Origin 的值为 *,请求将会失败。而将 Access-Control-Allow-Origin 的值设置为 a.b.com,则请求将成功执行。也就是说 Access-Control-Allow-Credentials 设置为 true 的情况下
    Access-Control-Allow-Origin 不能设置为 *

参考资料

【JavaScript】事件循环 Event Loop

执行 & 运行

JavaScript 的执行和运行是两个不同概念的,执行,一般依赖于环境,比如 node、浏览器、Ringo 等, JavaScript 在不同环境下的执行机制可能并不相同。而 Event Loop 就是 JavaScript 的一种执行方式。所以下文我们还会梳理 node 的执行方式。而运行呢,是指JavaScript 的解析引擎。这是统一的。

image

JavaScript 有一个主线程 main thread,和调用栈 call-stack 也称之为执行栈。所有的任务都会放到调用栈中等待主线程来执行。

js引擎始终只有一个线程,它维护一个消息队列,当前函数栈执行完成之后就去不断地取消息队列中的消息(回调),取到了就执行。但是js引擎只负责取消息,不负责生产消息

js运行时,就负责给js引擎线程发送消息。比如浏览器DOM事件发送一条鼠标点击的消息(浏览器子线程和js引擎线程的IPC通信),那么js引擎在执行完函数栈之后就会取到这条鼠标点击信息,执行消息(即回调);比如node运行时读取文件,执行系统调用,完成后发送读取文件完成的消息,之后的过程同上。js运行时只负责生产消息,不负责取消息

单线程

JS 是单线程的,所以需要 Event Loop 来调度。JS 是通过事件队列 (EventLoop)的方式来实现异步回调的。
DOM API / 定时器 / http 请求这些 Web API都是浏览器提供的,帮助我们实现异步 / 非阻塞的行为 。

Event Loop

image

  1. JS 在执行代码时,代码首先进入执行栈,代码中可能包含一些同步任务和异步任务。
  2. 同步任务立即执行,执行完出栈,over。
  3. 异步任务也就是常见的 ajax 请求、setTimeout 等,代码调用到这些 api 的时候,WebAPIs 来处理这些问题,执行栈继续执行。
  4. 异步任务有了运行结果时,(当 ajax 请求结果返回时),WebAPIs 把对应的回调函数放到任务队列。
  5. 执行栈为空时来读取任务队列中的第一个函数,压入执行栈。
  6. 步骤 5 不断重复,执行栈为空时,系统就去任务队列中拿第一个函数压入栈继续执行。这个过程不断重复,这就是事件循环(Event Loop)。

Event Loop 的工作就是连接任务队列和调用栈,当调用栈中的任务均执行完毕出栈,调用栈为空时,Event Loop 会检查任务队列中是否存在等待执行的任务,如果存在,则取出队列中第一个任务,放入调用栈。

宏任务和微任务

  • 宏任务 (macro-task / ES6 规范称为 task ):script(整体代码)setTimeoutsetIntervalI/OsetImmedidate(Node.js)requestAnimationFrame
  • 微任务 (micro-task / ES6 规范称为 jobs):queueMicrotaskprocess.nextTick(Node.js)MutationObserverPromise.then /catch/finally

其实也可以理解为除了微任务以外的其它任务都是宏任务

任务队列,是包括一个宏任务队列和一个微任务队列的。每次执行栈为空的时候,系统会优先处理微任务队列,处理完微任务队列里的所有任务,再去处理宏任务。

  • 在当前的微任务没有执行完成时,是不会执行下一个宏任务的。
  • 所有会进入的异步都是指的事件回调中的那部分代码。

image

注意:

  • 根据 Promise 中 then 使用方式的不同做出不同的判断,是链式还是分别调用
  • process.nextTick 优先级高于 Promise.then

queueMicrotask

为什么需要这个 api?

  • 我们应当使用底层 api 来直接完成类似的功能,而非用顶层 api 进行模拟
  • 模拟过程中,对于异常情况,会造成一些困扰,比如 Promise.resolve 会将异常转化为一个 rejected 的 Promise
  • 模拟过程中,会创建额外的对象(造成一定意义上的浪费),比如 Promise.resolve 会返回一个 Promise 实例对象,而直接 queueMicrotask 则不会
  • 除了微任务,其他类型的异步任务都有对应的 api 可供使用,比如宏任务、RAF
  • 继上一点的基础上,语义性会更好,同时帮助开发者理解这些不同异步任务之间的区别
    • `setTimeout(callback, 0)`` - 宏任务
    • requestAnimationFrame(callback) - RAF
    • queueMicrotask(callback) - 微任务

window.requestAnimationFrame(…)

Even though window.requestAnimationFrame(…) is a function of DOM object window, it’s callback is queued in a micro task queue but it’s execution strategy is different.

参考资料

【JavaScript】执行上下文

image

定义

当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)

一共三种类型:

  • 全局执行上下文:只有一个,浏览器中的全局对象就是 window 对象,this 指向这个全局对象。
  • 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文。
  • Eval 函数执行上下文: 指的是运行在 eval 函数中的代码,很少用而且不建议使用。

对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

执行上下文栈

JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文

执行上下文的创建

执行上下文分两个阶段创建:1)创建阶段; 2)执行阶段

1)创建阶段

  1. 确定 this 的值,也被称为 This Binding。
  2. Lexical Environment(词法环境) 组件被创建。
  3. Variable Environment(变量环境) 组件被创建。

词法环境(Lexical Environment)

词法环境有两个组成部分

  • 环境记录:存储变量和函数声明的实际位置
  • 对外部环境的引用:可以访问其外部词法环境

词法环境有两种类型

  • 全局环境:是一个没有外部环境的词法环境,其外部环境引用为 null。拥有一个全局对象(window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,this 的值指向这个全局对象。
  • 函数环境:用户在函数中定义的变量被存储在环境记录中,包含了arguments 对象。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。

变量环境(Variable Environment)

变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性。

在 ES6 中,词法 环境和 变量 环境的区别在于前者用于存储函数声明和变量( let 和 const )绑定,而后者仅用于存储变量( var )绑定。

变量提升的原因:在创建阶段,函数声明存储在环境中,而变量会被设置为 undefined(在 var 的情况下)或保持未初始化(在 let 和 const 的情况下)。所以这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined ),但如果在声明之前访问 let 和 const 定义的变量就会提示引用错误的原因。这就是所谓的变量提升。

2)执行阶段

此阶段,完成对所有变量的分配,最后执行代码。

如果 Javascript 引擎在源代码中声明的实际位置找不到 let 变量的值,那么将为其分配 undefined 值。

参考资料

【CSS】层叠上下文

定义

层叠上下文

层叠上下文,英文称作”stacking context”,是HTML中的一个三维的概念。如果一个元素含有层叠上下文,我们可以理解为这个元素在z轴上就“高人一等”。

层叠水平

“层叠水平”英文称作 ”stacking level” ,决定了同一个层叠上下文中元素在 z 轴上的显示顺序。

遵循”后来居上“和”谁大谁上“的层叠准则。

普通元素的层叠水平优先由层叠上下文决定,因此,层叠水平的比较只有在当前层叠上下文元素中才有意义。

层叠顺序

“层叠顺序”英文称作 ”stacking order”。表示元素发生层叠时候有着特定的垂直显示顺序,注意,这里跟上面两个不一样,上面的层叠上下文和层叠水平是概念,而这里的层叠顺序是规则

image
著名的7阶层叠水平

在不考虑CSS3的情况下,当元素发生层叠时,层叠顺讯遵循上面途中的规则。
这里值得注意的是:

  1. 左上角 "层叠上下文background/border" 指的是层叠上下文元素的背景和边框。
  2. inline / inline-block 元素的层叠顺序要高于 block (块级)/ float (浮动)元素。
  3. 如果层叠上下文元素不依赖z-index 值,z-index: auto 可看成 z-index: 0 级别。而如果层叠上下文元素依赖 z-index 值,其层叠顺序由 z-index 值决定。

z-index与层叠上下文:

  1. 定位元素默认 z-index: auto 可以看成是 z-index: 0
  2. z-index 不为 auto 的定位元素创建层叠上下文
  3. z-index 层叠顺序的比较止步于父级层叠上下文

如何创建层叠上下文

  • 页面根元素天生具有层叠上下文,称为”根层叠上下文“
  • 普通元素设置 position 属性为非 static 值并设置 z-index 属性为具体数值,产生层叠上下文
  • CSS3 中的新属性也可以产生层叠上下文

CSS3 中的属性对层叠上下文的影响

  1. 父元素的 display 属性值为flex|inline-flex,子元素z-index属性值不为auto的时候,子元素为层叠上下文元素;
  2. 元素的 opacity 属性值不是 1
  3. 元素的 transform属性值不是 none
  4. 元素 mix-blend-mode 属性值不是 normal
  5. 元素的 filter属性值不是 none
  6. 元素的 isolation属性值是 isolate
  7. will-change指定的属性值为上面任意一个;
  8. 元素的 -webkit-overflow-scrolling 属性值设置为 touch

层叠上下文的特性

  • 层叠上下文可以嵌套,组合成一个分层次的层叠上下文
  • 每个层叠上下文和兄弟元素独立:当进行层叠变化和渲染的时候,只需要考虑后代元素
  • 每个层叠上下文是自成体系的:当元素的内容被层叠后,整个元素被认为是在父层的层叠顺序中

最佳实践

  • 不犯二准则:对于非浮层元素,避免设置 z-index 值,z-index 值没有任何道理需要超过 2
  • 对于浮层元素,可以通过 JS 获取 body 下子元素的最大 z-index 值,然后在此基础上加 1 作为浮层元素的 z-index

对于非浮层元素,不要过多地去运用 z-index 去调整显示顺序,要灵活地去运用层叠水平和"后来居上"的准则去让元素获得正确的显示,如果是在要设置 z-index 去调整,不建议非浮层元素 z-index 数值超过 2,对于 DOM 元素,-1, 0, 1, 2 足够让元素有正确的显示顺序。

对于浮层元素,往往是第三方组件开发,当你无法确认你的浮层是否会百分百覆盖在 DOM 树上的时候,你可以去动态获取页面 body 元素下所有子元素 z-index 的最大值,在此基础加一作为浮层元素 z-index 值,用于保证该浮层元素能够显示在最上方。

参考资料

【浏览器】Web安全

网络安全


XSS

Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。

XSS类型

类型 存储区 插入点
存储型 XSS 后端数据库 HTML
反射型 XSS URL HTML
DOM 型 XSS 后端数据库/前端存储/URL 前端 JavaScript

反射型XSS

反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。

POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见。

DOM 型 XSS

DOM 型 XSS 攻击,实际上就是前端 JavaScript 代码不够严谨,把不可信的内容插入到了页面。在使用 .innerHTML.outerHTML.appendChilddocument.write()等API时要特别小心,不要把不可信的数据作为 HTML 插到页面上,尽量使用 .innerText.textContent.setAttribute() 等。

存储型XSS

恶意脚本永久存储在目标服务器上。当浏览器请求数据时,脚本从服务器传回并执行,影响范围比反射型和DOM型XSS更大。存储型XSS攻击的原因仍然是没有做好数据过滤:前端提交数据至服务端时,没有做好过滤;服务端在接受到数据时,在存储之前,没有做过滤;前端从服务端请求到数据,没有过滤输出。

防御手段

主要防御手段:字符转义。

其他防御手段:

除了谨慎的转义,我们还需要其他一些手段来防范XSS攻击。

1. Content Security Policy
本质也是白名单,通过设置白名单, 我们可以设置允许浏览器加载哪些外部资源。
在服务端使用 HTTP的 Content-Security-Policy 头部来指定策略,或者在前端设置 meta 标签。

严格的 CSP 在 XSS 的防范中可以起到以下的作用:

  1. 禁止加载外域代码,防止复杂的攻击逻辑。
  2. 禁止外域提交,网站被攻击后,用户的数据不会泄露到外域。
  3. 禁止内联脚本执行(规则较严格,目前发现 GitHub 使用)。
  4. 禁止未授权的脚本执行(新特性,Google Map 移动版在使用)。
  5. 合理使用上报可以及时发现 XSS,利于尽快修复问题。

CSP 文档地址:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy

2. 输入内容长度控制

对于不受信任的输入,都应该限定一个合理的长度。虽然无法完全防止 XSS 发生,但可以增加 XSS 攻击的难度。

3. 输入内容限制

对于部分输入,可以限定不能包含特殊字符或者仅能输入数字等。

4. 其他安全措施

  • HTTP-only Cookie: 禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
  • 验证码:防止脚本冒充用户提交危险操作。

CSRF

CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

image

CSRF的特点:

  1. 攻击通常在第三方网站发起,如图上的站点B,站点A无法防止攻击发生。
  2. 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;并不会去获取cookie信息(cookie有同源策略)
  3. 跨站请求可以用各种方式:图片URL、超链接、CORS、Form提交等等(来源不明的链接,不要点击)

防御:

1. 添加验证码(体验不好)

验证码能够防御CSRF攻击,但是由于用户的体验会非常差,因此只在重要操作时增加验证码,确保账户安全。

2. 判断请求的来源:检测Referer(并不安全,Referer可以被更改)

Referer 可以作为一种辅助手段,来判断请求的来源是否是安全的,但是鉴于 Referer 本身是可以被修改的,因为不能仅依赖于 Referer

3. 使用Token(主流)

CSRF攻击之所以能够成功,是因为服务器误把攻击者发送的请求当成了用户自己的请求。那么我们可以要求所有的用户请求都携带一个CSRF攻击者无法获取到的Token。服务器通过校验请求是否携带正确的Token,来把正常的请求和攻击的请求区分开。跟验证码类似,只是用户无感知。

  • 服务端给用户生成一个token,加密后传递给用户
  • 用户在提交请求时,需要携带这个token
  • 服务端验证token是否正确

4. Samesite Cookie属性

为了从源头上解决这个问题,Google起草了一份草案来改进HTTP协议,为Set-Cookie响应头新增Samesite属性,它用来标明这个 Cookie是个“同站 Cookie”,同站Cookie只能作为第一方Cookie,不能作为第三方Cookie,Samesite 有两个属性值,分别是 Strict 和 Lax。部署简单,并能有效防御CSRF攻击,但是存在兼容性问题。

Samesite=Strict
Samesite=Strict 被称为是严格模式,表明这个 Cookie 在任何情况都不可能作为第三方的 Cookie,有能力阻止所有CSRF攻击。此时,我们在B站点下发起对A站点的任何请求,A站点的 Cookie 都不会包含在cookie请求头中。

Samesite=Lax
Samesite=Lax 被称为是宽松模式,与 Strict 相比,放宽了限制,允许发送安全 HTTP 方法带上 Cookie,如 Get / OPTIONSHEAD 请求。但是不安全 HTTP 方法,如: POSTPUTDELETE 请求时,不能作为第三方链接的 Cookie。

为了更好的防御CSRF攻击,可以组合使用以上防御手段。

点击劫持

frame busting

if ( top.location != window.location ){
    top.location = window.location
}

需要注意的是: HTML5 中 iframe 的 sandbox 属性、IE 中 iframe的 security 属性等,都可以限制 iframe 页面中的 JavaScript 脚本执行,从而可以使得 frame busting 失效。

X-Frame-Options

X-FRAME-OPTIONS是微软提出的一个http头,专门用来防御利用iframe嵌套的点击劫持攻击。并且在IE8、Firefox3.6、Chrome4以上的版本均能很好的支持。
可以设置为以下值:

  • DENY: 拒绝任何域加载
  • SAMEORIGIN: 允许同源域下加载
  • ALLOW-FROM: 可以定义允许 frame 加载的页面地址

中间人攻击(Man-in-the-Middle Attack, MITM)

MITM 攻击就是通过拦截正常的网络通信数据,并进行数据篡改和嗅探来达到攻击的目的,而通信的双方却毫不知情。

防御:

  1. 网站使用HTTPS
  2. 服务器执行HSTS协议:在HTTP Header中增加 Strict-Transport-Security 请求头

安全扫描工具

Mozilla HTTP Observatory
检查的主要范围包括:

  • Cookie
  • 跨源资源共享(CORS)
  • 内容安全策略(CSP)
  • HTTP公钥固定(Public Key Pinning)
  • HTTP严格安全传输(HSTS)状态
  • 是否存在HTTP到HTTPs的自动重定向
  • 子资源完整性(Subresource Integrity)
  • X-Frame-Options
  • X-XSS-Protection

参考资料

【Webpack】常用 plugins 与配置

压缩JS文件

uglifyjs-webpack-plugin

webpack.config.js

const UglifyWebpackPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
    optimization: {
        minimizer: [
            new UglifyWebpackPlugin({
                parallel: 4
            })
        ]
    }
}

terser-webpack-plugin

使用terser-webpack-plugin来压缩js文件,替换掉之前的 uglifyjs-webpack-plugin,解决uglifyjs不支持es6语法问题

webpack.config.js

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  plugins: [
    new TerserPlugin({
	    parallel: true,  // parallel 代表开启多进程
	    cache: true	     // 代表设置缓存
	}),
  ],
};

压缩CSS文件

使用 optimize-css-assets-webpack-plugin 来压缩 css 文件。

webpack.config.js

const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
    optimization: {
        minimizer: [
            new OptimizeCssAssetsWebpackPlugin()
        ]
    }
}

OptimizeCSSAssetsPlugin 中加载了一个 cssnano 的东西, cssnano 是 PostCSS 的 CSS 优化和分解插件,会自动采用格式很好的 CSS,并通过许多优化,以确保最终的生产环境尽可能小。

拷贝静态文件

可以利用 copy-webpack-plugin 这个插件将文件拷贝到目标目录下

webpack.config.js

const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
  plugins: [
    new CopyPlugin([
      { from: 'source', to: 'dest' },
      { from: 'other', to: 'public' },
    ]),
  ],
};

打包前先清空输出目录

const {CleanWebpackPlugin} = require('clean-webpack-plugin');

module.exports = {
    plugins: [
        new CleanWebpackPlugin()
    ]
}

不是很建议这么使用,一般是在build命令前加rm -rf dist && webpack

热加载

代码改动,动态局部加载 HMR(模块热替换)

webpack自带的插件HotModuleReplacementPlugin、NamedModulesPlugin

const webpack = require('webpack');

module.exports = {
    plugins: [
            new webpack.NamedModulesPlugin(), // webpack自带的插件 以便查看修补patch依赖
    		new webpack.HotModuleReplacementPlugin()
    ]
}

实现一个深拷贝

1. 初级版

深拷贝可以拆分成两步:浅拷贝+递归,浅拷贝时判断属性值是否是对象,如果是对象就进行递归操作,两个一结合就实现了深拷贝。

// 写法一
function cloneDeep1(source) {
    var target = {};
    for(var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            if (typeof source[key] === 'object') {
				// 如果当前结果是一个对象,那我们就继续递归这个结果
                target[key] = cloneDeep1(source[key]);
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}
// 写法二
function cloneDeep1(obj) {
    const keys = Object.keys(obj);
    return keys.reduce((memo, current) => {
        const value = obj[current];
        if (typeof value === 'object') {
            // 如果当前结果是一个对象,那我们就继续递归这个结果
            return {
                ...memo,
                [current]: cloneDeep1(value),
            };
        }
        return {
            ...memo,
            [current]: value,
        };
    }, {});
}

存在的问题:

  1. 只能处理 Plain Object,比如 null、数组、Symbol、key 里面 getter,setter 以及原型链上的内容无法处理
  2. 无法处理内部有循环引用的情况

拷贝数组

兼容数组的写法如下:

function isObject(obj) {
	return typeof obj === 'object' && obj != null;
}

function cloneDeep2(source) {
    if (!isObject(source)) return source; // 非对象返回自身
      
    var target = Array.isArray(source) ? [] : {};
    for(var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            if (isObject(source[key])) {
                target[key] = cloneDeep2(source[key]); // 注意这里
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}

循环引用

当对象存在循环引用的情况,即对象的属性间接或直接的引用了自身的情况,递归会进入死循环而导致栈内存溢出。

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这个存储空间,需要可以存储 key-value 形式的数据,且 key 可以是一个引用类型,我们可以选择 Map 这种数据结构:

  • 检查 map 中有无克隆过的对象
  • 有 --> 直接返回
  • 没有 --> 将当前对象作为 key,克隆对象作为 value 进行存储
  • 继续克隆
function clone(target, map = new Map()) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);
        for (const key in target) {
            cloneTarget[key] = clone(target[key], map);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

接下来,可以使用,WeakMap 替代 Map来使代码达到画龙点睛的作用。

function clone(target, map = new WeakMap()) {
    // ...
};

原因是:如果我们要拷贝的对象非常庞大时,使用 Map 会对内存造成非常大的额外消耗,而且我们需要手动清除 Map 的属性才能释放这块内存,而 WeakMap 会帮我们巧妙化解这个问题。

拷贝 Symbol

破解递归爆栈

上面四步使用的都是递归方法,但是有一个问题在于会爆栈,错误提示如下。

// RangeError: Maximum call stack size exceeded

那应该如何解决呢?其实我们使用循环就可以了,代码如下:

function cloneDeep5(x) {
    const root = {};

    // 栈
    const loopList = [
        {
            parent: root,
            key: undefined,
            data: x,
        }
    ];

    while(loopList.length) {
        // 广度优先
        const node = loopList.pop();
        const parent = node.parent;
        const key = node.key;
        const data = node.data;

        // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
        let res = parent;
        if (typeof key !== 'undefined') {
            res = parent[key] = {};
        }

        for(let k in data) {
            if (data.hasOwnProperty(k)) {
                if (typeof data[k] === 'object') {
                    // 下一次循环
                    loopList.push({
                        parent: res,
                        key: k,
                        data: data[k],
                    });
                } else {
                    res[k] = data[k];
                }
            }
        }
    }

    return root;
}

实现一个通用的 deepClone 函数

  1. 如果是基本数据类型,直接返回
  2. 如果是 RegExp 或者 Date 类型,返回对应类型
  3. 如果是复杂数据类型,递归。
  4. 考虑循环引用的问题
    function deepClone(obj, hash = new WeakMap()) { //递归拷贝
        if (obj instanceof RegExp) return new RegExp(obj);
        if (obj instanceof Date) return new Date(obj);
        if (obj === null || typeof obj !== 'object') {
            //如果不是复杂数据类型,直接返回
            return obj;
        }
        if (hash.has(obj)) {
            return hash.get(obj);
        }
        /**
         * 如果obj是数组,那么 obj.constructor 是 [Function: Array]
         * 如果obj是对象,那么 obj.constructor 是 [Function: Object]
         */
        let t = new obj.constructor();
        hash.set(obj, t);
        for (let key in obj) {
            //递归
            if (obj.hasOwnProperty(key)) {//是否是自身的属性
                t[key] = deepClone(obj[key], hash);
            }
        }
        return t;
    }

参考资料

【JavaScript】new 原理及实现

  • new 是一个运算符
  • new 的作用,就是执行构造函数,返回一个实例对象

new 的功能:

  • 访问到 Function 构造函数里的函数
  • 访问到 Function.prototype 中的属性

new做了什么:

  1. 创建了一个全新的对象。
  2. 这个对象会被执行[[Prototype]](也就是__proto__)链接。
  3. 生成的新对象会绑定到函数调用的this。
  4. 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。
  5. 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。

实现原理

使用 new 命令时,它后面的构造函数会执行以下操作:

  1. 创建一个空的简单的 JavaScript 对象
  2. 将空对象的原型,指向构造函数的 prototype 属性
  3. 将当前函数内部的 this 绑定到空对象
  4. 执行构造函数,如果没有返回对象,则返回 this,即新创建的对象

步骤 3 中,将新创建的对象作为了当前函数 this 的上下文,这也是为什么通过 new 创建实例时,构造函数内部的 this 指向创建的实例对象的原因。

手写

function objectFactory() {

    var obj = new Object(),//从Object.prototype上克隆一个对象

    Constructor = [].shift.call(arguments);//取得外部传入的构造器

    var F=function(){};
    F.prototype= Constructor.prototype;
    obj=new F();//指向正确的原型

    var result = Constructor.apply(obj, arguments);//借用外部传入的构造器给obj设置属性

    return typeof result === 'object' ? ret : obj;//确保构造器总是返回一个对象
};
  • new Object() 的方式新建了一个对象 obj
  • 取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数
  • 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
  • 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
  • 如果构造函数返回一个对象,那么我们也返回这个对象;否则,就返回默认值

https://mp.weixin.qq.com/s?__biz=MzAxODE2MjM1MA==&mid=2651555993&idx=1&sn=37bb41a7e178083de174bfd51308d369&chksm=80255f58b752d64e3006feb7d14b1e0cf47414f98863dc0ffb46b524f2926ac0dea805210353&mpshare=1&scene=1&srcid=%23rd

campcc/blog#3

https://juejin.im/post/5bde7c926fb9a049f66b8b52

【Webpack】Tree Shaking

Tree Shaking 通常用于描述移除 js 中未使用的代码

  • Tree Shaking 只适用于ES Module语法(既通过export导出,import引入),因为它依赖于ES Module的静态结构特性。

  • Tree Shaking 只在生产环境下才会无效,因为在开发环境下由于 source-map 等相关因素的影响,如果不把没有使用的代码一起打包进来的话,source-map 就不是很准确,会影响我们本地开发的效率

【JavaScript】面向对象之封装

把客观事物封装成抽象的类,隐藏属性和方法,仅对外公开接口。

ES6 之前的封装

ES6 的 class 实际是一个语法糖,在 ES6 之前,是没有类这个概念的,因此是借助于原型对象构造函数来实现。

function Person(sex) {
  var heart = "heart" // 私有属性
  var laugh = function() {
    // 私有方法
    console.log("ha...ha...");
  }

  this.sex = sex // 公有属性
  this.heartbeat = function() {
    // 公有方法
    console.log(heart + " beat");
  }
}

Person.prototype.home = "earth" //公有属性
Person.prototype.jump = function() {
  // 公有方法
  this.heartbeat();
  console.log("jump");
}

Person.core = "brain" //静态属性
Person.talk = function() {
  // 静态方法
  console.log("bla...bla...");
}

var man = new Person("male");

私有属性、公有属性、静态属性概念:

  • 私有属性和方法:只能在构造函数内访问不能被外部所访问(在构造函数内使用 var 声明的属性)
  • 公有属性和方法(或实例方法):对象外可以访问到对象内的属性和方法(在构造函数内使用 this 设置,或者设置在构造函数原型对象上比如 Person.prototype.xxx)
  • 静态属性和方法:定义在构造函数上的方法(比如 Person.xxx),不需要实例就可以调用(例如 Object.assign())

实例对象上的属性和构造函数原型上的属性:

  • 定义在构造函数原型对象上的属性和方法虽然不能直接表现在实例对象上,但是实例对象却可以访问或者调用它们。
  • 当访问一个对象的属性 / 方法时,它不仅仅在该对象上查找,还会查找该对象的原型,以及该对象的原型的原型,一层一层向上查找,直到找到一个名字匹配的属性 / 方法或到达原型链的末尾(null)。

遍历实例对象属性的三种方法:

  • 使用 for...in... 能获取到实例对象自身的属性和原型链上的属性使用
  • Object.keys()Object.getOwnPropertyNames() 只能获取实例对象自身的属性可以通过 .hasOwnProperty() 方法传入属性名来判断一个属性是不是实例自身的属性

ES6 之后的封装

在 ES6 之后,新增了 class 这个关键字。它可以用来代替构造函数,达到创建“一类实例”的效果。并且类的数据类型就是函数,所以用法上和构造函数很像,直接用 new 命令来配合它创建一个实例。

类的所有方法都定义在类的prototype属性上面:

class Cat {    
	constructor() {}    
	toString () {}    
	toValue () {}
}

// 等同于
function Cat () {}
Cat.prototype = {    
	constructor() {}    
	toString () {}    
	toValue () {}
}

将上面 ES5 的例子转化成 ES6:

class Person {
  constructor(sex) {
    var heart = "heart"; // 私有属性
	// 严格意义上并不是私有属性,只不过被局限于constructor这个构造函数中,是这个作用域下的变量而已
    var laugh = function() {
      // 私有方法
      console.log("ha...ha...");
    }

    this.sex = sex // 公有属性
    this.heartbeat = function() {
      // 公有方法
      console.log(heart + " beat");
    }
  }
  jump () {
    // 公有方法 等同于 Person.prototype.jump = function() {}
    this.heartbeat();
    console.log("jump");
  }
  home = "earth"; // 公有属性 等同于在constructor中 this.home = 'earth'
  static core = "brain" // 静态属性 等同于 Person.core = 'brain'
  static talk = function() {
    // 静态方法
    console.log("bla...bla...");
  }
}

var man = new Person("male");

class的基本概念:

  • 当你使用class的时候,它会默认调用constructor这个函数,来接收一些参数,并构造出一个新的实例对象(this)并将它返回。
  • 如果你的class没有定义constructor,也会隐式生成一个constructor方法

class中几种定义属性的区别:

  1. constructorvar一个变量,它只存在于constructor这个构造函数中
  2. constructor中使用this定义的属性和方法会被定义到实例上
  3. class中使用=来定义一个属性和方法,效果与第二点相同,会被定义到实例上
  4. class中直接定义一个方法,会被添加到原型对象prototype
  5. class中使用了static修饰符定义的属性和方法被认为是静态的,被添加到类本身,不会添加到实例上

other:

  • class本质虽然是个函数,但是并不会像函数一样提升至作用域最顶层
  • 如遇class中箭头函数等题目请参照构造函数来处理
  • 使用class生成的实例对象,也会有沿着原型链查找的功能
  • class 类内部所有定义的方法不可枚举
  • class 类不能重写 prototype 属性(writable 默认为 false)
  • class 类默认使用严格模式

参考资料

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.