JavaScrip 常用知识点总结


JavaScript 常见知识总结

目录
  • JavaScript 常见知识总结
    • 1. 原始值和引用值类型及差别
    • 2. 判断数据类型的常用方法
    • 3. 类数组和数组的区别与转换
    • 4. 数组常见的 API
    • 5. bind, call, apply 的区别
    • 6. new 的原理
    • 7. this 的全面解析
    • 8. 闭包及其作用
    • 9. 原型与原型链
    • 10. 继承
    • 11. 对象的深拷贝与浅拷贝
      • 11.1 浅拷贝的实现方法
      • 11.2 深拷贝的实现方法
    • 12. 防抖与节流
      • 12.1 防抖
      • 12.2 节流
    • 13. 作用域和作用域链、执行期上下文
    • 14. DOM 常见操作方法
      • 14.1 常用的查找方法
      • 14.2 常用的新建节点方法
      • 14.3 常用的添加新节点方法
    • 15. var && let && const
      • 15.1 变量提升
    • 16. 相等性判断
      • 16.1 严格相等 ===
      • 16.2 非严格相等 ==
      • 16.3 同值相等
      • 16.4 零值相等
      • 16.5 理解相等比较的模型
      • 16.6 什么时候使用 Object.is() 或是 ===
    • 17. JavaScript 中的内存管理
      • 17.1 引用技术
      • 17.2 标记-清除算法
      • 17.3 V8 的垃圾回收
        • 17.3.1 V8 的内存限制
        • 17.3.2 V8 的对象分配
        • 17.3.3 V8 的内存分代
        • 17.3.4 V8 的主要垃圾回收算法
    • 18. 手写发布订阅模式 EventEmitter
    • 19. 获取 url 参数
    • 20. 手写 Promise
    • 21. JS 事件循环
    • 18. Vue React Angular 对比
      • 18.1 Vue
        • 18.1.1 响应式
        • 18.1.2 模板语法
        • 18.1.3 实现 MVVM
        • 18.1.4 使用友好,上手简单
        • 18.1.5 弱化性能优化,用户只需关注逻辑
      • 18.2 React
        • 18.2.1 不可变的设计思想,也是函数式编程的实践
        • 18.2.2 all in js
        • 18.2.3 活跃的社区,繁华的生态
        • 18.2.4 多端发展跨平台的能力
        • 18.2.4 Fiber(时间分片)
      • 18.3 Angular
        • 18.3.1 提供了完成的端到端的解决方案
      • 18.4 三大框架的联系
        • 18.4.1 组件化
        • 18.4.2 都是数据驱动视图
        • 18.4.3 共同的开发套路
      • 18.5 区别
        • 18.5.1 模板 vs JSX
        • 18.5.2 Virtual DOM vs Incremental DOM
        • 18.5.3 函数式编程,声明式编程
        • 18.5.4 社区复杂度
        • 18.5.5 入门难度

1. 原始值和引用值类型及差别

  • 原始值 也叫基本类型,例如 null, undefined, string, number, boolean ,可以通过 typeof 判断某数据是何简单数据类型,但是 typeof nullobject

    var a = 10;
    var b = "hello";
    var c = false;
    var d = null;
    var e = undefined;
    
    console.log(typeof a); // number
    console.log(typeof b); // string
    console.log(typeof c); // boolean
    console.log(typeof d); // object
    console.log(typeof e); // undefined
    
  • 引用值Object, Function, Date, Array, RegExp

    // 引用值
    var o = {
      name: "zxh",
      age: 24,
    };
    console.log(o.name); // zxh
    
  • 区别

    • 原始值存储在栈中,引用值存储在堆中
    • 原始值是以值得拷贝进行赋值,值是不可变的;引用值是以引用的拷贝方式进行赋值,只是可变的
    • 原始值的比较是值的比较,引用值得比较是引用的比较(比较引用的是否为同一对象)

2. 判断数据类型的常用方法

  1. typeof

    typeof 进行类型判断的返回值有:undefined, string, number, boolean, object, symbol, function

    • typeof null 返回 object
    • typeof 正则 返回 object
    console.log(typeof 1);               // number
    console.log(typeof true);            // boolean
    console.log(typeof 'mc');            // string
    console.log(typeof Symbol)           // function
    console.log(typeof function(){});    // function
    console.log(typeof console.log());   // function
    console.log(typeof []);              // object 
    console.log(typeof {});              // object
    console.log(typeof null);            // object
    console.log(typeof undefined);       // undefined
    

    image-20220227140820784

    优点:能够快速区分基本数据类型

    缺点:不能将Object、Array 和 Null 区分,都返回object

  2. instanceof 用于检测某个对象的原型链 __proto__ 上是否存在另一个对象的 prototype

    function instance(target, cons) {
      return cons.prototype.isPrototypeOf(target);
    }
    
    console.log(1 instanceof Number);                    // false
    console.log(true instanceof Boolean);                // false 
    console.log('str' instanceof String);                // false  
    console.log([] instanceof Array);                    // true
    console.log(function(){} instanceof Function);       // true
    console.log({} instanceof Object);                   // true
    

    优点:能够区分Array、Object和Function,适合用于判断自定义的类实例对象

    缺点:Number,Boolean,String 基本数据类型不能判断

  3. Object.prototype.toString.call() 在任何值上调用 Object 原生的 toString() 方法,返回一个 [object NativeConstructorName] 格式的字符串。每个类在内部都有一个 [[Class]] 属性,这个属性中就指定了上述字符串中的构造函数名。但是它不能检测非原生构造函数的构造函数名

    var toString = Object.prototype.toString;
    console.log(toString.call(1));                      //[object Number]
    console.log(toString.call(true));                   //[object Boolean]
    console.log(toString.call('mc'));                   //[object String]
    console.log(toString.call([]));                     //[object Array]
    console.log(toString.call({}));                     //[object Object]
    console.log(toString.call(function(){}));           //[object Function]
    console.log(toString.call(undefined));              //[object Undefined]
    console.log(toString.call(null));                   //[object Null]
    

    优点:精准判断数据类型

    缺点:写法繁琐不容易记,推荐进行封装后使用

  4. constructor

    指向该对象实例的 __proto__.constructor

    constructor 不能判断 undefinednull,并且使用它是不安全的,因为 contructor 的指向是可以改变的

3. 类数组和数组的区别与转换

  • 类数组 指的是可以 通过索引访问元素 并且 拥有 length 属性的对象,如 arguments 对象、NodeList 对象等

    例如:

    var arrLike = {
      0: "my",
      1: "name",
      2: "is",
      3: "zxh",
      length: 4,
    };
    
    // 对应的数组对象为
    var arr = ["my", "name", "is", "zxh"];
    
  • 区别 类数组对象虽然可以通过数组下标取值,但是类数组对象不能调用数组原型上的方法

  • 转换

    • Array.prototype.slice.call(arrLike, 0)
    • Array.prototype.splice.call(arrLike, 0)
    • Array.from(arrLike)
    • var newArr = [... arrLike]
  • 在类数组上使用数组的方法

    • 通过 Function.call
      • Array.prototype.push.call(arrLike, “hello”)
    • 通过 Function.apply
      • Array.prototype.push.apple(arrLike, [“hello”])
  • arguments 对象 函数体中定义了 Arguments 对象,包含函数的参数和 length, callee 属性

    • callee 属性指向函数自身,可以通过它来调用自身函数

4. 数组常见的 API

  • 改变数组本身的方法
    • fill(value, start, end) 用一个固定值填充一个数组中从起始索引到终止索引内的全部元素
    • pop 从数组中删除最后一个元素,并返回该元素的值
    • push 将一个或多个元素添加到数组的末尾,并返回该数组的新长度
    • reverse 将数组中元素的位置颠倒,并返回该数组
    • shift 从数组中删除第一个元素,并返回该元素的值
    • unshift 将一个或多个元素添加到数组的头部,并返回该数组的新长度
    • sort([compareFunction]) 对数组元素进行原地排序并返回此数组,如果省略compareFunction,元素按照转换为的字符串的各个字符的Unicode位点进行排序。
    • splice 通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容
  • 不改变自身的方法
    • concat(value1[, value2[, ...[, valueN]]]) 用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组
    • includes(valueToFind, index) 判断一个数组是否包含一个指定的值,如果包含则返回 true,否则返回 false
    • join 将一个数组的所有元素连接成一个字符串并返回这个字符串
    • slice
    • indexOf 返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回 -1
    • lastIndexOf
    • flat([depth]) 按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回
    • at(index) 返回给定下标的数组元素,可以使用负下标
  • 遍历方法
    • every(callback(element[, index[, array]])[, thisArg]) 如果回调函数的每一次返回都为 truthy 值,返回 true ,否则返回 false
    • some 测试数组中是不是至少有一个元素通过了被提供的函数测试,返回的是一个Boolean类型的值。
    • map 返回一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值
    • reduce 对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值
    • forEach 对数组的每个元素执行一次给定的函数,不会改变原有值
    • filter 创建一个新数组, 其包含通过所提供函数实现的测试的所有元素
    • find 返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined
    • findIndex 方法返回数组中满足提供的测试函数的第一个元素的索引。若没有找到对应元素则返回-1

5. bind, call, apply 的区别

  • callapply 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。

  • 对于 applycall 二者而言,作用完全一样,只是接受参数的方式不太一样

    func.call(this, arg1, arg2);
    func.apply(this, [arg1, arg2])
    

    例如 定义一个 log 方法,让它可以代理 console.log

    function log(){
      console.log.apply(console, arguments); // 参数不确定
    };
    

    接下来的要求是给每一个 log 消息添加一个"(app)"的前辍,比如:

    log("hello world"); //(app)hello world
    
    function log(){
      var args = Array.prototype.slice.call(arguments); // 类数组转换为标准数组
      args.unshift('(app)');
     
      console.log.apply(console, args);
    };                                        
    
  • 区别

    1. 当我们使用一个函数需要改变 this 指向的时候才会用到 call,apply,bind

    2. 如果你要传递的参数不多,则可以使用 fn.call(thisObj, arg1, arg2 ...)

    3. 如果你要传递的参数很多,则可以用数组将参数整理好调用 fn.apply(thisObj, [arg1, arg2 ...])

    4. 如果你想生成一个新的函数长期绑定某个函数给某个对象使用,则可以使用 const newFn = fn.bind(thisObj); newFn(arg1, arg2...)

    5. call,apply,bind 不传参数自动绑定在 window

  • 实现 bind

    • bind 并不是立即执行,而是返回一个性函数,且此函数的 this 无法再次修改

    bind 作用:

    • 可以修改函数 this 执行

    • bind 返回一个绑定 this 的新函数 boundFunction

    • 支持函数柯里化,返回 bound 函数可以传递部分参数

      所谓函数柯里化其实就是在函数调用时只传递一部分参数进行调用,函数会返回一个新函数去处理剩下的参数

    • boundFunctionthis 无法再次修改,使用 call, apply 也不行

    // bind 函数实现
    Function.prototype.myBind = function (context) {
        // 1. 判断调用对象是否为函数
        if (typeof this !== "function") {
            throw new TypeError("Error");
        }
    
        // 2. 获取参数 类数组转换
        var args = [...arguments].slice(1),
            fn = this;
    
        // 3. 返回绑定的函数
        return function Fn() {
            // 根据调用方式,传入不同绑定值
            return fn.apply(
                this instanceof Fn ? this : context,
                args.concat(...arguments)
            );
        };
    };
    
  • 实现 call

    // call 函数实现 ...args
    Function.prototype.myCall = function (context) {
        // 1. 判断调用对象
        if (typeof this !== "function") {
            console.error("type error");
        }
    
        // 2. 获取参数 arguments 为类数组对象 需要使用 [...arguments] 转换为数组
        let args = [...arguments].slice(1),
            result = null;
    
        // 3. 判断 context 是否传入,未传入则设置为 window (默认绑定)
        context = context || window;
    
        // 4. 将调用函数设置为对象的方法
        context.fn = this;
    
        // 5. 调用函数
        result = context.fn(...args);
    
        // 6. 将属性删除
        delete context.fn;
    
        // 7. 返回函数执行结果
        return result;
    };
    
  • 实现 apply

    // apply 函数实现 []
    Function.prototype.myApply = function (context) {
        // 1. 判断调用对象是否为函数
        if (typeof this !== "function") {
            throw new TypeError("Error");
        }
    
        let result = null;
    
        // 2. 判断 context 是否存在吗,如果未传入则为 window
        context = context || window;
    
        // 3. 将函数设为对象的方法
        context.fn = this;
    
        // 4. 调用方法
        if (arguments[1]) {
            result = context.fn(...arguments[1]);
        } else {
            result = context.fn();
        }
    
        // 5. 将属性删除
        delete context.fn;
    
        // 6. f
        return result;
    };
    

6. new 的原理

new 大概会执行以下四个步骤:

  • 创建一个空对象
  • 将空对象的原型链连接到另一个对象
  • 执行构造函数中的代码并绑定 this 到这个对象
  • 如函数没有返回值,则返回该对象
function _new() {
    // 1. 创建一个空对象
    let obj = {};

    // 2. 将空对象的原型链链接到传入的对象
    let [Con, ...args] = arguments;
    obj.__proto__ = Con.prototype;

    // 3. 执行函数并绑定 this
    let res = Con.apply(obj, args);

    // 4. 如果函数有返回值并且为 object,则返回函数的返回值,否则返回 obj
    return res instanceof Object ? res : obj;
}

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.getName = function () {
  return this.name;
};

let p = _new(Person, "sillywa", 23);

7. this 的全面解析

JavaScript 中的 this 实在运行时进行绑定的,并不是再编写时,他的上下文取决于函数调用的各种条件this 的绑定和函数生命的位置没有任何关系,只取决于函数的调用方式

  1. 函数是否在 new 中调用(new 绑定)?如果是的话,this 绑定的是新创建的对象。

    function foo(a) {
        this.a = a;
    }
    var bar = new foo(2);
    console.log(bar.a); // 2
    
  2. 函数是否通过 call、apply 显示绑定或者硬绑定?如果是的话,this 绑定的是指定对象。

    function foo() {
        console.log(this.a);
    }
    var obj = {
        a:2
    };
    foo.call(obj);  // 2
    
  3. 函数是否在某个上下文中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。

    function foo() {
        console.log(this.a);
    }
    var obj = {
        a: 2,
        foo: foo
    };
    obj.foo();  // 2
    
  4. 如果都不是,使用默认绑定。严格模式下绑定到 undefined,否则绑定到全局对象。

    // 隐式丢失
    function foo() {
        console.log(this.a);
    }
    var obj = {
        a: 2,
        foo: foo
    };
    var bar = obj.foo;  // 函数别名!
    
    var a = "oops,global";
    
    bar();  // "oops,global"
    
  5. 箭头函数不使用 this 的四种标准规则,而是根据外层作用域来决定 this

    function foo() {
        return a => {
            // this 来自 foo()
            console.log(this.a);
        }
    }
    var obj1 = {
        a: 2
    };
    var obj2 = {
        a: 3
    };
    var bar = foo.call(obj1);
    bar.call(obj2); // 2
    

8. 闭包及其作用

一个函数有权访问另一个函数作用域中的变量,就形成闭包

闭包表现如下:

  • 第一,闭包是一个函数,而且存在于另一个函数当中
  • 第二,闭包可以访问到父级函数的变量,且该变量不会销毁
function fib() {
  let n1 = 1,
    n2 = 1;
  return function () {
    let result = n1 + n2;
    n1 = n2;
    n2 = result;
    return result;
  };
}

const f = fib();

f(); // 2
f(); // 3
f(); // 5
f(); // 8
f(); // 13

闭包的作用

  • 隐藏变量,避免全局污染
  • 以读取函数内部的变量

闭包缺点

  • 导致变量不会被垃圾回收机制回收,造成内存消耗
  • 不恰当的使用闭包可能会造成内存泄漏的问题

闭包应用

  1. 模仿块级作用域

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

    上面可以使用闭包能使下面的代码按照我们预期的进行执行(每隔1s打印 0,1,2,3,4)

  2. 私有变量

    function MyObject() {
      // 私有变量和私有函数
      var privateVariable = 10;
      function privateFunction() {
        return false;
      }
      // 特权方法
      this.publicMethod = function () {
        privateVariable++;
        return privateFunction;
      };
    }
    
  3. 静态私有变量

    (function () {
      var name = "";
      //
      Person = function (value) {
        name = value;
      };
      Person.prototype.getName = function () {
        return name;
      };
      Person.prototype.setName = function (value) {
        name = value;
      };
    })();
    
    var person1 = new Person("xiaoming");
    console.log(person1.getName()); // xiaoming
    person1.setName("xiaohong");
    console.log(person1.getName()); // xiaohong
    
    var person2 = new Person("luckyStar");
    console.log(person1.getName()); // luckyStar
    console.log(person2.getName()); // luckyStar
    

    上面代码通过一个匿名函数实现块级作用域,在块级作用域中 变量 name 只能在该作用域中访问,同样的通过闭包(作用域链)的方式实现 getNamesetName 来访问 name, 而 getNamesetName 又是原型对象的方法,所以它们成了 Person 实例的共享方法。
    这种模式下,name 就变成了一个静态的、由所有实例共享的属性。在一个实例上调用 setName() 会影响所有的实例。

  4. 模块模式

    var singleton = function () {
      var privateVarible = 10;
      function privateFunction() {
        return false;
      }
    
      return {
        publicProperty: true,
        publicMethod: function () {
          privateVarible++;
          return privateFunction();
        },
      };
    };
    

    模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,首先定义了私有变量和函数

9. 原型与原型链

无论什么时候,只要创建了一个函数,就会根据为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会获得一个 constructor,该属性是一个指向 prototype 属性所在函数的指针。

原型链规定了对象如何查找属性,对于一个对象来说,如果它本身没有某个属性,则会沿着原型链一直向上查找,知道找到属性或者查找完整个原型链。

原型链是实现继承的主要方法,其 基本思想是利用原型链让一个引用类型继承另一个引用类型的属性和方法

原型链

搞清楚这三个属性,__proto__prototypeconstructor

  1. __proto__constructor 属性是对象所独有的
  2. prototype 属性是函数独有的
  3. 函数同样也有属性 __proto__constructor
  • prototype 是函数独有的属性,从图中可以看到它从一个函数指向另一个对象,代表这个对象是这个函数的原型对象,这个对象也是当前函数所创建的实例的原型对象。

    prototype属性.png

  • __proto__ 属性是对象(包括函数)独有的。从图中可以看到 __proto__ 属性是从一个对象指向另一个对象,即从一个对象指向该对象的原型对象(也可以理解为父对象)。显然它的含义就是告诉我们一个对象的原型对象是谁

    proto属性.png

  • constructor 是对象才有的属性,从图中看到它是从一个对象指向一个函数的。指向的函数就是该对象的构造函数

    constructor属性.png

10. 继承

  1. 原型继承

    function SuperType() {
      this.name = "super";
    }
    function SubType() {}
    
    // 利用原型链实现继承
    SubType.prototype = new SuperType();
    
    var instance1 = new SubType();
    console.log(instance1.name); // super
    

    问题在于:

    • 包含引用类型值的原型属性会被所有实例共享,在通过原型来实现继承时,原型实际上也会变成另一个类型的实例。
    • 在创建子类类型的实例时,不能向超类类型的构造函数中传递参数
  2. 盗用构造函数继承

    function SuperType(age, name) {
      this.colors = ["blue", "red"];
      this.age = age;
      this.name = name;
    }
    function SubType() {
      SuperType.call(this, ...arguments);
    }
    
    var instance1 = new SubType(23, "sillywa");
    instance1.colors.push("yellow");
    console.log(instance1.colors, instance1.name);
    
    var instance2 = new SubType(12, "xinda");
    console.log(instance2.colors, instance2.name);
    

    缺点在于方法都只能在构造函数中定义,没有办法实现方法的复用

  3. 组合继承

    组合继承结合了原型继承和借用构造函数继承的优点,其背后的思想是,使用原型链实现对原型方法的继承,使用构造函数实现对实例属性的继承。

    SuperType.prototype.sayName = function () {
      return this.name;
    };
    function SubType(name, age) {
      // 通过 构造函数继承属性
      SuperType.call(this, name);
      this.age = age;
    }
    // 通过原型继承方法
    SubType.prototype = new SuperType();
    
    // ** 重写了 SubType 的 prototype 属性,因此其 constructor 也被重写了,需要手动修正 **
    SubType.prototype.constructor = SubType;
    
    // 定义子类自己的方法
    SubType.prototype.sayAge = function () {
      return this.age;
    };
    

    组合继承最大的问题是,无论什么情况下都会调用两次超类的构造函数。

  4. 原型式继承

    借助原型可以通过已有的对象创建新对象,同时还不必因此创建自定义类型

    function create(o) {
      function F() {}
      F.prototype = o;
      return new F();
    } // Object.create
    var person = {
      name: "sillywa",
      firends: ["Johe"],
    };
    
    var person1 = create(person);
    person1.name = "coder";
    person1.firends.push("Kobe");
    
    var person2 = create(person);
    person2.firends.push("Cury");
    console.log(person2.firends); // ["Johe", "Kobe", "Cury"]
    
  5. 寄生式继承

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

    function createAnother(original) {
      var clone = Object.create(original);
      clone.sayHi = function () {
        console.log("Hi");
      };
      return clone;
    }
    
  6. 组合寄生式继承

    function SuperType(name) {
      this.name = name;
      this.colors = [];
    }
    SuperType.prototype.sayName = function () {
      return this.name;
    };
    
    function SubType(name, age) {
      // 第一次调用父类的构造函数
      SuperType.call(this, name);
      this.age = age;
    }
    // 关键代码
    SubType.prototype = Object.create(SuperType.prototype);
    SubType.prototype.constructor = SubType;
    
    SubType.prototype.sayAge = function () {
      return this.age;
    };
    

11. 对象的深拷贝与浅拷贝

11.1 浅拷贝的实现方法

// 1. 浅拷贝的实现
(function () {
  // 遍历赋值
  //  for in
  function clone1(obj) {
    var cloneObj = {};
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        cloneObj[key] = obj[key];
      }
    }
    return cloneObj;
  }

  // Object.keys()
  function clone2(obj) {
    var cloneObj = {};
    for (const key of Object.keys(obj)) {
      cloneObj[key] = obj[key];
    }
    return cloneObj;
  }

  // Object.entries
  function clone3(obj) {
    var cloneObj = {};
    for (const [key, value] of Object.entries(obj)) {
      cloneObj[key] = value;
    }
    return cloneObj;
  }

  // 2. Object.assign
  function clone4(obj) {
    return Object.assign(obj, {});
  }
})();

11.2 深拷贝的实现方法

  1. 使用 JSON.parse(JSON.stringfy(obj))

    function deepClone(obj) {
        return JSON.parse(JSON.stringify(obj));
    }
    let obj = {
        name: "zxh",
        color: ["cyan", "light"],
        reg: /a/g,
        say: function () {
            console.log("fun say");
        },
    };
    let obj2 = deepClone(obj);
    

    存在问题:遇到函数,undefined,Symbol,Date对象时会自动忽略,遇到正则时会返回空对象

    image-20220304160025883

  2. 使用递归

    // 1. JSON.strinfy() 与 JOSN.parse()
    // 存在问题:遇到函数,undefined,
    // Symbol,Date对象时会自动忽略,
    // 遇到正则时会返回空对象
    function deepClone(obj) {
        return JSON.parse(JSON.stringify(obj));
    }
    
    // 2. 递归
    // for in
    function deepClone1(obj) {
        // undefined 或者为 对象的时候
        if (!obj || typeof obj !== "object") {
            return obj;
        }
        // 为正则表达式的时候
        if (obj instanceof RegExp) {
            return new RegExp(obj);
        }
        
        var cloneObj = new obj.constructor();
        for (const key in obj) {
            if (Object.hasOwnProperty.call(obj, key)) {
                cloneObj[key] = arguments.callee(obj[key]);
            }
        }
        return cloneObj;
    }
    
    // Object.keys 推荐写法 简洁
    function deepClone2(obj) {
        if (!obj || typeof obj !== "object") {
            return obj;
        }
        if (obj instanceof RegExp) {
            return new RegExp(obj);
        }
        
        var cloneObj = new obj.constructor();
        for (const key of Object.keys(obj)) {
            cloneObj[key] = arguments.callee(obj[key]);
        }
        return cloneObj;
    }
    
    // Object.entries
    function deepClone3(obj) {
        if (!obj || typeof obj !== "object") {
            return obj;
        }
        if (obj instanceof RegExp) {
            return new RegExp(obj);
        }
        var cloneObj = new obj.constructor();
        for (const [key, value] of Object.entries(obj)) {
            cloneObj[key] = arguments.callee(value);
        }
        return cloneObj;
    }
    
    let obj = {
        name: "zxh",
        color: ["cyan", "light"],
        reg: /a/g,
        say: function () {
            console.log("fun say");
        },
    };
    let obj2 = deepClone1(obj);
    

    image-20220304213337951

12. 防抖与节流

12.1 防抖

函数在指定时间内只会触发一次,具体实现方法:

  • 第一次触发函数的时候,延迟delay时间执行,如果在delay时间段内再次触发该函数,则重新开始计时

  • 如果delay时间段内没有触发该函数,则执行该函数

function debounce(fn, delay) {
  let timer = null;
  return function () {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, delay);
  };
}

12.2 节流

防抖的问题是,在短时间内不断触发事件,回调函数永远不会执行。

节流的思想:在短时间内不断触发事件,回调函数只会在指定间隔时间内执行。

function throttle(fn, delay) {
  let timer = null;
  return function () {
    if (timer) {
      return false;
    }
    timer = setTimeout(() => {
      fn.apply(this, arguments);
      timer = null;
    }, delay);
  };
}

13. 作用域和作用域链、执行期上下文

作用域是一套规则,用于确定在何处以及如何查找变量。作用域共有两种主要的工作模式,词法作用域和动态作用域,大多数编程语言采用词法作用域,JavaScript也是基于词法作用域的。词法作用域意味着作用域是由书写代码时函数声明的位置来决定的

在JavaScript每个函数有自己的函数作用域,当执行流进入到一个函数的时候,函数的环境就会被推入到一个环境栈中。而在函数执行之后,栈将其环境弹出。

当代码在一个环境中执行的时候,会为该环境创建一个作用域链保证对执行环境有权访问的所有变量的有序访问。作用域链的最前端始终是当前执行代码所在环境,在变量查找的过程中,会沿着作用域链一层一层向上查找,直到找到变量或者找不到变量。

14. DOM 常见操作方法

14.1 常用的查找方法

  • document.getElementById('id属性值'); 返回拥有指定id的对象的引用

  • document.getElementsByClassName('class属性值'); 返回拥有指定class的对象集合

  • document.getElementsByTagName('标签名'); 返回拥有指定标签名的对象集合

  • document.getElementsByName('name属性值'); 返回拥有指定名称的对象结合

  • document/element.querySelector('CSS选择器'); 仅返回第一个匹配的元素

  • document/element.querySelectorAll('CSS选择器'); 返回所有匹配的元素

  • document.documentElement; 获取页面中的HTML标签

  • document.body; 获取页面中的BODY标签

  • document.all['']; 获取页面中的所有元素节点的对象集合型

14.2 常用的新建节点方法

  • document.createElement('元素名');创建新的元素节点

  • document.createAttribute('属性名');创建新的属性节点

  • document.createTextNode('文本内容');创建新的文本节点

  • document.createComment('注释节点'); 创建新的注释节点

  • document.createDocumentFragment( ); 创建文档片段节点

14.3 常用的添加新节点方法

  1. element.setAttribute( attributeName, attributeValue );给元素增加指定属性,并设定属性值

  2. element.setAttributeNode( attributeName );给元素增加属性节点

  3. 如需向 HTML DOM 添加新元素,您必须首先创建该元素(元素节点),然后向一个已存在的元素追加该元素。

15. var && let && const

ES6之前创建变量用的是var,之后创建变量用的是let/const

三者区别

  1. var定义的变量,没有块{}的概念,可以跨块访问, 不能跨函数访问。
    let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。
    const用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,且不能修改。

  2. var可以先使用,后声明,因为存在变量提升;let必须先声明后使用。

  3. var是允许在相同作用域内重复声明同一个变量的,而let与const不允许这一现象。

  4. 在全局上下文中,基于let声明的全局变量和全局对象GO(window)没有任何关系 ;
    var声明的变量会和GO有映射关系;

  5. 会产生暂时性死区:

    暂时性死区是浏览器的bug:检测一个未被声明的变量类型时,不会报错,会返回undefined
    如:console.log(typeof a) //undefined
    而:console.log(typeof a)//未声明之前不能使用
    let a

  6. let /const/function会把当前所在的大括号(除函数之外)作为一个全新的块级上下文,应用这个机制,在开发项目的时候,遇到循环事件绑定等类似的需求,无需再自己构建闭包来存储,只要基于let的块作用特征即可解决

15.1 变量提升

  • function 也存在提升

    例如:

    f()
    function f(){
        console.log('f')
    }
    

    function提升

  • 但是如果 函数名称和变量名称一样时,把 var f 提升的时候,并不会用 undefined 覆盖函数声明

    console.log(f)
    var f = 2
    function f(){
        console.log('f')
    }
    console.log(f)
    var f = 1
    f
    

    image-20220329214642234

    function x(){}
    var x
    x
    

    image-20220329215615170

16. 相等性判断

ES2015中有四种相等算法:

  • 抽象(非严格)相等比较 (==)
  • 严格相等比较 (===):用于: Array.prototype.indexOf(), Array.prototype.lastIndexOf()case-matching
  • 同值零:用于 %TypedArray%ArrayBuffer 构造函数、以及 MapSet 操作,并将用于 ES2016/ES7中的 String.prototype.includes()
  • 同值:用于所有其他地方

JavaScript提供三种不同的值比较操作:

  • 严格相等比较 (也被称作"strict equality", "identity", "triple equals"),使用 === ,
  • 抽象相等比较 ("loose equality","double equals") ,使用 ==
  • 以及 Object.is (ECMAScript 2015/ ES6 新特性)

简而言之,在比较两件事情时,双等号将执行类型转换; 三等号将进行相同的比较而不进行类型转换 (如果类型不同, 只是总会返回 false ); Object.is的行为方式与三等号相同,但是对于NaN和-0和+0进行特殊处理,所以最后两个不相同,而Object.is(NaN,NaN)将为 true。(通常使用双等号或三等号将NaN与NaN进行比较,结果为false,因为IEEE 754如是说.) 请注意,所有这些之间的区别都与其处理原语有关; 这三个运算符的原语中,没有一个会比较两个变量是否结构

16.1 严格相等 ===

全等操作符比较两个值是否相等,两个被比较的值在比较前都不进行隐式转换。如果两个被比较的值具有不同的类型,这两个值是不全等的。否则,如果两个被比较的值类型相同,值也相同,并且都不是 number 类型时,两个值全等。最后,如果两个值都是 number 类型,当两个都不是 NaN,并且数值相同,或是两个值分别为 +0 和 -0 时,两个值被认为是全等的。

var num = 0;
var obj = new String("0");
var str = "0";
var b = false;

console.log(num === num); // true
console.log(obj === obj); // true
console.log(str === str); // true

console.log(num === obj); // false
console.log(num === str); // false
console.log(obj === str); // false
console.log(null === undefined); // false
console.log(obj === null); // false
console.log(obj === undefined); // false

在日常中使用全等操作符几乎总是正确的选择。对于除了数值之外的值,全等操作符使用明确的语义进行比较:一个值只与自身全等。对于数值,全等操作符使用略加修改的语义来处理两个特殊情况:第一个情况是,浮点数 0 是不分正负的。区分 +0 和 -0 在解决一些特定的数学问题时是必要的,但是大部分情况下我们并不用关心。全等操作符认为这两个值是全等的。第二个情况是,浮点数包含了 NaN 值,用来表示某些定义不明确的数学问题的解,例如:正无穷加负无穷。全等操作符认为 NaN 与其他任何值都不全等,包括它自己。(等式 (x !== x) 成立的唯一情况是 x 的值为 NaN)

16.2 非严格相等 ==

相等操作符比较两个值是否相等,在比较前将两个被比较的值转换为相同类型。在转换后(等式的一边或两边都可能被转换),最终的比较方式等同于全等操作符 === 的比较方式。 相等操作符满足交换律。

相等操作符对于不同类型的值,进行的比较如下图所示:

被比较值 B
Undefined Null Number String boolean Object
Undefined true true false false false IsFalsy(B)
Null true true false false false IsFalsy(B)
被比较值 B [B] Number false false A === B A === ToNumber(B) A === ToNumber(B) A == ToPrimitive(B)
String false false ToNumber(A) === B A === B ToNumber(A) === ToNumber(B) ToPrimitive(B) == A
Boolean false false ToNumber(A) === B ToNumber(A) === ToNumber(B) A === B ToNumber(A) == ToPrimitive(B)
Object false false ToPrimitive(A) == B ToPrimitive(A) == B ToPrimitive(A) == ToNumber(B) A === B

在上面的表格中,ToNumber(A) 尝试在比较前将参数 A 转换为数字,这与 +A(单目运算符+)的效果相同。ToPrimitive(A)通过尝试调用 A 的A.toString()A.valueOf() 方法,将参数 A 转换为原始值(Primitive)。

一般而言,根据 ECMAScript 规范,所有的对象都与 undefined null 不相等。但是大部分浏览器允许非常窄的一类对象(即,所有页面中的 document.all 对象),在某些情况下,充当效仿 undefined 的角色。相等操作符就是在这样的一个背景下。因此,IsFalsy(A) 方法的值为 true ,当且仅当 A 效仿 undefined。在其他所有情况下,一个对象都不会等于 undefined null

var num = 0;
var obj = new String("0");
var str = "0";
var b = false;

console.log(num == num); // true
console.log(obj == obj); // true
console.log(str == str); // true

console.log(num == obj); // true
console.log(num == str); // true
console.log(obj == str); // true
console.log(null == undefined); // true

// both false, except in rare cases
console.log(obj == null);
console.log(obj == undefined);

有些开发者认为,最好永远都不要使用相等操作符。全等操作符的结果更容易预测,并且因为没有隐式转换,全等比较的操作会更快。

16.3 同值相等

同值相等解决了最后一个用例:确定两个值是否在任何情况下功能上是相同的。(这个用例演示了里氏替换原则的实例。)当试图对不可变(immutable)属性修改时发生出现的情况:

// 向 Nmuber 构造函数添加一个不可变的属性 NEGATIVE_ZERO
Object.defineProperty(Number, "NEGATIVE_ZERO",
                      { 
    value: -0, 
    writable: false, 
    configurable: false, 
    enumerable: false 
});

function attemptMutation(v)
{
    Object.defineProperty(Number, "NEGATIVE_ZERO", { value: v });
}

Object.defineProperty 在试图修改不可变属性时,如果这个属性确实被修改了则会抛出异常,反之什么都不会发生。例如如果 v 是 -0 ,那么没有发生任何变化,所以也不会抛出任何异常。但如果 v 是 +0 ,则会抛出异常。不可变属性和新设定的值使用 same-value 相等比较。

同值相等由 Object.is 方法提供。

16.4 零值相等

与同值相等类似,不过会认为 +0 与 -0 相等。

16.5 理解相等比较的模型

在 ES2015 以前,你可能会说双等和三等是“扩展”的关系。比如有人会说双等是三等的扩展版,因为他处理三等所做的,还做了类型转换。例如 6 == "6" 。反之另一些人可能会说三等是双等的扩展,因为他还要求两个参数的类型相同,所以增加了更多的限制。怎样理解取决于你怎样看待这个问题。

但是这种比较的方式没办法把 ES2015 的 Object.is 排列到其中。因为 Object.is 并不比双等更宽松,也并不比三等更严格,当然也不是在他们中间。从下表中可以看出,这是由于 Object.is 处理 NaN 的不同。注意假如 Object.is(NaN, NaN) 被计算成 false ,我们就可以说他比三等更为严格,因为他可以区分 -0+0 。但是对 NaN 的处理表明,这是不对的。 Object.is 应该被认为是有其特殊的用途,而不应说他和其他的相等更宽松或严格。

判等

x y == === Object.is
undefined undefined true true true
null null true true true
true true true true true
false false true true true
"foo" "foo" true true true
0 0 true true true
+0 -0 true true false
0 false true false false
"" false true false false
"" 0 true false false
"0" 0 true false false
"17" 17 true false false
[1,2] "1,2" true false false
new String("foo") "foo" true false false
null undefined true false false
null false false false false
undefined false false false false
{ foo: "bar" } { foo: "bar" } false false false
new String("foo") new String("foo") false false false
0 null false false false
0 NaN false false false
"foo" NaN false false false
NaN NaN false false true

16.6 什么时候使用 Object.is() 或是 ===

总的来说,除了对待NaN的方式,Object.is唯一让人感兴趣的,是当你需要一些元编程方案时,它对待0的特殊方式,特别是关于属性描述器,即你的工作需要去镜像Object.defineProperty的一些特性时。如果你的工作不需要这些,那你应该避免使用Object.is,使用===来代替。即使你需要比较两个NaN使其结果为true,总的来说编写使用NaN 检查的特例函数(用旧版本ECMAScript的isNaN方法)也会比想出一些计算方法让Object.is不影响不同符号的0的比较更容易些。

17. JavaScript 中的内存管理

像C语言这样的底层语言一般都有底层的内存管理接口,比如 malloc()free()。相反,JavaScript是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。 释放的过程称为垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其他高级语言)开发者错误的感觉他们可以不关心内存管理。

内存生命周期

  1. 分配所需要的内存
  2. 使用分配到的内存(读、写)
  3. 不需要时将其释放/归还

所有语言第二部分都是明确的。第一和第三部分在底层语言中是明确的,但在像JavaScript这些高级语言中,大部分都是隐含的。

JavaScript 的内存分配

值的初始化

为了不让程序员费心分配内存,JavaScript 在定义变量时就完成了内存分配。

使用值

使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。

当内存不再需要使用时释放

大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“哪些被分配的内存确实已经不再需要了”。它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。

高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。

17.1 引用技术

垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。

在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)。

这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

限制:循环引用

该算法有个限制:无法处理循环引用的事例。在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

17.2 标记-清除算法

这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。

这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。

这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。

从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。

循环引用不再是问题了

在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。第二个示例同样,一旦 div 和其事件处理无法从根获取到,他们将会被垃圾回收器回收。

限制: 那些无法从根对象查询到的对象都将被清除

尽管这是一个限制,但实践中我们很少会碰到类似的情况,所以开发者不太会去关心垃圾回收机制。

17.3 V8 的垃圾回收

17.3.1 V8 的内存限制

Node不同于其他后端语言,Node在对系统的内存使用中,只能使用到系统的部分内存,比如64位系统只能使用1.4GB,32位系统只能使用0.7GB。随之到来的问题是Node采用单线程,就导致每个线程无法对大的内存对象进行处理,比如将一个2GB的文件读入内存进行字符串分析处理,即使你有16G的物理内存。

17.3.2 V8 的对象分配

在javascript中我们的基本类型存储在栈中,所有对象都分配给了堆处理。 我们每赋值一个对象,该对象的内存就会分配在堆中。如果已申请堆所剩内存不足以分配新的对象,将会继续申请新内存,直到堆的大小超过V8的内存大小限制为止。

V8对象分配

至于V8的内存限制,起源于V8本身是chrome为浏览器设计而生,而浏览器中对于网页来说,V8控制的内存绰绰有余。还源于V8设计者对于V8的垃圾回收机制的限制,官方以1.5GB的垃圾回收堆内存为例,V8执行一个小的垃圾回收要使用50毫秒以上,做一次常规非增量式垃圾回收要在1秒以上。

最关键的,javascript的垃圾回收会对javascript执行线程形成阻塞,作为一个开发人员你应该能够清楚时长1秒的进程阻塞,对你的项目性能的影响,故此V8的设计者采用了对堆内存进行限制的策略。

17.3.3 V8 的内存分代

V8的垃圾回收策略主要基于分代,那么怎么分代呢?

在V8中,主要将内存分为新生代老生代两类。

新生代指的是那些存活时间较短的对象

老生代指的是存活时间较长的或者常驻内存的对象

而新生代加老生代的对象所占空间大小就是V8的堆的整体大小。

前端-V8的内存分代

V8提供了设置新生代和老生代最大内存值的方式,从而可以调整V8的整体内存限制,使用更多的内存空间。

  • 使用--max-old-space-size来调整老生代最大空间

  • --max-new-space-size来调整新生代最大空间,但是该操 作需要在Node进程启动时就设置才有效。

17.3.4 V8 的主要垃圾回收算法

  1. Scanvenge 算法

    Scanvenge是一种复制形式的垃圾回收算法,是应用于新生代对象中的一种垃圾回收算法,算法首先将堆内存一分为二,两部分空间一半用来分配赋值的对象,叫做From空间,另一半处于空闲的叫做To空间。

    为什么要有一半空间用来闲置呢?这不是让我们的可用内存更小了吗?

    当我们为堆分配对象时,会将分配对象放到From空间中存储,

    在V8的垃圾和回收过程中,

    • 会首先检查From中存活的对象(什么是存活的对象就是指那些还被继续引用没有完全释放的对象),
    • V8会将From中存活的对象复制到To空间中,同时清理掉已经被释放的对象空间。
    • 完成该过程From空间和To空间即完成了角色对换,也就是在下一次回收中,之前的From空间变成了To空间,之前的To空间变成了From空间。

    Scanvenge算法

    缺点: 明显的缺点就是只能使用堆内存的一半

    优点: ** 但是随之带来的好处就是它在时间效率**上的优异的表现,属于典型的牺牲空间换取时间的算法。

    需要强调的是,开头提到的 Scanvenge算法是应用于新生代对象中的一种垃圾回收算法,因为新生代对象中的生命周期较短的特性,也契合于该算法优先时间考虑的特性。

    怎样算生命周期较长的对象?

    当一个对象经过多次复制依然存活时,它将会被认为是生命周期较长的对象。这种生命周期较长的对象随后会被移动到老生代对象中,采用新的算法(Mark-Sweep&Mark-Compact)进行管理,这个过程称为晋升。

    通过上图可以了解到,对象进行垃圾回收是怎样从From到To之间转换的,那么这个晋升的过程在哪儿体现呢?

    在默认情况下,V8对新生代对象进行从From到To空间进行复制时,会先检查它的内存地址来判断这个对象是否已经经历过一次Scanvenge回收。如果已经经历过,那么会将该对象从From空间直接复制到老生代空间,如果没有,才会将其复制到To空间。

    对象晋升的条件主要有两个:

    • 一个是对象是否经历过Scanvenge回收
    • 一个是To空间的内存占用超过限制。

    假设一个对象像刚才说的没有经历过Scanvenge回收,要将它复制到To空间之前,还要再进行一次检查。检查To空间是否已经使用了超过25%,如果To空间超过25%,该对象将直接被晋升到老生代空间进行管理。

    完整看一下这个流程:

    未经历过Scanvenge回收检查流程

    对象晋升后,该对象即成为老生代中的存活周期较长的对象,所以我们可以重新对老生代进行定义:老生代对象为存活周期较长或常驻内存的对象,或为新生代对象回收中溢出的对象

    至于为什么设置25%的原因是,当一次Scanvenge回收完成时,To空间变为From空间,如果新的From空间使用占比过高,将对接下来的内存分配到这个新的From空间过程存在很大的影响。

  2. Mark-Sweep & Mark-Compact 算法

    老生代中的对象使用的回收算法,这种算法(Mark-Sweep)也是我们常说的垃圾回收中的 标记清除 算法。

    首先,老生代空间不会一分为二,老生代空间进行垃圾回收时,首先是标记阶段。V8会在标记阶段遍历老生代空间中的所有对象,并标记存活的对象(即还没有被完全释放的对象),在随后的清除阶段,会将所有未标记的老生代对象全部回收。

    标记清除过程

    Mark-Sweep在执行完清除之后,导致内存空间出现不连续的情况,就像你的磁盘分析图一样。

    这样会带来的一个问题就是,当你需要分配一个较大的对象时,剩余的内存因为碎片化的原因,没有任何一个内存碎片足以分配给这个大的对象内存空间,就会导致提前触发垃圾回收,而这次回收是不必要的。

    所以Mark-Compact算法随之而生,Mark-Compact比Mark-Sweep增加了一个 整理 的概念,它的回收执行顺序是标记—整理—清除Mark-Compact所谓的整理概念是指在对象同样被标记为存活后,会将活着的对象往一端移动,移动完成后在直接清理掉死亡的对象内存。

    标记清除整理

    两种差别显而易见,Mark-Compact算法执行后的内存空间更合理。但是因为Mark-Compact算法需要移动对象,随之导致的就是它的执行速度没有Mark-Sweep快。

    所以在V8中主要使用Mark-Sweep算法,只有在空间不足以对新生代中晋升过来的对象进行分配时,才会使用Mark-Compact算法进行回收

    回收算法 Scanvenge Mark-Sweep Mark-Compact
    速度 最快 中等 最慢
    空间开销 双倍空间(无碎片) 少(有碎片) 少(无碎片)
    是否移动对象
  3. Incrental Marking 算法

    因为垃圾回收会阻塞javascript的运行,故此老生代对象又因为其占用空间大,存活对象多的特点,对其进行标记,整理,回收的过程引起的阻塞要远远比新生代对象回收过程一起的阻塞要严重的多,Incremental Marking算法成为了优化老生代对象耗时的算法选择。

    为了降低老生代空间垃圾回收带来的停顿影响,V8 采用了增量标记(incremental marking)的算法。将原本一口气停顿完成的来及回收过程拆分为许多小“步进”,每做完一“步进”就让JavaScript应用逻辑继续执行一小会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成。取得的效果就是,将老生代空间垃圾回收的最大停顿时间可以减少到原本的1/6左右。

    增量标记

    V8 后续还引入了延迟清理(lazy sweeping)、增量式整理(incremental compaction)、并发标记 等技术。

18. 手写发布订阅模式 EventEmitter

class EventEmitter {
  constructor() {
    // 单例模式
    if (!EventEmitter.instance) {
      EventEmitter.instance = this;
      this.handleMap = {};
    }
    //map结构,用于储存事件与其对应的回调
    return EventEmitter.instance;
  }

  //事件订阅,需要接收订阅事件名和对应的回调函数
  on(eventName, callback) {
    this.handleMap[eventName] = this.handleMap[eventName] ?? [];
    this.handleMap[eventName].push(callback);
  }

  //事件发布,需要接收发布事件名和对应的参数
  emit(eventName, ...args) {
    if (this.handleMap[eventName]) {
      //这里需要浅拷贝一下handleMap[eventName],因为在 once 添加订阅时会修改this.handleMap,若once绑定在前就会导致后一个监听被移除
      const handlers = [...this.handleMap[eventName]];
      handlers.forEach((callback) => callback(...args));
    }
  }

  //移除订阅,需要移除的订阅事件名及指定的回调函数
  remove(eventName, callback) {
    const callBacks = this.handleMap[eventName];
    const index = callBacks.indexOf(callback);
    if (index !== -1) {
      callBacks.splice(index, 1);
    }
  }

  //添加单次订阅,触发一次订阅事件后取消订阅,需要添加的订阅事件名及指定的回调函数
  once(eventName, callback) {
    const warpper = (...args) => {
      callback(...args);
      this.remove(eventName, warpper);
    };
    this.on(eventName, warpper);
  }
}

测试:

//基础测试
const eventBus = new EventEmitter();
eventBus.once("demo", (params) => {
  console.log(1, params);
});
eventBus.on("demo", (params) => {
  console.log(2, params);
});
eventBus.on("demo", (params) => {
  console.log(3, params);
});
eventBus.emit("demo", "someData");
console.log(eventBus);

手写EventEmitter测试

19. 获取 url 参数

  • 直接使用 URLSearchParams 方法

    // 创建一个URLSearchParams实例
    const urlSearchParams = new URLSearchParams(window.location.search);
    // 把键值对列表转换为一个对象
    const params = Object.fromEntries(urlSearchParams.entries());
    
  • 使用 split 方法

    function getParams(url) {
      const res = {}
      if (url.includes('?')) {
        const str = url.split('?')[1]
        const arr = str.split('&')
        arr.forEach(item => {
          const key = item.split('=')[0]
          const val = item.split('=')[1]
          res[key] = decodeURIComponent(val) // 解码
        })
      }
      return res
    }
    
    // 测试
    const user = getParams('http://www.baidu.com?user=%E9%98%BF%E9%A3%9E&age=16')
    console.log(user) // { user: '阿飞', age: '16' }
    

20. 手写 Promise

// Promise 实现

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

function Promise(executor) {
  // 默认属性
  this.PromiseState = PENDING;
  this.PromiseResult = null;
  // 保存回调
  this.callbacks = [];

  // 保存示例的 this
  const self = this;

  // resolve 函数
  function resolve(data) {
    if (self.PromiseState !== PENDING) {
      return;
    }

    // 1. 设置对象状态为 fulfilled
    self.PromiseState = FULFILLED;
    // 2. 设置对象结果值
    self.PromiseResult = data;

    // 调用成功的回调
    self.callbacks.forEach((callback) => {
      setTimeout(() => {
        callback.onResolved(data);
      });
    });
  }

  function reject(data) {
    if (self.PromiseState !== PENDING) {
      return;
    }

    // 1. 设置对象状态为 rejected
    self.PromiseState = REJECTED;
    // 2. 设置对象结果值
    self.PromiseResult = data;

    // 调用失败的回调
    self.callbacks.forEach((callback) => {
      setTimeout(() => {
        callback.onRejected(data);
      });
    });
  }

  try {
    // 同步调用 执行器函数
    executor(resolve, reject);
  } catch (e) {
    // 修改 promise 状态为 rejected
    reject(e);
  }
}

// 添加 then 方法
Promise.prototype.then = function (onResolved, onRejected) {
  if (typeof onResolved !== "function") {
    onResolved = (value) => value;
  }
  if (typeof onRejected !== "function") {
    onRejected = (reason) => {
      throw reason;
    };
  }

  let self = this;
  return new Promise((resolve, reject) => {
    function callback(type) {
      try {
        let result = type(self.PromiseResult);
        if (result instanceof Promise) {
          result.then(
            (v) => {
              resolve(v);
            },
            (r) => {
              reject(r);
            }
          );
        } else {
          resolve(result);
        }
      } catch (e) {
        reject(e);
      }
    }

    // 调用回调函数
    if (this.PromiseState === FULFILLED) {
      setTimeout(() => {
        callback(onResolved);
      });
    }
    if (this.PromiseState === REJECTED) {
      setTimeout(() => {
        callback(onRejected);
      });
    }

    // 如果当前为 PENGDING 状态
    if (this.PromiseState === PENDING) {
      // 保存回调函数
      this.callbacks.push({
        onResolved: function () {
          callback(onResolved);
        },
        onRejected: function () {
          callback(onRejected);
        },
      });
    }
  });
};

// 添加 catch 方法
Promise.prototype.catch = function (onRejected) {
  return this.then(undefined, onRejected);
};

// finally
Promise.prototype.finally = function (fn) {
  return this.then(
    function (value) {
      return Promise.resolve(fn()).then(function () {
        return value;
      });
    },
    function (error) {
      return Promise.resolve(fn()).then(function () {
        throw error;
      });
    }
  );
};

Promise.resolve = function (value) {
  return new Promise((resolve, reject) => {
    if (value instanceof Promise) {
      value.then(
        (v) => {
          resolve(v);
        },
        (r) => {
          reject(r);
        }
      );
    } else {
      resolve(value);
    }
  });
};

Promise.reject = function (reason) {
  return new Promise((resolve, reject) => {
    reject(reason);
  });
};

Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    let count = 0,
      arr = [];
    for (let i = 0; i < promises.length; i++) {
      promises[i].then(
        (v) => {
          count++;
          arr[i] = v;
          if (count === promises.length) {
            resolve(arr);
          }
        },
        (r) => {
          reject(r);
        }
      );
    }
  });
};

Promise.race = function (promises = []) {
  return new Promise((resolve, reject) => {
    for (let i = 0; i < promises.length; i++) {
      promises[i].then(
        (v) => {
          resolve(v);
        },
        (r) => {
          reject(r);
        }
      );
    }
  });
};

Promise.any = function (promises = []) {
  return new Promise((resolve, reject) => {
    let count = 0;
    for (let i = 0; i < promises.length; i++) {
      promises[i].then(
        (v) => {
          resolve(v);
        },
        (r) => {
          count++;
          if (count === promises.length) {
            console.log("promise any:", r);
            // reject(r);
            reject(
              new AggregateError("No promise in Promise.any was resolved")
            );
          }
        }
      );
    }
  });
};

21. JS 事件循环

JS事件循环

  • 主线程

    所有的同步任务都是在主线程里执行的,异步任务可能会在macrotask或者microtask里面

    • 同步任务 指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
    • 异步任务 指的是不进入主线程,某个异步任务可以执行了,该任务才会进入主线程执行
  • 微任务 micro task

    • promise
    • async
    • await
    • process.nextTick(node)
    • mutationObserver
  • 宏任务 marco task

    • script 整体代码
    • setTimeout
    • setInterval
    • setImmediate
    • I/O
    • UI render
  • 大致流程

    简单的说,事件循环(eventLoop)是单线程的JavaScript在处理异步事件时进行的一种循环过程,具体来讲,对于异步事件它会先加入到事件队列中挂起,等主线程空闲时会去执行事件队列中的事件。

    • 主线程任务——>微任务——>宏任务 如果宏任务里还有微任务就继续执行宏任务里的微任务,如果宏任务中的微任务中还有宏任务就在依次进行
    • 主线程任务——>微任务——>宏任务——>宏任务里的微任务——>宏任务里的微任务中的宏任务——>直到任务全部完成 我的理解是在同级下,微任务要优先于宏任务执行
    • 在同一轮任务队列中,同一个微任务产生的微任务会放在这一轮微任务的后面,产生的宏任务会放在这一轮的宏任务后面

    • 在同一轮任务队列中,同一个宏任务产生的微任务会马上执行,产生的宏任务会放在这一轮的宏任务后面

总结:

  1. 微任务队列优先于宏任务队列执行;

  2. 微任务队列上创建的宏任务会被后添加到当前宏任务队列的尾端;

  3. 微任务队列中创建的微任务会被添加到微任务队列的尾端;

  4. 只要微任务队列中还有任务,宏任务队列就只会等待微任务队列执行完毕后再执行;

  5. 只有运行完 await 语句,才把 await 语句后面的全部代码加入到微任务行列;

  6. 在遇到 await promise时,必须等 await promise 函数执行完毕才能对await语句后面的全部代码加入到微任务中;

    • 在等待 await Promise.then 微任务时:

      • 运行其他同步代码;

      • 等到同步代码运行完,开始运行 await promise.then 微任务;

      • await promise.then 微任务完成后,把await语句后面的全部代码加入到微任务行列;

18. Vue React Angular 对比

18.1 Vue

vue的优点包括

18.1.1 响应式

响应式的系统可谓相当方便,也就是基于是数据可变的,通过对每一个属性建立Watcher来监听,当属性变化的时候,响应式的更新对应的虚拟dom。具体原理不在赘述,直接上图:

vue响应式的特点

18.1.2 模板语法

在vue中,我们就行在写html一样自由畅快,这就得利于vue的模板语法

{{message}}

首先我们说vue的模板其实不是html,那么既然不是html他一定是被转换成了一个js代码,就是所谓的模板编译,在vue 中使用的就是vue-template-compiler这个模板编译工具, 那这个工具做了什么呢?其实就是将 Vue 2.0 模板预编译为渲染函数(template => ast => render)

//模板代码
const compiler = require('vue-template-compiler')
const result = compiler.compile(`
  

This is my vue render test

my name is {{myName}}

` ) console.log(result)
{
//编译结果
  ast: {
    type: 1,
    tag: 'div',
    attrsList: [ [Object] ],
    attrsMap: { id: 'test' },
    rawAttrsMap: {},
    parent: undefined,
    children: [ [Object], [Object], [Object] ],
    plain: false,
    attrs: [ [Object] ],
    static: false,
    staticRoot: false
  },
  render: `with(this){return _c('div',{attrs:{"id":"test"}},[
        _m(0),          // 上述提到的静态子树,索引为0 

This is my vue render test

_v(" "), // 空白节点

之间的换行内容 _c('p',[_v("my name is "+_s(myName))]) //

my name is {{myName}}

])}`, staticRenderFns: [ `with(this){return _c('div',[_c('p',[_v("This is my vue render test")])])}` ], errors: [], tips: [] }

然后我们执行这个render函数,就会返回一个vnode,接下来就是我们熟悉的diff算法,到更新视图

18.1.3 实现 MVVM

实现了MVVM我至少认为这是一个福音,用数据去驱动视图,简化繁琐的操作dom,更关注与业务逻辑,这样就能节省开发时间,和写出易于维护的代码,那么什么叫mvvm呢?

MVVM

View, 和model很好理解,那么viewmodel到底是什么呢?他是一个抽象的东西,在vue 中viewmodel其实就是比如我们的方法,我们的事件,等等,都是这一层,这一层的操作,帮助改变数据,或者改变视图,起到承上启下的作用,也实现了,vue非常经典的数据双向绑定

18.1.4 使用友好,上手简单

这是vue最为亮眼的一点,比起react的jsx,比起angular的rxjs,以及各种依赖注入,vue 的语法风格,和使用api相当友好,社区也都有都一些相对固定的方案,当然,还有一点比较幸福的是vue的性能优化做的相当到位,不用我们手动优化,不用像react那样父组件更新render子组件也要跟新,到底还是中国人写的,有那么点中庸之道的意思,把简单和选择留给别人,复杂和无奈留给自己……

18.1.5 弱化性能优化,用户只需关注逻辑

相比与react和angular vue在按照模板格式开发时就没有太多限制,得益于响应式系统,也不用去做相对应的性能优化,由于vue 内部的模板语法,能在开发环境下编译,这样就能在编译的时候去做一些性能优化比如,在这段vue我明确知道第二个name是变量这样在内部就会有针对性的优化


而在react 中,由于jsx的写法灵活,不确定性多,所以在编译时就无法去做性能优化,而性能优化放到了我么开发者身上

    { data.map((name, i) =>
  • {i !== 1 ? i : name}
  • ) }

18.2 React

18.2.1 不可变的设计思想,也是函数式编程的实践

我们知道,在react中性能优化的权利交给了开发者,所以,在默认情况下当react 的父组件更新时,子组件必然更新,这样,就需要我们使用react 提供的一些优化手段,比如scu,比如purecomponent和memo等,来进行相关的性能优化,我们知道 ShouldComponentUpdate(SCU)可以用来阻断更新的蔓延。但是React组件默认并没有实现它,因而需要开发者来判断是否需要对应用进行性能优化。在这个方法中,你可以检测组件的state或props是否被修改,如果没有修改那么将返回false,那么这个组件和它的子组件都不会被重新渲染。问题在于这里:如果数据是可变的,并且被直接修改了,SCU会一直返回false,这就导致组件永远不被重新渲染,在设计之初,这个不可变值的的理念就贯穿react,甚至还为不可变值专门耗时两年开发了Immutable.js 这个js的库,这也是函数式编程的理念。

18.2.2 all in js

在react中一切皆js,其实所谓的jsx也只是一个语法糖,正是由于这样的开发体验,许多开发者会有种写的更爽的感觉,他没有vue中各种模板语法的限制,编写自由度高,他注重更更大的颗粒度拆分,推重组件化的设计思想,在react 中一切皆是组件,一个大型的项目都是有一个个组件仿佛积木一般堆积而成

18.2.3 活跃的社区,繁华的生态

这一点是react 最为骄傲的一点,他清楚自己应该做什么,剩下都交给了社区去做,他不像vue一样,生态社区相对稳定,基本上一套下来整个社区都认可一些相对固定的方案,而在react中可谓百花齐放,比如在公共数据状态管理就有redux,mobx,还有最近出来的concent 的也是让人耳目一新,比如css 也是不可开交,前有sass,less,后又css in js 的style-components,在比如,一个可以实现在redux中实现异步的中间件就有redux-thunk,和redux-saga等等,更别提在react基础上出现的dvajs,这样一个用于代替redux的库,以及umi的出现,目的是为了代替cra,总之有选择困难症的同学用react可要慎重,因为,选择真的太多了,你会无从下手。正是由于react这开放的思维方式(对应中国人的vue中庸),各种轮子层出不穷,大家搞研究的热情不减,前端也越来越重要,工资也越来越高。我觉得好多大厂之所以选择react正是因为react折腾起来顺手,都是技术大牛,谁不想没事搞搞大新闻,是吧!

18.2.4 多端发展跨平台的能力

由于RN的加持,搞定react的语法之后,你甚至就可以去手撸一个app,这里vue 是做不到的 ,也正是因为这样,你会发现,在react 中的事件是自己写的,而不是用的浏览器原生的,就是因为他需要用自己写的这一套合成事件的机制来实现跨端事件,原理其实很简单

react事件机制

18.2.4 Fiber(时间分片)

react16中新增了Fiber(时间分片),这种渲染机制,他将Reconcilation阶段的人物分成一个个执行单元,也就是所谓的fiber,然后利用requestIdleCallback这个方法去在主进程空闲的状态下执行一个个fiber,原理如下:

fiber原理

如此一来,大致解决了由于react渲染时由于diff计算过长占用主进程所带来的卡顿,以及掉帧问题。

18.3 Angular

18.3.1 提供了完成的端到端的解决方案

之前之所以说angular 又大又全,是因为,他不需要借助社区的力量,仅仅靠自己就能实现一个产品从开始到上线的解决方案,比如,他提供完备的单元测试方案,这里你在新建组件的时候都会给你一个测试的ts,在比如,他会提供开发时所需的所有服务,比如http服务,比如router服务,比如公共数据管理服务service,这些angular 全部完备,他更像是一个后端语言,严格的类型检查,拥抱ts,强制性分层,以及更好的工程,面向对象编程的思想。angular很高大全同时也很复杂,angular官方库给了你一切你想要的,使用angular技术栈,基本上你不用对组件选型浪费太多时间精力(对比react)。

18.4 三大框架的联系

18.4.1 组件化

三大框架中,都是提倡组件化开发的框架,所以,比如当你在开发vue的项目时,具备组件化的思想,在平常开发中能够有意识的去抽离展示型组件,和容器型组件,那么,在开发react 和angular时,你只会得心应手,不会处处为难

18.4.2 都是数据驱动视图

三大框架,都是属于mvvm框架,数据驱动视图的思想始终贯穿始终,所以我们在开发时,只需要关注数据变化即可,虽然使用方式不尽相同,react 属于函数式,angular 和vue 属于声明式编程,但是,数据驱动的思想不变,比如react 使用setState赋值,而vue 和angular 使用显式直接赋值他们最后的目的都是改值,从而触发页面更新

18.4.3 共同的开发套路

在三大框架中,由于都遵循组件化思想,所以,都有着相同的开发套路,只是使用方式略有差别,比如,都有父子组件传递,都有数据管理框架,都有前端路由,都有插槽,只不过在anguar中叫做投影组件,在react 中叫组合,都能实现一些所谓的高阶(hoc)函数

18.5 区别

18.5.1 模板 vs JSX

React与Vue,angular最大的不同是模板的编写。Vue和angulr鼓励你去写近似常规HTML的模板。写起来很接近标准HTML元素,只是多了一些属性。而react则崇尚all in js 所以独创jsx的编写风格,但是他们本质上都是一个语法糖,编译之后都是一个可执行的函数,

18.5.2 Virtual DOM vs Incremental DOM

在底层渲染方面,vue 和react 都使用的虚拟dom,而angular 却没有使用,使用的是Incremental DOM

18.5.3 函数式编程,声明式编程

在react中不可变值贯穿整个框架,如果你要改变数据,那么必须调用api去改,这就是函数式编程的思想,而在vue,和angualr中,声明式编程的思想也深入人心,既方便有快捷。这种直接赋值的方式,和用api赋值的方式,其实,只是用法不同而已,本质还是数据驱动

18.5.4 社区复杂度

在angular 和vue 中,几乎给你想要的全部给你了,而react 追求的更多的是自力更生,所以,有选择困难症的人才会如此的纠结。

18.5.5 入门难度

angular 工程化最好的框架,,也是由于拥抱rxjs,和ts(到现在ts的泛型我都搞不明白),也是最难的框架

react 上手也也比较简单,尤其是16引入hooks之后,号称api终结者,但是由于社区太过活跃各种新东西层出不穷,中文文档晦涩难懂,导致相对于vue上手也有难度

vue由于是中国人写的,有着完善的中文文档,和稳定的社区,并且有着亲切的模板语法,应当说是入门相对来说是最简单的了。