一. JS基础

1. 数据类型

总共8种数据类型,7种基础的数据类型StringNumberBooleanNullUndefinedSymbolBigInt,一个对象类型Object

  • Symbol可以定义对象的唯一属性名
  • BigInt可以表示很大的数

1) typeof判断数据类型

typeof可以判断基本的数据类型,可以判断function,无法判断NullObjectArray,都为object类型

  1. typeof 'a' // string
  2. typeof 1 // number
  3. typeof true // boolean
  4. typeof undefined // undefined
  5. typeof Symbol() // symbol
  6. typeof 12n // bigint
  7. typeof function(){} // function
  8. /** 无法判断 */
  9. typeof null // object
  10. typeof {} // object
  11. typeof [] // object

2) instanceof判断对象类型

instanceof可以判断对象类型,原理是判断对象的原型链是否有该类型的原型

  1. class People {}
  2. class Jay extends People {}
  3. const jayChou = new Jay();
  4. /** 因为实例jayChou顺着原型链可以找到Jay.prototype和People.prototype */
  5. jayChou instanceof People // true
  6. jayChou instanceof Jay // true

3) Object.prototype.toString.call()判断所有的原始类型,包含内置类型MathDateArrayFunctionError
  1. /** 原始数据类型 */
  2. Object.prototype.toString.call('a') // [object String]
  3. Object.prototype.toString.call(1) // [object Number]
  4. Object.prototype.toString.call(true) // [object Boolean]
  5. Object.prototype.toString.call(null) // [object Null
  6. Object.prototype.toString.call(undefined) // [object Undefined]
  7. Object.prototype.toString.call(Symbol()) // [object Symbol]
  8. Object.prototype.toString.call(BigInt) // [object BigInt]
  9. Object.prototype.toString.call({}) // [object Object]
  10. /** 内置类型 */
  11. Object.prototype.toString.call([]) // [object Array]
  12. Object.prototype.toString.call(function(){}) // [object Function]
  13. Object.prototype.toString.call(new Date()) // [object Date]
  14. Object.prototype.toString.call(Math) // [object Math]
  15. Object.prototype.toString.call(new Error()) // [object Error]

4) 判断Array类型的方法

4种

  1. Array.isArray([]) // true
  2. [].__proto__ === Array.prototype // true
  3. [] instanceof Array // true
  4. Object.prototype.toString.call([]) // [object Array]

5) 深拷贝的实现
  1. 递归遍历对象的属性
  2. 考虑对象,数组和基本类型的克隆方式不同,基本类型直接返回,对象和数组递归克隆;
  3. 使用while替换for…in优化速度
  4. 考虑循环引用,使用WeakMap判断当前对象是否已经被克隆,如果被克隆过直接返回
  5. 考虑对象保留原型,对于可遍历对象使用Object.create(target.constructor.prototype)创建对象
  6. 考虑引用类型中的可遍历类型Object,Array,Map,Set和不可遍历的类型的String,Number,Boolean,Date,Symbol,Regexp等内置对象的克隆
  7. 考虑创建引用类型的方式,从对象获取构造函数或**Object.create**
  8. Symbol,Regexp的特殊克隆实现
  9. 考虑(个锤子)克隆函数function,lodash不做特殊处理

实现:

  1. // 第二天重新实现
  2. const objectType = '[object Object]';
  3. const arrayType = '[object Array]';
  4. const mapType = '[object Map]';
  5. const setType = '[object Set]';
  6. const argumentsType = '[object Arguments]';
  7. const iterableType = [objectType, arrayType, mapType, setType, argumentsType];
  8. const stringType = '[object String]';
  9. const numberType = '[object Number]';
  10. const booleanType = '[object Boolean]';
  11. const errorType = '[object Error]';
  12. const dateType = '[object Date]';
  13. const symbolType = '[object Symbol]';
  14. const regexpType = '[object Regexp]';
  15. function deepclone(target, map = new WeakMap()) {
  16. /** 基本类型 */
  17. if (!isObject(target)) {
  18. return target;
  19. }
  20. /** 循环引用 */
  21. if (map.get(target)) {
  22. return map.get(target);
  23. }
  24. /** 可遍历对象的原型继承 */
  25. let clone = {};
  26. const type = Object.prototype.toString.call(target);
  27. if (iterableType.includes(type)) {
  28. clone = Object.create(target.constructor.prototype);
  29. } else {
  30. /** 不可遍历对象的复制 */
  31. return cloneOther(target)
  32. }
  33. map.set(target, clone);
  34. /** 处理map */
  35. if (type === mapType) {
  36. target.forEach((value, key) => {
  37. clone.set(key, deepclone(value));
  38. });
  39. return clone;
  40. }
  41. /** 处理set */
  42. if (type === setType) {
  43. target.forEach((value) => {
  44. clone.add(clone(value));
  45. });
  46. return clone;
  47. }
  48. /** 普通对象,数组 */
  49. for (let key in target) {
  50. clone[key] = deepclone(target[key]);
  51. }
  52. return clone;
  53. }
  54. function isObject(target) {
  55. const type = typeof target;
  56. return (type === 'object' || type === 'function') && (target !== null);
  57. }
  58. function cloneOther(target) {
  59. switch(Object.prototype.toString.call(target)) {
  60. case booleanType:
  61. case stringType:
  62. case numberType:
  63. case dateType:
  64. case errorType:
  65. return new target.constructor(target);
  66. case symbolType:
  67. return Object(Symbol.prototype.valueOf.call(target));
  68. case regexpType:
  69. const reFlags = /\w*$/;
  70. const res = new target.constructor(target.source, reFlags.exec(target));
  71. res.lastIndex = target.lastIndex;
  72. return res;
  73. default:
  74. return null;
  75. }
  76. }

参考:如何写出一个惊艳面试官的深拷贝?
如何 clone 一个正则?

6) IEEE 754 Number数的表示、

IEEE 754规定浮点数由符号位(1),阶码exponent(11位),尾数mantissa(53位)组成;
符号位:没啥好说的
阶码:采用移码,阶码的真实数字需要手动-1023,阶码的范围-1023~1024
尾数:由于整数部分总是1,所以省去;尾数表示小数部分
二. 前端整理 JavaScript - 图1
问题:如何表示0?
答:因为1 * 2-1023 ,所以可以忽略不计

问题:**Number.MAX_SAFE_INTEGER**是如何得来的?
答:Math.pow(2, 53) - 1,长度16位,9007199254740991

问题:为什么0.1+0.2 != 0.3
答:因为进制转换对阶运算会丢失数字精度,当差值小于Number.EPSILON时可以认为他们相等
Number.EPSILON表示1与Number可表示大于1的最小浮点数之间的差值

2. 原型和原型链

1) 原型:

每个js对象在创建的时候(null除外)都会与之关联一个对象,这个对象就是prototype对象,即原型;每个对象都能从原型中继承一些属性。

2) 原型链:

相关联的原型组成的链状结构称之为原型链
二. 前端整理 JavaScript - 图2

3) 其他一些结论性概念:
  1. js对象分为普通对象函数对象,两种对象都有__proto__属性
    1. 普通对象:有__proto__属性
    2. 函数对象:有__proto__属性,独有prototype属性
  2. ObjectFunction是内置的函数,Array,Date这些也是内置函数
  3. __proto__是一个对象,有__proto__constructor属性
  4. 原型对象prototypeconstructor属性
  5. 原型对象的constructor属性指向构造函数本身(如:Person.prototype.constructor === Person)
  6. 实例的__proto__和原型对象prototype的指向同一个地方,指向原型(如:person.proto === Person.prototype)
  7. 普通函数是Function的实例,Object函数也是Function的实例

    3. 继承的实现

    js没有类的概念,只能使用原型来实现继承
    1) 原型链继承
    父类的实例是子类的原型,重点是这句SubType.prototype = new SuperType();
    缺点:创建实例时不能给父类型传参,父类所有的引用类型子类共享 ```javascript function Parent() { this.name = ‘jay’; this.work = [‘cooker’, ‘programmer’]; } Parent.prototype.getName = function() { return this.name; }

function child(age) { this.age = age; } child.prototype = new Parent();

const me = new child(26); const you = new child(31); you.work[2] = ‘deliveryman’; console.log(‘me.work :>> ‘, me.work);

  1. <a name="j2HG3"></a>
  2. ##### 2) 借用构造函数
  3. 调用父类的构造函数`Parent.call(this, name)`,将this指向子类。<br />优点:解决了原型继承父类引用对象共享的问题<br />缺点:无法继承父类的原型
  4. ```javascript
  5. function Parent(name) {
  6. this.name = name;
  7. this.work = ['cooker', 'programmer'];
  8. }
  9. Parent.prototype.getName = function() {
  10. return this.name;
  11. }
  12. function Child(name, age) {
  13. Parent.call(this, name);
  14. this.age = age;
  15. }
  16. const me = new Child('jay', 26);
  17. console.log('me.name :>> ', me.name); 、// 可以访问普通属性
  18. console.log('me.getName() :>> ', me.getName()); // 报错 无法继承父类原型

3) 组合继承

组合继承是原型链继承和借用构造函数继承的组合
优点:父类的普通属性,原型都可以继承
缺点:构造函数执行了2次,普通属性出现了2次,如me.nameme.__proto__.name都存在

  1. function Parent(name) {
  2. this.name = name;
  3. this.work = ['cooker', 'programmer'];
  4. }
  5. Parent.prototype.getName = function() {
  6. return this.name;
  7. }
  8. function Child(name, age) {
  9. this.age = age;
  10. Parent.call(this, name);
  11. }
  12. Child.prototype = new Parent();
  13. const me = new Child('jay', 26);
  14. console.log('me.getName() :>> ', me.getName());

4) 原型式继承

const me = Object.create(Parent),把Parent对象作为me的原型
缺点:父类引用类型的值共享

  1. const Parent = {
  2. name: 'jay',
  3. age: 26,
  4. work: ['cooker', 'programmer'],
  5. getName() {
  6. return this.name;
  7. }
  8. }
  9. const me = Object.create(Parent);
  10. const you = Object.create(Parent);
  11. you.work[2] = 'deliveryman';
  12. console.log('me.work :>> ', me.work);

5) 寄生组合式继承

比较完美的方案了,父类的引用对象不会共享

  1. function inherit(subType, superType) {
  2. const prototype = Object.create(superType.prototype);
  3. prototype.constructor = subType;
  4. subType.prototype = prototype;
  5. }
  6. function Parent(name) {
  7. this.name = name;
  8. this.work = ['cooker', 'programmer'];
  9. }
  10. Parent.prototype.getName = function() {
  11. return this.name;
  12. }
  13. function Child(name, age) {
  14. Parent.call(this, name);
  15. this.age = age;
  16. }
  17. inherit(Child, Parent);
  18. const me = new Child('jay', 26);
  19. const you = new Child('wang', 31);
  20. you.work[2] = 'deliveryman';
  21. console.log('me.work :>> ', me.work);

4. 作用域和作用域链

js采用静态作用域(词法作用域),作用域是在创建的时候(解释阶段)确定。所以如果需要确定一个变量取值,需要到创建函数的作用域取值

1) 作用域

作用域规定了如何查找变量,函数;确定了当前执行环境对变量和函数的访问权限;作用域分为全局作用域,函数作用域和块级作用域

2) 作用域链

如果某一变量在当前作用域中没有定义,就会上级作用域层层查找知道全局作用域,这种层级关系就叫做作用域链

3) 全局作用域和函数作用域

var和function只有全局作用域和函数作用域,所以只有函数体会产生新的作用域;js引擎会在解释阶段对var和function进行变量提升,所以允许他们在声明前使用
var的特点:

  1. 只有全局作用域和函数作用域
  2. 只提升声明,不提升赋值
  3. 声明在代码不可达的区域也可以提升 ```javascript if(false) { var a = 1; } console.log(a); // 没有报错,undefined
  1. 4. 相同作用域中可以**重复声明**
  2. **function的特点:**
  3. 1. 只有全局作用域和函数作用域
  4. 1. 既提升声明也提升赋值
  5. 1. 函数声明的提升**在变量提升之前**
  6. 1. 声明在不可达的区域不能提升
  7. ```javascript
  8. if (false) {
  9. function fn() {};
  10. }
  11. fn(); // 报错,fn未定义

4) 块级作用域

ES6增加了块级作用域,let、const变量可以声明块级作用域;块级作用域才是正常的作用域

  1. 不允许声明前使用
  2. 禁止重复声明

    5. 执行上下文和执行上下文栈

    1) 执行上下文

    概念:执行上下文是评估和执行JavaScript代码的环境的抽象概念,每当JavaScript代码执行的时候,它都在执行上下文中运行。
    类型:

  3. 全局执行上下文:在函数外面的代码都在全局执行上下文中,一个程序只有一个全局执行上下文,在程序运行时被压入执行上下文栈的栈底。全局执行上下文做了两件事情 :

    1. 创建全局对象(浏览器window,node环境global)
    2. this指向全局对象
  4. 函数执行上下文:函数被调用的时候会创建函数执行上下文,并把该上下文压入栈中,调用结束后上下文从栈中弹出
  5. eval执行上下文:忽略先

执行上下文中的三个重要的属性:

  1. 变量对象(variable object,vo)
  2. 作用域链(scope chain)
  3. this
    2) 执行上下文栈
    执行栈用于存储在代码执行期间创建的所有上下文,程序开始运行时会创建全局上下文压入栈中;在遇到函数调用的时候会创建函数执行上下文并压入栈中,函数执行完成会将该上下文弹出;控制流程始终保持在执行上下文栈的栈顶
    3) 变量对象(vo)
    每一个执行上下文都有一个与之关联的变量对象,这个对象存储了上下文中定义的变量和函数。
    函数调用时会立即创建一个活动对象(AO),并将这个活动对象作为变量对象;以下时活动对象的创建过程: ```javascript function foo(a, b) { var c = 10; function d() {
    1. console.log('d');
    } var e = function () {
    1. console.log('e');
    }; (function f() {}) if (true) {
    1. var g = 20;
    } else {
    1. var h = 30;
    } }

foo(10);

  1. 1. 初始化活动对象
  2. 活动对象会以`arguments`为属性初始化,`arguments`的值为arguments对象
  3. ```javascript
  4. AO = {
  5. arguments: <Args>
  6. }

arguments对象是一个类数组对象,包含实参的值,对象有如下属性:

  • length:实参的个数
  • callee:指向函数本身
  • 下标index:存储了传入实参的值
    1. 进入执行环境

进入执行环境后,会扫描所有的变量声明和函数声明,在活动对象中添加3类属性:

  • 形参和实参:实参的值添加进arguments属性中,形参作为活动对象的属性被添加,如果传入了实参,则值为实参的值,如果没有传入实参,则为undefined;
  • 函数声明:函数声明的名称作为属性名添加到对象中,值指向函数对象的引用;如果变量对象中已经有该属性则替换值
  • 变量声明:变量声明的的名称作为属性名添加到对象中,值为undefined;如果变量对象中已经有该属性则跳过不添加
    1. AO = {
    2. arguments: {
    3. callee: show,
    4. length: 1,
    5. 0: 10,
    6. },
    7. a: 10,
    8. b: undefined,
    9. c: undefined,
    10. d: <d reference>,
    11. e: undefined,
    12. g: undefined,
    13. h: undefined
    14. }
  1. 执行代码阶段

执行代码阶段所有的属性都会被赋值,活动对象包含arguments对象 + 形参 + 函数声明 + 局部变量(不包含表达式)

  1. AO = {
  2. arguments: {
  3. callee: show,
  4. length: 1,
  5. 0: 10,
  6. },
  7. a: 10,
  8. b: undefined,
  9. c: 10,
  10. d: <d reference>,
  11. e: <function reference>,
  12. g: 20,
  13. h: undefined
  14. }

6) 作用域链

当查找一个变量的时候,会从当前的变量对象查找,如果查找不到就从(静态作用域的,编译阶段已经确认)父级执行上下文的变量对象查找直至全局变量对象,这样由多个执行上下文的变量对象组成的链表就叫作用域链。
作用域链形成的过程如下:

  1. function foo() {
  2. function bar() {
  3. ...
  4. }
  5. }
  1. 创建(编译)阶段,就已经确定每个函数的作用域,保存在各自的[[scope]]属性中 ```javascript foo.[[scope]] = [ globalContext.VO ]

bar.[[scope]] = [ fooContext.AO, globalContext.VO ]

  1. 2. 执行阶段,foo的执行上下文作用域链属性`Scope`复制一份foo函数的`[[scope]]` ,并添加自己的AO到作用域链顶端。查找变量的时候按照作用域链的顺序逐层向外查找
  2. ```javascript
  3. // 执行到foo
  4. fooContext = {
  5. AO: ...,
  6. Scope: [fooContext.AO, foo.[[scope]]]
  7. }
  8. // 执行到bar
  9. barContext = {
  10. AO: ...,
  11. Scope: [barContext.AO, bar.[[scope]]]
  12. }

参考:理解 Javascript 执行上下文和执行栈

7) this指向

属性:this是执行上下文中的一个属性,在函数被调用进去执行上下文时确定,在上下文运行代码期间不会改变
指向:this由激活上下文代码的调用者提供,即this指向函数的调用上下文(调用这个函数的父上下文)
在全局环境中,严格模式下this为undefined,非严格模式下为全局环境对象
this的指向有如下4个结论:

  1. 当作为对象被调用时,this指向对象obj.b() // this指向obj
  2. 当作为函数被调用时,this指向全局环境vat b = obj.b; b() // this指向window、global
  3. 当使用new调用时,this指向当前对象的实例var b = new obj.b() // this指向b,new调用了内部的[[call]]
  4. 当使用call,bind调用时,this指向绑定的对象c = {}; obj.b.call(c) // this指向c
    1. var obj = {
    2. a: 1,
    3. b: function(){
    4. console.log(this);
    5. }
    6. }
    箭头函数的this:箭头函数没有this,所以箭头函数中的this直接在作用域链中查找,直到全局作用域
    参考:深入解析this
    JavaScript的this原理—阮一峰

    6. 闭包

    定义:函数和它周围环境(Lexical Enviroment词法环境)的引用绑定在一起,这样的组合叫做闭包;简而言之,闭包指那些能访问自由变量的函数。
    自由变量:函数中既不是形参,又不是局部变量的对象
    作用:闭包可以让内层的函数访问到外层函数的作用域
    理论上:所有的js函数都是闭包的,因为函数(创建的时候就)包含了上层的上下文数据(应该就是作用域)
    实际上:闭包函数是指那些:
  • 即使创建函数的上下文已经被销毁,函数依然存在(函数作为变量被返回)
  • 代码中包含自由变量的函数

用途

  1. 缓存数据(使函数有状态) ```javascript function createCache() { const data = {}; return {
    1. set(key, value) {
    2. data[key] = value;
    3. },
    4. get(key) {
    5. return data[key];
    6. }
    }; }

const c = createCache(); c.set(‘a’, 12); console.log(c.get('a') :>>, c.get(‘a’));

  1. 2. 提供私有变量的公共访问方法
  2. ```javascript
  3. function Hello(name) {
  4. this.getName = function () {
  5. return name;
  6. }
  7. }
  8. const h = new Hello('Jay');
  9. console.log('h.getName() :>> ', h.getName());
  1. 闭包和IIFE没啥关系,因为IIFE并没有访问外部的变量

    1. let arr = [];
    2. for (var i = 0; i < 5; i++) {
    3. arr[i] = (function (k) {
    4. return function () {
    5. return k;
    6. }
    7. })(i);
    8. }
    9. console.log('arr[0]() :>> ', arr[0]());

    但是闭包可以配个IIFE做一些封装,防止污染,保证内部内部变量的安全

    1. const Person = (function () {
    2. let name = "Jay";
    3. return {
    4. getName() {
    5. return name;
    6. }
    7. }
    8. })();
    9. console.log('Person.getName() :>> ', Person.getName());
    10. console.log('Person.name :>> ', Person.name);

    缺点:导致内存消耗增加,因为闭包导致已经执行完成的执行上下文没有被销毁

    7. call,apply,bind实现

    1) call的实现

    实现call有两个目标:

  2. 改变this指向

  3. 执行函数

    1. /**
    2. * 在Funtcion.prototype上添加myCall
    3. * 假设需要将this指向obj,就将函数变成obj的一个属性fn,myCall中this就是我们要执行的函数
    4. * 再执行obj.fn
    5. * 最后删除这个属性即可
    6. */
    7. Function.prototype.myCall = function (context) {
    8. // this即要执行的函数
    9. if (typeof this !== 'function') {
    10. throw new Error('Type Error');
    11. }
    12. let res;
    13. const args = [...arguments].slice(1);
    14. context = context || window;
    15. const fn = Symbol('fn');
    16. context[fn] = this;
    17. res = context[fn](...args);
    18. delete context[fn];
    19. return res;
    20. }

    2) apply的实现

    在call实现的基础上,apply支持类数组对象,需要使用Apply.from转换

    1. Function.prototype.myApply = function (context) {
    2. // this即要执行的函数
    3. if (typeof this !== 'function') {
    4. throw new Error('Type Error');
    5. }
    6. let res;
    7. let args = [...arguments].slice(1)[0];
    8. if (!args) {
    9. args = [];
    10. }
    11. args = Array.from(args);
    12. context = context || window;
    13. const fn = Symbol('fn');
    14. context[fn] = this;
    15. res = context[fn](...args);
    16. delete context[fn];
    17. return res;
    18. }

    3) bind的实现
    bind返回一个函数,显示的绑定this为obj,它有如下特性

  4. 永久绑定,原始函数fn绑定完的函数bindFn指向永远指向bind的对象obj

  5. 绑定时支持传递参数,也就是说原始函数被柯里化了
  6. 当作普通函数调用的时候,this指向绑定的对象
  7. 当作构造函数调用的时候,this指向创建出的实例

实现需要注意的点:

  1. 首先判断this是不是函数类型
  2. 可以使用new.targetthis instanceof bindFn判断是否是构造函数调用
  3. 可以使用Array.from将类数组对象转换成数组
  4. 无论是构造函数调用还是普通函数调用,绑定函数bindFn的原型都继承自原始函数的原型fn
  5. 为了防止修改绑定函数bindFn的原型影响原始函数的原型fn,可以使用一个中继函数tempFn

    1. Function.prototype.myBind = function (context) {
    2. if (typeof this !== 'function') {
    3. throw new Error('Type Error');
    4. }
    5. const fn = this;
    6. // 支持柯里化
    7. const args = Array.prototype.slice.call(arguments, 1);
    8. const bindFn = function () {
    9. const bindObj = new.target ? this : context;
    10. return fn.apply(bindObj, args.concat(Array.from(arguments)));
    11. }
    12. // 中间函数
    13. const tempFn = function () { };
    14. tempFn.prototype = fn.prototype;
    15. bindFn.prototype = new tempFn();
    16. return bindFn;
    17. }

    参考:js 手动实现bind方法,超详细思路分析!
    JavaScript深入之bind的模拟实现

    8. new的实现

    new的实现思路也是我们使用new语法的时候引擎帮我们做了什么

  6. 创建一个空对象obj

  7. 对象obj的原型对象指向构造函数的原型
  8. 调用构造函数并把this指向对象obj
  9. 判断函数的返回值,如果是对象则返回这个对象,否则返回创建的对象obj

    1. function myNew(fn) {
    2. const newObj = Object.create(fn.prototype);
    3. const res = fn.apply(newObj, [...arguments].slice(1));
    4. return res instanceof Object ? res : newObj;
    5. }

    9. 异步

    1) 从事件循环的角度看异步代码如何运行:
  10. 在调用栈中:首先js引擎在执行上下文栈中Stack运行同步代码,遇到浏览器提供的相关API(WebAPI)就交给浏览器创建相应的进程运行(如计时器进程,网络请求进程),同时交给浏览器的还有回调函数callback。执行栈运行同步代码直到栈空,等在回调队列的回调函数压入栈中执行

  11. 在WebAPI中,浏览器创建相应的进程运行如计时器进程,网络请求进程;一旦这些任务执行完成,就将相应的回调函数放入回调队列中,等待放入调用栈中执行。

对于scroll,resize等这些事件;他们的回调会放在另一个任务队列中,优先级稍微高于普通的任务 列, 但又不是总是优先执行。这里我们可以得到下面的结论:

  • 任务队列可能有一个或多个
    • 鼠标,键盘事件
    • 其他的任务
  1. 在调用栈为空时会执行微任务,而后优先判断是否需要渲染,渲染时机是由屏幕刷新率,页面性能等决定;一般每16.6ms重绘一次页面,保证页面60帧/s。当然也有可能两个宏任务之间跳过渲染,跳过渲染的条件:
    • 浏览器判断更新渲染不会带来视觉上的变化
    • 帧动画回调为空,也就是没有requestAnimationFrame的回调(map of animation frame callback为空)

二. 前端整理 JavaScript - 图3
总结而言就是:

  1. 清空调用栈,即同步代码执行完毕
  2. 执行微任务
  3. 尝试渲染DOM:都是DOM重新渲染的机会
  4. 触发事件循环:从回调队列执行下一个回调函数,触发下一轮事件循环

2) 宏任务和微任务分类

宏任务:setTimeout,setInterval,DOM事件
微任务:Promise,async/await,MutationObserver

问题:为什么微任务在宏任务之前执行?
答:

  1. 微任务是由ES语法规定,微任务被压入微任务队列
  2. 宏任务是由浏览器规定,宏任务被压入宏任务队列
  3. 微任务在事件循环之前进行,宏任务在事件循环后进行

参考:深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调

10. Promise

1) Promise的三种状态:
  • pending:进行中
  • resolved,settled:已完成,一旦状态确定就无法改变
    • fulfilled(resolved):已成功
    • rejected:已失败
      2) Promise.prototype.then
  1. then是状态改变的回调函数,包含可选的resolve,reject两个回调函数promise.then(resolve, rejection);
  2. then返回一个新的Promise实例,所以可以链式调用

    3) Promise.prototype.catch
  3. catch是promise.then(null, rejection)promise.then(undefined, rejection)的另一种写法。catch可以捕获错误

  4. catch也返回一个新的Promise实例,所以可以链式调用
  5. promise的错误可以一直向后传递知道被捕获promise.then().then().catch()。但是promise的错误并不会影响Promise外部的代码,会被吃掉(程序可以继续运行) ```javascript // 打印hi并报错,因为Error不会中止代码 const p1 = new Promise((resolve, reject) => { resolve(x + 1); });

p1 .then((result) => console.log(carry on)) console.log(‘hi’);

  1. 4. promise的同步函数的Error是可以被rejection捕获到的
  2. ```javascript
  3. // oh, error, ReferenceError: x is not defined
  4. const p1 = new Promise((resolve, reject) => {
  5. resolve(x + 1);
  6. });
  7. p1
  8. .then((result) => console.log(`carry on`))
  9. .catch((error) => console.log(`oh, error, ${error}`));

4) Promise.prototype.finally
  1. finally不接受任何参数,不关心前面的Promise的状态是resolve还是reject都会执行
  2. finally总会返回原来的值,但是是新的Promise对象 ```javascript const p1 = Promise.resolve(2) // 返回值是2,状态是resloved的Promise实例 const p2 = p1.finally(); console.log(p1 === p2) // false

const p3 = Promise.reject(4) // 返回值是4,状态是rejected的Promise实例 const p4 = p1.finally(); console.log(p3 === p4) // false

  1. 3. 相当于一个then的特例
  2. ```javascript
  3. Promise.prototype.myFinally = function (callback) {
  4. const pCtor = this.constructor;
  5. return this.then(
  6. (value) => pCtor.resolve(callback()).then(() => value),
  7. (error) => pCtor.resolve(callback()).then(() => { throw error })
  8. );
  9. }
  10. // 验证
  11. const p2 = p1.myFinally(() => console.log('myFinally'));
  12. console.log('p2 :>> ', p2);
  13. const p3 = p1.finally(() => console.log('finally'));
  14. console.log('p3 :>> ', p3);
  15. // p2 :>> Promise {<pending>}[[Prototype]]: Promise[[PromiseState]]: "fulfilled"[[PromiseResult]]: 1
  16. // p3 :>> Promise {<pending>}[[Prototype]]: Promise[[PromiseState]]: "fulfilled"[[PromiseResult]]: 1
  17. // myFinally
  18. // finally

5) Promise.all()

Promise.all将多个Promise实例包装成一个Promise对象;
接受一个数组(或iterable对象),数组的成员必须是Promise实例

  • 如果所有的Promise对象状态变成fulfilled,Promise.all的状态才变成fulfilledthen的返回值是所有promise返回值组成的数组
  • 如果有一个Promise对象的状态变成reject。Promise.all的状态变成rejectcatch的返回值是第一个变成reject的值
    1. // err :>> 2
    2. const p1 = new Promise((resolve, reject) => {
    3. resolve(1);
    4. });
    5. const p2 = new Promise((resolve, reject) => {
    6. reject(2);
    7. });
    8. const p3 = new Promise((resolve, reject) => {
    9. resolve(3);
    10. });
    11. const p4 = new Promise((resolve, reject) => {
    12. reject(4);
    13. });
    14. Promise.all([p1, p2, p3, p4])
    15. .then(result => console.log('result :>> ', result))
    16. .catch(err => console.log('err :>> ', err));
    6) Promise.race()
    Promise.race将多个Promise实例包装成一个Promise对象,并返回率先改变状态的promise对象,无论是fulfilled还是rejected
    1. // err :>> 2
    2. const p1 = new Promise((resolve, reject) => {
    3. setTimeout(() => resolve(1), 1000);
    4. });
    5. const p2 = new Promise((resolve, reject) => {
    6. setTimeout(() => reject(2), 500);
    7. });
    8. Promise.race([p1, p2])
    9. .then(result => console.log('result :>> ', result))
    10. .catch(err => console.log('err :>> ', err));
    7) Promise.allSettled()
    Promise.allSettled将多个Promise实例包装成一个Promise对象,等到所有的实例改变状态后返回一个数组(无论是resolved还是rejected),数组包含所有改变状态后的对象({status: 'fulfilled', value: 1}),用来确定一组异步操作是否都结束
    1. /*
    2. result :>> (2) [{…}, {…}]
    3. 0: {status: 'fulfilled', value: 1}
    4. 1: {status: 'rejected', reason: 2}
    5. length: 2
    6. [[Prototype]]: Array(0)
    7. */
    8. const p1 = new Promise((resolve, reject) => {
    9. setTimeout(() => resolve(1), 1000);
    10. });
    11. const p2 = new Promise((resolve, reject) => {
    12. setTimeout(() => reject(2), 500);
    13. });
    14. Promise.allSettled([p1, p2])
    15. .then(result => console.log('result :>> ', result))
    8) Promise.any()
    Promise.any的状态有两种可能
  1. 只要有一个Promise实例的状态变为fulfilled,则then返回值是这个实例的值
  2. 如果所有的Promise实例状态都是rejected,则返回catch返回值是AggregateError

    1. /*
    2. res1 :>> 1
    3. res2 :>> AggregateError: All promises were rejected
    4. */
    5. const p0 = Promise.resolve(0);
    6. const p1 = Promise.resolve(1);
    7. const p2 = Promise.reject(2);
    8. const p3 = Promise.reject(3);
    9. const p4 = Promise.reject(4);
    10. Promise.any([p1, p2, p3, p4]).then((res1) => {
    11. console.log('res1 :>> ', res1);
    12. });
    13. Promise.any([p2, p3]).catch((res2) => {
    14. console.log('res2 :>> ', res2);
    15. });

    9) Promise.resolve()

    Promise.resolve将现有对象转换成Promise对象,状态为resolved,现有对象有如下的情况:

  3. 普通对象或空:转换为Promise对象,值为这个普通对象或undefined

    1. // p0 p1同理
    2. const p0 = Promise.resolve(0);
    3. const p1 = new Promise((resolve, reject) => resolve(0));
  4. Promise对象,原封不动的返回

    1. const p0 = Promise.resolve();
    2. const p1 = Promise.resolve(p0);
    3. console.log('p0 === p1 :>> ', p0 === p1); // true
  5. thenable对象(含then方法的对象),抓换为Promise对象并立即执行then

    1. // res :>> 2
    2. const p0 = {
    3. then(resolve) {
    4. resolve(2)
    5. }
    6. };
    7. const p1 = Promise.resolve(p0);
    8. p1.then((res) => console.log('res :>> ', res));

    10) Promise.reject()

    Promise.reject同理,Promise状态为rejected

    11) Promise.try() 浏览器还没实现

    Promise.try参数是一个函数fn,如果fn是同步函数就以同步的方式运行,如果是异步就以异步的方式运行

    11.Promise实现

  6. 在constructor中执行执行器executor

  7. executor传入resolve和reject方法,resolve和reject使用箭头函数
  8. 状态status和返回值value,reason在resolve和reject的改变
  9. then根据不同的状态执行不同的回调函数
  10. 考虑resolve不是立即执行的情况,在then里面状态为pending的时候缓存回调函数,在resolve里面如果有缓存函数则执行
  11. 考虑同一个Promise实例多次调用then;缓存回调函数使用数组
  12. 考虑链式调用,在then中返回新的Promise对象;
  13. 判断返回值类型;如果是promise对象立即调用它的then,使它的状态改变;否则直接调用resolve
  14. 判断返回值是否和then返回的promise对象是否相同;如果相同报错;这里需要使用queueMicrotask把回调函数放入微任务队列执行
  15. 考虑在执行器executor和then中捕获错误;在执行executor,onFulfilled和onReject的时候使用try catch捕获
  16. 考虑使用then的两个回调函数都是可选的,并且是可传递的;如果不存在使用默认函数代替
  17. 考虑实现静态函数Promise.resolvePromise.reject
  18. 考虑thenable对象的情况 ```javascript const PENDING = ‘pending’; const FULFILLED = ‘fulfilled’; const REJECTED = ‘rejected’;

class MyPromise { constructor(executor) { /**

  1. * 执行器,创建Promise对象的时候马上执行
  2. * 执行的时候传入resolvereject函数
  3. * reject可以捕获执行器中的错误
  4. */
  5. try {
  6. executor(this.resolve, this.reject);
  7. } catch (e) {
  8. this.reject(e);
  9. }
  10. }
  11. value = null;
  12. reason = null;
  13. status = PENDING;
  14. /** 为了支持多次调用,暂存的变量改成数组 */
  15. onFulfilled = [];
  16. onRejected = [];
  17. /**
  18. * 使用箭头函数是因为如果是普通函数,resolve、reject执行的时候,this指向全局
  19. * 箭头函数保证了this指向promise对象的实例
  20. * 只有在pending的状态下才能改变状态
  21. */
  22. resolve = (value) => {
  23. if (this.status === PENDING) {
  24. this.status = FULFILLED;
  25. this.value = value;
  26. // 如果有成功的回调就执行
  27. while (this.onFulfilled.length) {
  28. this.onFulfilled.shift()(value);
  29. }
  30. }
  31. };
  32. reject = (reason) => {
  33. if (this.status === PENDING) {
  34. this.status = REJECTED;
  35. this.reason = reason;
  36. // 如果有失败的回调就执行
  37. while (this.onRejected.length) {
  38. this.onRejected.shift()(reason);
  39. }
  40. }
  41. };
  42. /** 支持resolve,reject静态调用 */
  43. static resolve(parameter) {
  44. if (parameter instanceof MyPromise) {
  45. return parameter;
  46. }
  47. return new MyPromise((resolve) => {
  48. resolve(parameter)
  49. });
  50. }
  51. static reject(parameter) {
  52. if (parameter instanceof MyPromise) {
  53. return parameter;
  54. }
  55. return new MyPromise((resolve, reject) => {
  56. reject(parameter)
  57. });
  58. }
  59. then(onFulfilled, onRejected) {
  60. /** 改造两个回调函数为可选参数 */
  61. onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (value) => value;
  62. onRejected = typeof onRejected === 'function' ? onRejected : (reason) => { throw reason };
  63. /** 为了支持链式调用,返回一个新的Promise对象 */
  64. const promise = new MyPromise((resolve, reject) => {
  65. const fulfillMicroTask = () => {
  66. /** 创建一个微任务等待返回的promise创建完成后判断是否和返回值x相同 */
  67. queueMicrotask(() => {
  68. /** 支持在then中捕获错误 */
  69. try {
  70. /** 获取成功回调的返回值 */
  71. const x = onFulfilled(this.value);
  72. /** 针对x不同的类型集中处理resolve */
  73. this.resolvePromise(promise, x, resolve, reject);
  74. } catch (e) {
  75. reject(e);
  76. }
  77. })
  78. }
  79. const rejectedMicroTask = () => {
  80. /**
  81. * 改造成和fulfill相同的结构
  82. * 1. 增加异步状态下的链式调用
  83. * 2. 增加返回值类型的判断
  84. * 3. 增加Promise返回自己的错误处理
  85. * 4. 增加错误捕获
  86. */
  87. queueMicrotask(() => {
  88. try {
  89. const x = onRejected(this.reason);
  90. this.resolvePromise(promise, x, resolve, reject);
  91. } catch (e) {
  92. reject(e);
  93. }
  94. });
  95. }
  96. if (this.status === FULFILLED) {
  97. fulfillMicroTask();
  98. } else if (this.status === REJECTED) {
  99. rejectedMicroTask();
  100. } else if (this.status === PENDING) {
  101. /**
  102. * 成功和失败的回调,不一定里面执行。需要先暂存起来
  103. * 完成和上面相同的改造
  104. */
  105. this.onFulfilled.push(fulfillMicroTask);
  106. this.onRejected.push(rejectedMicroTask);
  107. }
  108. });
  109. return promise;
  110. }
  111. resolvePromise(promise, x, resolve, reject) {
  112. if (x === promise) {
  113. return reject(new TypeError('Chaining cycle detected for promise #<Promise>'));
  114. }
  115. if (x instanceof MyPromise) {
  116. /** 如果返回值是Promise对象,立即执行then,使Promise改变状态为fulfilled或rejected */
  117. x.then(resolve, reject);
  118. } else {
  119. /** 否则直接将状态变为fulfilled状态 */
  120. resolve(x);
  121. }
  122. }

}

  1. 参考:[从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节](https://juejin.cn/post/6945319439772434469#heading-28)
  2. <a name="IrxQt"></a>
  3. #### 12. Promise.all实现
  4. ```javascript
  5. Promise.myAll = function (promises) {
  6. return new Promise((resolve, reject) => {
  7. if (typeof promises[Symbol.iterator] !== 'function') {
  8. reject('Type Error');
  9. } else if (promises.length === 0) {
  10. resolve([]);
  11. } else {
  12. const res = [];
  13. let count = 0;
  14. for (let i = 0; i < promises.length; i++) {
  15. Promise.resolve(promises[i]).then((value) => {
  16. res.push(value);
  17. count++;
  18. if (count === promises.length) {
  19. resolve(res);
  20. }
  21. }).catch((reason) => {
  22. reject(reason);
  23. });
  24. }
  25. }
  26. });
  27. }

13. async/await 和 Promise的特点和区别

  1. async/await只能在函数中使用,返回Promise对象
  2. Promise.all所有异步操作同步执行;async/await异步操作顺序执行
  3. async/await使用try catch捕获异常,Promise使用catch捕获异常
  4. async/await有些情况下替代Promise解决回调地狱

    14. 垃圾回收

    标记清除:从root节点开始递归的清除所有的子对象,如果对象可达则将其标记。垃圾回收器会回收没有被标记的对象的内存
    引用计数:如果对象被引用,则计数+1;如果该引用的变量被覆盖,则计数-1。如果计数为0则说明对象不可达,对象内存会被立即回收
    参考:「硬核JS」你真的了解垃圾回收机制吗

    15. EventEmit实现

    事件机制是订阅发布模式的实现;使用hashMap存储事件名和对应的回调函数;key是事件名,value是回调函数数组

  5. emit(type, ...args)触发事件回调

  6. on(type, fn)注册事件
  7. once(type, fn)单次注册事件;可以包装一个函数,触发时off
  8. off(type, fn)取消注册事件
  9. removeAllListeners取消注册所有事件

    1. class MyEventEmitter {
    2. constructor() {
    3. this.events = {};
    4. }
    5. emit(type, ...args) {
    6. if (type in this.events) {
    7. const fns = this.events[type];
    8. fns.forEach((fn) => {
    9. fn.apply(this, args);
    10. })
    11. }
    12. return this;
    13. }
    14. on(type, fn) {
    15. if (!(type in this.events)) {
    16. this.events[type] = [];
    17. }
    18. this.events[type].push(fn);
    19. return this;
    20. }
    21. once(type, fn) {
    22. const execFn = (...args) => {
    23. fn.apply(this, args);
    24. this.off(type, execFn);
    25. }
    26. this.on(type, execFn);
    27. return this;
    28. }
    29. off(type, fn) {
    30. if (type in this.events) {
    31. const i = this.events[type].indexOf(fn);
    32. if (i >= 0) {
    33. this.events[type].splice(i, 1);
    34. }
    35. }
    36. return this;
    37. }
    38. removeAllListeners(type) {
    39. if (type in this.events) {
    40. this.events[type] = null;
    41. }
    42. return this;
    43. }
    44. }

    16.ArrayBuffer,TypedArray

    ArrayBuffer:二进制对象,是对固定长度连续内存空间的引用。单位是字节
    TypedArray:是ArrayBuffer类型化的视图,有Uint8Array, Int8Array, Float32Array, Float64Array
    DataViwe:是未类型化的视图,可以在创建后使用方法类型化.getUi8nt(i)
    TextDecoder, TextEncoder:二进制数据和字符串之间编解码
    Blob:具有类型的二进制数组,blob = type + blobParts; blobParts可以是blob, BufferSource, String类型的数组
    使用blob.arrayBuffer转化为数组
    File:继承自blob

    17. DOM,BOM

    1) addEventListenerf的第三个参数

    第三个参数是options可选参数,包含以下值:

  10. capture:是否切换为捕获模式

  11. once:是否触发一次后立马移除
  12. passive:是否永远忽略回调函数中的preventDefault方法
  13. signal:当传入的对象的abort方法被调用的时候移除监听

    18. Object的所有方法

  14. Object.assign(target, source),将source中的可枚举属性复制覆盖到target中,不包含原型上的值。并返回target ```javascript function Parent() { this.pa = 1; }; Parent.prototype.pb = 2; const source = new Parent();

const target = { a: 1, b: 2 }; Object.assign(target, source); console.log(‘target :>> ‘, target); // 原型上没有pb

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/23168078/1649984093625-6c85ff30-cd80-4e52-91e2-b830e5b12dd5.png#clientId=u67a87f5e-129b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=143&id=ue996158f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=143&originWidth=280&originalType=binary&ratio=1&rotation=0&showTitle=false&size=5157&status=done&style=none&taskId=u21b980ad-e719-496a-810a-e02ad3e53c0&title=&width=280)
  2. 2. `Object.create(obj, propertyOptions)`,返回一个对象,以obj为原型,可以在propertyOptions中像`defineProperty`一样添加属性
  3. ```javascript
  4. const Parent = {
  5. a: 1,
  6. b: 2,
  7. c() {
  8. return this.a;
  9. }
  10. };
  11. const p = Object.create(Parent, {
  12. d: {
  13. value: 4, // 默认undefined
  14. writable: true, // 默认 false
  15. configurable: true, // 默认false
  16. enumerable: true // 默认false
  17. // get 默认undefined
  18. // set 默认undefined
  19. }
  20. });
  21. console.log(p.__proto__ === Parent); // true
  22. console.log(p.d); // 4
  1. Object.defineProperty(obj, prop, options), Object.defineProperties(obj, propretyOptions)前者针对单一属性, 后者批量更改属性 ```javascript const Parent = { a: 1, b: 2 }; const p = Object.defineProperties(Parent, { a: { value: 0 }, d: { value: 4, enumerable: true } }); console.log(Parent);

const p2 = Object.defineProperty(Parent, ‘c’, { value: 3, enumerable: true }) console.log(Parent);

  1. 3. `Object.entries`返回**自身对象**键值对的**二维数组**。
  2. ```javascript
  3. const parent = {
  4. a: 1
  5. }
  6. const child = Object.create(parent);
  7. child.b = 2;
  8. console.log(Object.entries(child)); // [ [ 'b', 2 ] ]
  1. Object.freeze冻结一个对象并返回它,不能增加,修改,删除对象自身,原型上的属性。也不能修改它的访问器属性。严格模式下会抛出错误 ```javascript ‘use strict’ const parent = { a: 1 }

const child = Object.create(parent); child.b = 2; Object.freeze(child); child.a = 2 // throw error

console.log(‘child.a :>> ‘, child.a);

  1. 5. `Object.fromEntries`将类Entreis的二维数组转化为对象,`entries`的逆向工程
  2. ```javascript
  3. const child = Object.fromEntries([
  4. ['a', 1],
  5. ['b', 2],
  6. ]);
  7. console.log('child :>> ', child); // child :>> { a: 1, b: 2 }
  1. Object.getOwnPropertyDescriptor(obj, key)返回对象自身某个属性的描述符

Object.getOwnPropertyDescriptors(obj)返回对象自身所有属性的描述符

  1. const Parent = {
  2. a: 1
  3. }
  4. const child = Object.create(Parent);
  5. child.b = 2
  6. const descriptorA = Object.getOwnPropertyDescriptor(child, 'a');
  7. const descriptorB = Object.getOwnPropertyDescriptor(child, 'b');
  8. console.log(descriptorA); // undefined
  9. console.log(descriptorB); // { value: 2, writable: true, enumerable: true, configurable: true }
  10. console.log(Object.getOwnPropertyDescriptors(child));
  11. //{
  12. // b: { value: 2, writable: true, enumerable: true, configurable: true }
  13. //}
  1. Object.getOwnPropertyNames返回对象自身普通属性的数组,包括不可枚举属性但是不包括Symbol属性 ```javascript const Parent = { a: 1 };

const child = Object.create(Parent, { b: { value: 2, enumerable: false } }); child.c = 3; child[Symbol(‘d’)] = 4;

console.log(Object.getOwnPropertyNames(child)); // [‘b’, ‘c’]

  1. 8. `Object.getOwnPropertySymbols`返回**对象自身Symbol属性**的数组,**包含不可枚举属性**但是不包括普通属性
  2. ```javascript
  3. const Parent = {
  4. [Symbol('a')]: 1
  5. };
  6. const child = Object.create(Parent, {
  7. [Symbol('b')]: {
  8. value: 2,
  9. enumerable: false
  10. }
  11. });
  12. child[Symbol('c')] = 3;
  13. console.log(Object.getOwnPropertySymbols(child)); // [Symbol(b), Symbol(c)]
  1. Object.getPrototypeOf返回对象的原型 ```javascript const Parent = { a: 1 }

const child = Object.create(Parent);

console.log(Object.getPrototypeOf(child) === Parent); // true

  1. 10. `Object.prototype.hasOwnProperty`判断是否是**对象自身属性**的key
  2. ```javascript
  3. const Parent = {
  4. a: 1
  5. }
  6. const child = Object.create(Parent);
  7. child.b = 2;
  8. console.log(child.hasOwnProperty('a')); // false
  9. console.log(child.hasOwnProperty('b')); // true
  1. Object.is判断两个值是否相同。其中NaN相同,+0, -0相同
  2. Object.isForzen判断对象是否被冻结

    1. console.log(Object.isFrozen(Object.freeze({}))) // true
  3. Object.prototype.isPrototypeOf判断一个对象是否是另一个对象原型链上的原型

    1. const Parent = {
    2. a: 1
    3. };
    4. const child = Object.create(Parent);
    5. console.log(Parent.isPrototypeOf(child)); // true
    6. console.log(child.isPrototypeOf(child)); // false
  4. Object.seal密封一个对象,可以改已有属性;该对象不可拓展新的属性,不可删除已有属性

Object.isSealed判断一个对象是否是密封的对象

  1. const Parent = {
  2. a: 1
  3. };
  4. const child = Object.create(Parent);
  5. child.b = 2;
  6. Object.seal(child);
  7. child.c = 3;
  8. child.b = 4;
  9. delete child.b;
  10. console.log('child :>> ', child); // { b: 4 }
  1. Object.preventExtensions使一个对象不可拓展,可以改已有属性;但是可以删除已有属性 ```javascript const Parent = { a: 1 }; const child = Object.create(Parent); child.b = 2;

Object.preventExtensions(child); child.c = 3; delete child.b

console.log(‘child :>> ‘, child); // {}

  1. 16. `Object.keys`返回自身**可枚举属性**返回的数组
  2. 16. `Object.prototype.propertyIsEnumerable`判断对象**自身属性**是否是可枚举的
  3. ```javascript
  4. const Parent = {
  5. a: 1
  6. };
  7. const child = Object.create(Parent, {
  8. d: {
  9. value: 4,
  10. enumerable: false
  11. }
  12. });
  13. child.b = 2;
  14. console.log(child.propertyIsEnumerable('a')); // false
  15. console.log(child.propertyIsEnumerable('d')); // false
  1. Object.setPrototypeOf(obj, prototype)设定一个对象的原型,会有很大的性能问题
  2. Object.prototype.toLocaleString返回一个对象在不同语言环境下的字符串表示 ```javascript const Parent = { a: 1 }; const child = Object.create(Parent, { d: { value: 4, enumerable: false } }); child.b = 2;

console.log(child.toLocaleString()); // 默认的[object Object]

  1. <a name="vWX3k"></a>
  2. ### 二. Web存储
  3. <a name="UcS5e"></a>
  4. #### 1. cookie
  5. **定义**:cookie是服务器发送给用户浏览器的一小片段数据;浏览器会自动将cookie存储在本地;浏览器会在下次发送请求到同一服务器的时候自动携带上它。一般用于确认两个请求是否来自于同一浏览器。<br />**使用场景**:
  6. 1. 会话状态管理(用户登录状态,购物车等)
  7. 1. 个性化设置(用户自定义设置,主题等)
  8. 1. 浏览器行为追踪(跟踪分析用户行为)
  9. **特点**:
  10. 1. 大小限制为4kb
  11. 1. 只支持ASCII,其他字符要转码
  12. **cookie参数**:服务器使用`Set-Cookie`设置响应头,响应头中`Set-Cookie`可以有多个<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/23168078/1647957611478-c798ba72-6209-46e1-b157-94f3f534b020.png#clientId=uf61c2c84-b568-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=327&id=u63c85fb0&margin=%5Bobject%20Object%5D&name=image.png&originHeight=327&originWidth=501&originalType=binary&ratio=1&rotation=0&showTitle=false&size=25379&status=done&style=none&taskId=u6bc4a32b-95c7-4552-8fa0-0b5efc7587d&title=&width=501)<br />cookie当中可选参数的说明:
  13. 1. `httpOnly:true`是否只允许http访问,如果为true则js脚本的`document.cookie`无法访问该数据
  14. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/23168078/1647958131113-ec7dc5e7-2239-41d8-bf78-fd6063f6e011.png#clientId=uf61c2c84-b568-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=45&id=u932eab65&margin=%5Bobject%20Object%5D&name=image.png&originHeight=45&originWidth=469&originalType=binary&ratio=1&rotation=0&showTitle=false&size=5416&status=done&style=none&taskId=u291bdd39-a5cb-4cec-a926-479599fe3ec&title=&width=469)
  15. ```javascript
  16. // age=12 无法访问到
  17. const cookie = document.cookie;
  18. console.log('cookie :>> ', cookie); // cookie :>> user=jay
  1. secure如果服务端设置了cookie为secure则cookie只能通过加密传输(https)。浏览器端会报500错误并且无法获取cookie

服务端设置secure的效果:

  1. ctx.cookies.set(
  2. 'user',
  3. 'jay',
  4. {
  5. maxAge: 5000000,
  6. httpOnly: false,
  7. secure: true // 无法通过http传输
  8. }
  9. );

image.pngimage.png
客户端设置sucure的效果:
无法保存到本地且无法传输到服务端
image.png
image.png

  1. document.cookie = 'user=chou;Secure';
  2. axios.get(`${host}:3000/user`).then((res) => {
  3. console.log(res.data);
  4. const cookie = document.cookie;
  5. console.log('cookie :>> ', cookie); // cookie :>>
  6. });

由于浏览器做了特殊设置,localhost设置secure效果和https相同;
参考:cookie设置secure属性不生效

  1. SameSite声明cookie在什么情况下可以携带,有效预防CSRF
    • strict:cookie只会在请求地址与当前域名相同时才会传输,完全禁止第三方发送请求携带cookie
    • Lax:默认值;允许第三方get请求携带cookie
    • None:没有限制,请求自动携带
  2. Expires/Max-Age:Sessioncookie过期时间,默认会话阶段;如果cookie过期会自动从本地删除(document.cookie无法访问,下次请求也不会带上);
  3. Domain:origin指定哪些主机可接受cookie,默认origin;如果指定了某个域,则其他域js脚本无法访问该cookie且请求同一主机时不会携带上它
  4. Path:/指定主机下哪些路径可以接受cookie,效果和Domain类似,是Domain的补充
    1. // localhost可以正常访问修改;上传时会携带
    2. // http://72.26.21.36:3000不可以访问修改,上传时不会携带
    3. ctx.cookies.set(
    4. 'user',
    5. 'jay',
    6. {
    7. maxAge: 5000000,
    8. httpOnly: false,
    9. domain: 'localhost'
    10. }
    11. );
    image.png
    image.png
    问题:跨域请求的时候如何携带上cookie?
    答:客户端的xhr.withCredentials=true,跨域的服务端(前提是允许跨域)的响应头需要设置Access-Control-Allow-Credential:true
    withCredentials只对跨域的请求有效,同源的请求会被忽略

    2. localStorage和sessionStorage

    localStoragesessionStorage都是Web存储技术;使用的方法相同:getItem, setItem, removeItem, clear
  • localStorage['a']像数组一样获取某个值value。没有key会返回null
  • localStorage.key(n)获取第n位的键key,下标越界会返回null
  • 总之storage找不到键值不会报错,而是会返回null

共同点:

  1. 大小5M左右
  2. 操作是同步的
  3. 键值总是以字符的形式存储

区别:

  1. localStorage的数据永久存储;sessionStorage只在会话期间存储,页面关闭会被删除

    三. 跨域

    1. 同源策略

    同源策略限制不同源的资源之间的交互,如A网站不能请求B网站的资源;同源是指协议 + 域名 + 端口相同。
    同源策略可以防止XSS,CSRF(中间人)攻击;
    CSRF攻击:用户访问A网站,拿到A网站的cookie;然后访问恶意的B网站,如果没有同源策略B可以获取A网站的cookie,模拟用户携带A的cookie向A发送恶意请求。
    同源策略限制访问非同源的:

  2. Cookie,localStorage,sessionStorage,indexDB等

  3. DOM
  4. ajax请求

不受同源策略限制的:

  1. script标签,可以使用jsonp跨域
  2. link标签,外联css
  3. img``video,audio标签
  4. iframe中嵌入的资源;总结就是有src、href的标签都可以跨域
  5. css中@font-face引用的字体

    2. 跨域解决方案

    跨域解决方案如下

    1) CORS跨域资源共享

    cors允许浏览器向跨源的服务器发送ajax请求;需要服务器添加响应头允许跨域

  6. 简单请求的CORS

简单请求意味着浏览器不需要发送预检请求OPTIONS,只需要服务端响应头添加Access-Control-Allow-Origin且值包含请求的域

  1. Access-Control-Allow-Origin: * // 值包含origin(请求的域)就可以

简单请求满足以下条件:

  1. 请求方法只能是GET, POST, HEAD
  2. 用户只能添加以下请求头:
    • Content-Type:限制为text/plaintext/x-www-form-urlencodedmultipart/form-data
    • Content-Language:客户端希望采用的语言
    • Accept-Language:客户端声明可以理解的自然语言
    • Accept:告知服务器可以处理的类型
    • Width:不推荐
    • Viewport-Width:不推荐
    • Save-Data:客户端对减少数据使用量的偏好
    • DPR:客户端的像素比
    • Downlink:连接服务器的大约带宽
  3. XMLHttpRequestUpload对象没有注册任何事件,请求中没有使用ReadableStream对象
    1. 复杂请求的CORS

复杂请求需要发送预检请求OPTIONS,这就需要服务器支持处理OPTIONS请求并添加3个必要的响应头

  1. /**
  2. 简单请求和复杂请求都必须要有且包含请求的域;
  3. 如果设置允许携带Cookie(Access-Control-Allow-Credentials: true)就不允许设置为*
  4. */
  5. ctx.set('Access-Control-Allow-Origin', ctx.headers.origin);
  6. /**
  7. 允许跨域请求的方法,必须包含请求头中的Access-Control-Request-Method方法
  8. */
  9. ctx.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE');
  10. /**
  11. 允许跨域请求头自定义设置的header,必须包含Access-Control-Request-Headers中的字段
  12. */
  13. ctx.set('Access-Control-Allow-Headers', 'Content-Type, Accept');
  14. /**
  15. 可选的是否允许携带Cookie,客户端请求需要设置xhr.withCredentials=true;
  16. Access-Control-Allow-Origin不能为*
  17. */
  18. ctx.set('Access-Control-Allow-Credentials', true);
  19. /**
  20. 可选的OPTIONS缓存的生命周期,生命周期以内再次跨域请求不需要发送预检请求
  21. 默认位5s,-1代表禁止缓存OPTIONS
  22. */
  23. ctx.set('Access-Control-Max-Age', 600);

2) JSONP

JSONP利用script标签不受同源策略限制的特性;script标签外链的脚本执行定义的回调函数,同时传递后端处理完的参数。

  1. 参数一般放在query里面,包含传递的参数,回调函数名称
  2. 仅支持GET请求
  3. 兼容性好

前端实现

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>jsonp</title>
  8. </head>
  9. <body>
  10. <script>
  11. function jsonpCallback(who, res) {
  12. console.log(who + res);
  13. }
  14. </script>
  15. <script src="http://172.26.21.36:5000/sayhello?msg=jay&cb=jsonpCallback"></script>
  16. </body>
  17. </html>

后端实现

  1. const Koa = require('koa');
  2. const Router = require('@koa/router');
  3. const app = new Koa();
  4. const router = new Router();
  5. router.get('/sayhello', (ctx, next) => {
  6. const { cb, msg } = ctx.query;
  7. ctx.body = `${cb}('teacher:' , '${msg}')`
  8. });
  9. app
  10. .use(router.routes())
  11. .use(router.allowedMethods());
  12. app.listen(5000, () => {
  13. console.log('C server start at port 5000');
  14. })

接口返回的内容就是执行回调函数
image.png

3) 代理

服务端通信没有同源策略的显示,可以在客户端和服务端之间添加一个和客户端同源的代理服务器;代理服务请请求到资源后转发给客户端。
前端开发过程中普遍采用http-proxy-middleware库在开发过程中代理接口;如vue,webpack都有相关的快捷设置
还可以使用nginx等反向代理

  1. webpack中的proxy设置:
    1. // devServer.proxy
    2. const path = require("path");
    3. const HtmlWebpackPlugin = require("html-webpack-plugin")
    4. module.exports = {
    5. entry: {
    6. index: "./index.js"
    7. },
    8. output: {
    9. filename: "bundle.js",
    10. path: path.resolve(__dirname, "dist")
    11. },
    12. devServer: {
    13. port: 8000,
    14. proxy: {
    15. "/api": {
    16. target: "http://localhost:8080"
    17. }
    18. }
    19. },
    20. plugins: [new HtmlWebpackPlugin({
    21. filename: "index.html",
    22. template: "webpack.html"
    23. })]
    24. };
    2, vue中的设置 ```javascript // vue2 config/index.js proxyTable: { ‘/api’: { target: ‘http://localhost:8080‘, } }

// vue3 vue.config.js module.exports = { devServer: { port: 8000, proxy: { “/api”: { target: “http://localhost:8080“ } } }, };

  1. 3. 自己实现一个
  2. 前端部分
  3. ```html
  4. <!DOCTYPE html>
  5. <html lang="en">
  6. <head>
  7. <meta charset="UTF-8">
  8. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  9. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  10. <title>proxy</title>
  11. </head>
  12. <body>
  13. <button id="proxy">通过正向代理发送跨域请求</button>
  14. <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  15. <script>
  16. const button = document.querySelector('#proxy');
  17. button.addEventListener('click', function () {
  18. // 可以用添加源,直接使用'/login'
  19. axios.get('http://172.26.21.36:5000/login').then((res) => {
  20. // res.data :>> {code: 0, message: '登陆成功'}
  21. console.log('res.data :>> ', res.data);
  22. })
  23. });
  24. </script>
  25. </body>
  26. </html>
  1. 后端部分
  1. // proxy http-proxy-middleware koa兼容性不好
  2. const express = require('express');
  3. const { createProxyMiddleware } = require('http-proxy-middleware');
  4. const app = express();
  5. app.use('/static', express.static('static'));
  6. app.use('/login', createProxyMiddleware({
  7. target: 'http://172.26.21.36:3000',
  8. changeOrigin: true
  9. }));
  10. app.listen(5000, () => {
  11. console.log('proxy server run at port 5000');
  12. });
  13. // login.js
  14. const Koa = require('koa');
  15. const Router = require('@koa/router');
  16. const app = new Koa();
  17. const router = new Router();
  18. /**
  19. * index.html 加载的时候会请求login接口
  20. */
  21. router.get('/login', (ctx, next) => {
  22. ctx.cookies.set(
  23. 'user',
  24. 'jay',
  25. {
  26. maxAge: 5000000,
  27. httpOnly: false,
  28. domain: '172.26.21.36',
  29. path: '/static'
  30. }
  31. );
  32. ctx.cookies.set('age', 12);
  33. ctx.body = {
  34. code: 0,
  35. message: '登陆成功'
  36. };
  37. });
  38. app
  39. .use(router.routes())
  40. .use(router.allowedMethods());
  41. app.listen(3000, () => {
  42. console.log('A server start at port 3000');
  43. })

4) WebSocket

WebSocket没有同源限制,这里刚好学习以下WebSocket
特点

  1. WebSocket传输层使用TCP协议
  2. WebSocket默认端口是80,443;握手阶段采用http服务
  3. 可以双向发送数据,性能开销小
  4. 可以发送文本,二进制数据
  5. 没有同源限制
  6. 协议标识符ws、wss

ws.readyState有四种状态:

  1. 正在连接 0:WebSocket.CONNECTING
  2. 连接成功,可以通信 1:WebSocket.OPEN
  3. 正在关闭 2:WebSocket.CLOSING
  4. 已经关闭 3:WebSocket.CLOSED

四个事件

  1. open
  2. message
  3. close
  4. error

两个的方法ws.send(data)``ws.close()
后端代码演示:

  1. const WebSocket = require('ws');
  2. const server = new WebSocket.Server({ port: 3010 });
  3. server.on('connection', (socket) => {
  4. console.log('有什么东西连进来啦');
  5. socket.send('作为服务器得发点什么')
  6. socket.on('message', (e) => {
  7. console.log('收到了来自客户端的消息 :>> ', e);
  8. })
  9. })

前端代码演示:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>websocket</title>
  8. </head>
  9. <body>
  10. <script>
  11. const ws = new WebSocket('ws://172.26.21.36:3010');
  12. ws.onopen = (e) => {
  13. console.log('连接成功');
  14. }
  15. ws.onmessage = (e) => {
  16. console.log('接受到消息:', e);
  17. }
  18. ws.onclose = (e) => {
  19. console.log('连接关闭');
  20. }
  21. ws.onerror = (e) => {
  22. console.log('发生错误');
  23. }
  24. </script>
  25. </body>
  26. </html>

5) postMessage

使用window.postMessage跨域,一般用于

  1. 当前页面和嵌套iframe通信
  2. 当前页面和新window.open的页面通信
  3. 多窗口页面的通信

发送方postmessage.html,需要接收方window实例:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>postmessage</title>
  8. </head>
  9. <body>
  10. <iframe src="http://172.26.21.36:5000/static/onmessage.html" frameborder="0" id="frame" onload="load()"
  11. width="500px" height="500px"></iframe>
  12. <script>
  13. // 这个页面通过http://localhost:3000/static/postmessage.html打开
  14. function load() {
  15. const frame = document.querySelector('#frame');
  16. frame.contentWindow.postMessage(
  17. '通过postMessage发送',
  18. 'http://172.26.21.36:5000'
  19. );
  20. console.log('已经发送');
  21. }
  22. </script>
  23. </body>
  24. </html>

接收方onmessage.html:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. <p>hi</p>
  11. <p id="msg"></p>
  12. <script>
  13. window.addEventListener('message', (e) => {
  14. console.log('onmessage接收到的消息 :>> ', e);
  15. const msg = document.querySelector('#msg');
  16. msg.innerHTML = e.data;
  17. })
  18. </script>
  19. </body>
  20. </html>

6) document.location,window.location.hash,window.name等

下次补充

四. 手写

1. 防抖

定义:防抖就是让一个事件在触发n秒后才执行;如果事件在n秒内再次触发,就以最新触发的时间为准,n秒后执行;
防抖的应用

  1. 防止resize,scroll事件频繁触发
  2. 防止input输入框动态搜索input事件频繁触发
  3. 按钮提交事件,只触发最后一次

实现的思路

  1. 基础的:首先需要返回一个函数d,函数d内创建一个定时器timer,wait毫秒后执行;这个定时器可以用闭包的特性保存在返回函数d外;由于防抖需要在最后一次触发函数wait时间后执行;所以返回函数d在重新设置定时器timer前需要清除它
  2. 考虑this指向和传递参数;this的执行就是返回函数d的指向;参数就是返回函数d的arguments;可以用func.apply修改this指向和参数
  3. 考虑添加一个立即执行的immediate参数;是否在第一个触发的时候直接执行函数;添加一个first标志位,如果是第一个触发且立即执行则直接执行func函数;这里可以返回返回值
  4. 考虑添加取消防抖的方法;添加一个cancel的方法和canceled的标志位;cancel方法里设置canceled为false并且清除定时器

    1. function debounce(func, wait, immediate) {
    2. let timeout;
    3. let first = true;
    4. let canceled = false;
    5. const d = function (...args) {
    6. if ((first && immediate) || canceled) {
    7. first = false;
    8. return func.apply(this, args);
    9. }
    10. clearTimeout(timeout);
    11. timeout = setTimeout(() => {
    12. func.apply(this, args);
    13. }, wait);
    14. }
    15. d.cancel = () => {
    16. clearTimeout(timeout);
    17. canceled = true;
    18. }
    19. return d;
    20. }

    2. 节流

    定义:函数在触发n秒后执行;如果函数在n秒内再次触发则忽略它直到函数执行;即函数每个一段时间最多只执行一次
    应用场景:

  5. 搜索框input联想

  6. 鼠标不断点击、移动;规定一段时间内事件只触发一次

实现的思路:

  1. setTimeout版:如果计时器存在就不执行,如果不存在就设定一个计时器,函数n秒后执行;并把计时器设为null。考虑this的指向和传参问题。考虑增加立即执行immediate和第一次执行first的标志位
  2. timestamp版:记住上一次执行的时间previous;如果触发函数的时候距离上一次执行时间大于wait;则执行函数并更新上一次执行时间previous ```javascript /*停止触发时执行最后一次 / function throttle(func, wait, immediate) { let timeout; let first = true; return function (…args) {
    1. if (immediate && first) {
    2. first = false;
    3. return func.apply(this, args);
    4. }
    5. if (!timeout) {
    6. timeout = setTimeout(() => {
    7. timeout = null;
    8. func.apply(this, args);
    9. }, wait);
    10. }
    } }

/* 立即执行,停止触发后不执行最后一次 / function throttleTimestamp(func, wait) { let previous = +new Date(); return function (…args) { const now = +new Date(); if (now - previous >= wait) { func.apply(this, args); previous = now; } } }

  1. <a name="ivyKF"></a>
  2. #### 3. 快排
  3. ```javascript
  4. function quickSort(array) {
  5. sort(array, 0, array.length - 1);
  6. }
  7. function sort(array, left, right) {
  8. if (left < right) {
  9. const index = partion(array, left, right);
  10. sort(array, left, index - 1);
  11. sort(array, index + 1, right);
  12. }
  13. }
  14. function partion(array, left, right) {
  15. // 以左一为基准
  16. const pivot = array[left];
  17. let i = left;
  18. let j = right;
  19. while (i < j) {
  20. while (i < j && array[j] >= pivot) {
  21. j--;
  22. }
  23. // 不需要交换,直接覆盖最左边的
  24. array[i] = array[j];
  25. while (i < j && array[i] <= pivot) {
  26. i++;
  27. }
  28. array[j] = array[i];
  29. }
  30. // 基准最后落在i位
  31. array[i] = pivot;
  32. return i;
  33. }

4. instanceof

  1. function myInstanceof(target, origin) {
  2. if (typeof target !== 'object' || target === null) {
  3. return false;
  4. }
  5. if (typeof origin !== 'function') {
  6. throw new Error('Type Error');
  7. }
  8. let prototype = Object.getPrototypeOf(target);
  9. if (prototype === origin.prototype) {
  10. return true;
  11. }
  12. return myInstanceof(prototype, origin);
  13. }

5. 数组扁平化

  1. // reduce + 递归
  2. function flatten(array) {
  3. return array.reduce((previous, next) => {
  4. return previous.concat(Array.isArray(next) ? flatten(next) : next);
  5. }, []);
  6. }

6. reduce

如果有初始值,从下标1开始遍历

  1. Array.prototype.myReduce = function (callback, initial) {
  2. const array = this;
  3. if (array.length === 0) {
  4. return initial;
  5. }
  6. const hasInitial = initial == null;
  7. let previous = hasInitial ? array[0] : initial;
  8. let k = hasInitial ? 1 : 0;
  9. for (let i = k; i < array.length; i++) {
  10. previous = callback(previous, array[i], i, array);
  11. }
  12. return previous;
  13. }

7. 数组去重

  1. function unique(array) {
  2. return Array.from(new Set(array));
  3. }
  4. function unique2(array) {
  5. return array.filter((item, index) => array.indexOf(item) === index);
  6. }

8. 带并发限制的Promise异步调度器

问题:最多并发2个

  1. class Scheduler {
  2. add(promiseMaker) {}
  3. }
  4. const timeout = (time) =>
  5. new Promise((resolve) => {
  6. setTimeout(resolve, time);
  7. });
  8. const scheduler = new Scheduler();
  9. const addTask = (time, order) => {
  10. scheduler.add(() => timeout(time).then(() => console.log(order)));
  11. };
  12. addTask(1000, "1");
  13. addTask(500, "2");
  14. addTask(300, "3");
  15. addTask(400, "4");
  16. // output:2 3 1 4
  17. // 一开始,1,2两个任务进入队列。
  18. // 500ms 时,2完成,输出2,任务3入队。
  19. // 800ms 时,3完成,输出3,任务4入队。
  20. // 1000ms 时,1完成,输出1。

思路:

  1. add返回一个函数promiseCreator;这个函数返回一个Promise对象;
  2. promiseCreator并不是add之后立马执行;而是放在一个队列里面,并保存reslove,reject回调;尝试立马执行
  3. 只有并行数量小于设定时才立马取出队头的promiseCreator执行;否则等一个任务执行完成后再尝试执行
    1. class Scheduler {
    2. maxCount = 2;
    3. runningCount = 0;
    4. promises = [];
    5. constructor(maxCount) {
    6. if (typeof maxCount === 'number') {
    7. this.maxCount = maxCount;
    8. }
    9. }
    10. add(promiseCreator) {
    11. return new Promise((resolve, reject) => {
    12. promiseCreator.resolve = resolve;
    13. promiseCreator.reject = reject;
    14. this.promises.push(promiseCreator);
    15. this.run();
    16. })
    17. }
    18. run() {
    19. if (this.runningCount < this.maxCount && this.promises.length) {
    20. this.runningcount++;
    21. const promise = this.promises.shift();
    22. promise().then((value) => {
    23. promise.resolve();
    24. }).catch((error) => {
    25. promise.reject(error);
    26. }).finally(() => {
    27. this.runningcount--;
    28. this.run();
    29. })
    30. }
    31. }
    32. }
    参考:实现一个带并发限制的异步调度器,保证同时运行的任务最多有两个