js引用类型引发的问题

前言

背景javascript

  • 被问道的引用问题
    • 1 一次会议问道的引用类型问题,同窗想使用惰性加载,但愿在网页中fetch获得的数据保存下了,若是某些不改变参数,就不在发起请求,前提是会对fetch返回的数据进行修改,但还有使用以前的fetchdata
    if(window.fetchData&&window.fetchDataSomeKeyLength=== window.fetchData.length) return
    const data = fetch(`${url}`
    window.fetchData = data
    window.fetchDataSomeKeyLength=data.someKey.length
    processData(data)//data.someKey的length作了处理
    复制代码

  • 要get的点
    • 1 js基本类型和引用类型的区别
    • 2 栈存储和堆存的区别
    • 3 js垃圾回收机制
    • 4 活动对象、执行上下文、this
    • 5 闭包的造成
    • 6 深copy的实现

基本类型和引用类型的区别

6种基本类型

  • string
  • number
  • bool
  • null
  • undefined
  • symbol

通俗易懂的话来说,js的基本类型使用用来存储值得,它们分配大小是有限度 在定义基本类型变量的时候它们的内存都被分配完成,前端

  • 数字有最大值和最小值
  • null undefined的是固定的值
  • bool 值为 true和false

stringnumberbooleansymbol 这四种类型统称为原始类型(Primitive) ,表示不能再细分下去的基本类型;symbol 表示独一无二的值,经过 Symbol 函数调用生成,因为生成的 symbol 值为原始类型,因此 Symbol 函数不能使用 new 调用;nullundefined 一般被认为是特殊值,这两种类型的值惟一,就是其自己。java

引用类型

  • 对象es6

  • 数组web

  • 函数面试

和基本类型区分开来。对象在逻辑上是属性的无序集合或者有序集合,是存放各类值的容器。对象值存储的是引用地址,因此和基本类型值不可变的特性不一样,对象值是可变的。算法

包装对象

咱们知道对象拥有属性和方法。但好比字符串这种基本类型值不属于对象为何还拥有属性和方法呢?

实际上在引用字符串的属性或方法时,会经过调用 new String() 的方式转换成对象,该对象继承了字符串的方法来处理属性的引用,一旦引用结束,便会销毁这个临时对象,这就是包装对象的概念。编程

不只仅只是字符串有包装对象的概念,数字和布尔值也有相对应的 new Number()new Boolean() 包装对象。nullundefined 没有包装对象,访问它们的属性会报类型错误。数组

字符串、数字和布尔值经过构造函数显式生成的包装对象,既然属于对象,和基本类型的值必然是有区别的,这点能够经过 typeof 检测出来。安全

typeof 'seymoe'                 // 'string'
typeof new String('seymoe')     // 'object'

复制代码

数据类型的判断

  • typeof

  • instanceof

  • Object.prototype.toString()

    typeof
    `typeof` 操做符来判断一个值属于哪一种基本类型,返回值是一个string,对null判断有误,认为null是个空指针
    typeof 'seymoe' // 'string' 
    typeof true // 'boolean' 
    typeof 10 // 'number' 
    typeof Symbol() // 'symbol' 
    typeof null // 'object' 
    没法断定是否为 null 
    typeof undefined // 'undefined'
    复制代码

    若是使用 typeof 操做符对对象类型及其子类型,譬如函数(可调用对象)、数组(有序索引对象)等进行断定,则除了函数都会获得 object 的结果。

    typeof {} // 'object'
    typeof [] // 'object'
    typeof(() => {})// 'function'
    复制代码

    因为没法得知一个值究竟是数组仍是普通对象,显然经过 typeof 判断具体的对象子类型远远不够。

    instanceof
    经过 `instanceof` 操做符也能够对对象类型链上的构造函数进行断定,其原理就是测试构造函数的 `prototype` 是否出如今被检测对象的原型链上。 ``` [] instanceof Array // true ({}) instanceof Object // true (()=>{}) instanceof Function // true ```

    注意:instanceof 也不是万能的。其原理就是测试构造函数

    var a={}
     a.__proto__=[]
     a instanceof Array //true
     a instanceof Object //true
    
    复制代码
    Object.prototype.toString()
    `Object.prototype.toString()` 能够说是断定 JavaScript 中数据类型的终极解决方法了,具体用法请看如下代码:
    Object.prototype.toString.call({})            // '[object Object]'
     Object.prototype.toString.call([])              // '[object Array]'
     Object.prototype.toString.call(() => {})        // '[object Function]'
     Object.prototype.toString.call('seymoe')        // '[object String]'
     Object.prototype.toString.call(1)               // '[object Number]'
     Object.prototype.toString.call(true)            // '[object Boolean]'
     Object.prototype.toString.call(Symbol())        // '[object Symbol]'
     Object.prototype.toString.call(null)            // '[object Null]'
     Object.prototype.toString.call(undefined)       // '[object Undefined]'
    
     Object.prototype.toString.call(new Date())      // '[object Date]'
     Object.prototype.toString.call(Math)            // '[object Math]'
     Object.prototype.toString.call(new Set())       // '[object Set]'
     Object.prototype.toString.call(new WeakSet())   // '[object WeakSet]'
     Object.prototype.toString.call(new Map())       // '[object Map]'
     Object.prototype.toString.call(new WeakMap())   // '[object WeakMap]'
    复制代码

数据类型转换

ToPrimitive
JavaScript 对象转换到基本类型值时,会使用 ToPrimitive 算法,这是一个内部算法,是编程语言在内部执行时遵循的一套规则。

ToPrimitive 算法在执行时,会被传递一个参数 hint,表示这是一个什么类型的运算(也能够叫运算的指望值),根据这个 hint 参数,ToPrimitive 算法来决定内部的执行逻辑。

hint 参数的取值只能是下列 3 者之一:

  • string
  • number
  • default
转换算法
当对象与到基本类型值发生转换时,会按照下面的逻辑调用对象上的方法: **为了进行转换,JavaScript 会尝试查找并调用三个对象方法:**
  1. 调用obj[Symbol.toPrimitive](hint)- 带有符号键Symbol.toPrimitive(系统符号)的方法,若是存在这样的方法,

  2. 不然若是提示是 "string"

    • 尝试obj.toString()obj.valueOf(),不管存在什么。
  3. 不然,若是提示是"number""default"

    • 尝试obj.valueOf()obj.toString(),不管存在什么。
肯定 hint

咱们提到了 ToPrimitive 算法中用到的 hint 参数,那怎样肯定一次运算场景下的 hint 取值是什么呢?很简单----新建一个对象,打印各个运算场景下的 hint 值:

let obj = {
  name: "John",
  money: 1000,

  [Symbol.toPrimitive](hint) {
    console.log(`hint: ${hint}`);
  }
};

alert(obj) // hint: string 
+obj // hint: number
obj + 500 // hint: default



// 一个没有提供 Symbol.toPrimitive 属性的对象,参与运算时的输出结果
var obj1 = {};
console.log(+obj1);     // NaN
console.log(`${obj1}`); // "[object Object]"
console.log(obj1 + ""); // "[object Object]"

// 接下面声明一个对象,手动赋予了 Symbol.toPrimitive 属性,再来查看输出结果
var obj2 = {
  [Symbol.toPrimitive](hint) {
    if (hint == "number") {
      return 10;
    }
    if (hint == "string") {
      return "hello";
    }
    return true;
  }
};
console.log(+obj2);     // 10      -- hint 参数值是 "number"
console.log(`${obj2}`); // "hello" -- hint 参数值是 "string"
console.log(obj2 + ""); // "true"  -- hint 参数值是 "default"
复制代码
## Symbol.toPrimitive 和 toString/valueOf 方法
并不要求 `Symbol.toPrimitive` 和 `toString/valueOf` 方法必须返回 `hint` 参数值所暗示的类型值。

但要注意下面两点:

  1. Symbol.toPrimitivetoString 方法的返回值必须是基本类型值。
  2. valueOf 方法除了能够返回基本类型值,也能够返回其余类型值。

当咱们建立一个普通对象时({}new Object() 的方式等),对象上是不具有 [Symbol.toPrimitive] (方法)属性的。因此,对于普通对象的到基本类型值的运算,通常按照具体场景:

  1. hint 值为 "string" 时,先调用 toStringtoString 若是返回一个基本类型值了,则返回、终止运算;不然接着调用 valueOf 方法。
  2. 不然,先调用 valueOfvalueOf 若是返回一个基本类型值了,则返回、终止运算;不然接着调用 toString 方法。

2 栈存储和堆存的区别

栈数据结构

栈是一种特殊的列表,栈内的元素只能经过列表的一端访问,这一端称为栈顶。 栈被称为是一种后入先出(LIFO,last-in-first-out)的数据结构。 因为栈具备后入先出的特色,因此任何不在栈顶的元素都没法访问。 为了获得栈底的元素,必须先拿掉上面的元素。

在这里,为方便理解,经过类比乒乓球盒子来分析栈的存取方式。

16b8c0af7dd2aa15.png

这种乒乓球的存放方式与栈中存取数据的方式一模一样。 处于盒子中最顶层的乒乓球 5,它必定是最后被放进去,但能够最早被使用。 而咱们想要使用底层的乒乓球 1,就必须将上面的 4 个乒乓球取出来,让乒乓球1处于盒子顶层。 这就是栈空间先进后出,后进先出的特色。

堆数据结构
堆是一种通过排序的树形数据结构,每一个结点都有一个值。 一般咱们所说的堆的数据结构,是指二叉堆。 堆的特色是根结点的值最小(或最大),且根结点的两个子树也是一个堆。 因为堆的这个特性,经常使用来实现优先队列,堆的存取是随意,这就如同咱们在图书馆的书架上取书, 虽然书的摆放是有顺序的,可是咱们想取任意一本时没必要像栈同样,先取出前面全部的书, 咱们只须要关心书的名字。
变量类型与内存的关系

基本数据类型保存在栈内存中,由于基本数据类型占用空间小、大小固定,经过按值来访问,属于被频繁使用的数据。 为了更好的搞懂基本数据类型变量与栈内存,咱们结合如下例子与图解进行理解:

let num1 = 1; 
let num2 = 1;
复制代码

16b8c0b2fba2bdef.png

引用数据类型存储在堆内存中,由于引用数据类型占据空间大、大小不固定。 若是存储在栈中,将会影响程序运行的性能; 引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。 当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中得到实体

// 基本数据类型-栈内存
let a1 = 0;
// 基本数据类型-栈内存
let a2 = 'this is string';
// 基本数据类型-栈内存
let a3 = null;

// 对象的指针存放在栈内存中,指针指向的对象存放在堆内存中
let b = { m: 20 };
// 数组的指针存放在栈内存中,指针指向的数组存放在堆内存中
let c = [1, 2, 3];

复制代码

16b8c0b5752823f6.png 所以当咱们要访问堆内存中的引用数据类型时,实际上咱们首先是从变量中获取了该对象的地址指针, 而后再从堆内存中取得咱们须要的数据。

从内存角度来看变量复制
let a = 20;
let b = a;
b = 30;
console.log(a); // 此时a的值是50

复制代码

在这个例子中,a、b 都是基本类型,它们的值是存储在栈内存中的,a、b 分别有各自独立的栈空间, 因此修改了 b 的值之后,a 的值并不会发生变化。

16b8c0b73d4ebd08.png

引用数据类型的复制
let m = { a: 10, b: 20 };
let n = m;
n.a = 15;
console.log(m.a) //此时m.a的值是多少,是10?仍是15?

复制代码

在这个例子中,m、n都是引用类型,栈内存中存放地址指向堆内存中的对象, 引用类型的复制会为新的变量自动分配一个新的值保存在变量中, 但只是引用类型的一个地址指针而已,实际指向的是同一个对象, 因此修改 n.a 的值后,相应的 m.a 也就发生了改变。

16b8c0b9df03d885.png

栈内存和堆内存的优缺点

在JS中,基本数据类型变量大小固定,而且操做简单容易,因此把它们放入栈中存储。 引用类型变量大小不固定,因此把它们分配给堆中,让他们申请空间的时候本身肯定大小,这样把它们分开存储可以使得程序运行起来占用的内存最小。

栈内存因为它的特色,因此它的系统效率较高。 堆内存须要分配空间和地址,还要把地址存到栈中,因此效率低于栈。

3 js垃圾回收机制

为何要有垃圾回收

在C语言和C++语言中,咱们若是想要开辟一块堆内存的话,须要先计算须要内存的大小,而后本身经过malloc函数去手动分配,在用完以后,还要时刻记得用free函数去清理释放,不然这块内存就会被永久占用,形成内存泄露。

可是咱们在写JavaScript的时候,却没有这个过程,由于人家已经替咱们封装好了,V8引擎会根据你当前定义对象的大小去自动申请分配内存。

不须要咱们去手动管理内存了,因此天然要有垃圾回收,不然的话只分配不回收,岂不是没多长时间内存就被占满了吗,致使应用崩溃。

垃圾回收的好处是不须要咱们去管理内存,把更多的精力放在实现复杂应用上,但坏处也来自于此,不用管理了,就有可能在写代码的时候不注意,形成循环引用等状况,致使内存泄露。

垃圾回收机制
标记清除

当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,由于只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。

可使用任何方式来标记变量。好比,能够经过翻转某个特殊的位来记录一个变量什么时候进入环境,或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪一个变量发生了变化。如何标记变量并不重要,关键在于采起什么策略。

  • (1)垃圾收集器在运行的时候会给存储在内存中的全部变量都加上标记(固然,可使用任何标记方式)。
  • (2)而后,它会去掉运行环境中的变量以及被环境中变量所引用的变量的标记
  • (3)此后,依然有标记的变量就被视为准备删除的变量,缘由是在运行环境中已经没法访问到这些变量了。
  • (4)最后,垃圾收集器完成内存清除工做,销毁那些带标记的值并回收它们所占用的内存空间。

目前,IE、Firefox、Opera、Chrome和Safari的JavaScript实现使用的都是标记清除式的垃圾回收策略(或相似的策略),只不过垃圾收集的时间间隔互有不一样。

引用计数
引用计数的垃圾收集策略不太常见。含义是跟踪记录每一个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。

若是同一个值又被赋给另外一个变量,则该值的引用次数加1。相反,若是包含对这个值引用的变量改变了引用对象,则该值引用次数减1。

当这个值的引用次数变成0时,则说明没有办法再访问这个值了,于是就能够将其占用的内存空间回收回来。

这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。

循环引用是指对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用,看个例子:
复制代码
function foo () {
    var objA = new Object();
    var objB = new Object();
    
    objA.otherObj = objB;
    objB.anotherObj = objA;
}

复制代码

这个例子中,objA和objB经过各自的属性相互引用,也就是说,这两个对象的引用次数都是2。

在采用标记清除策略的实现中,因为函数执行后,这两个对象都离开了做用域,所以这种相互引用不是问题。

但在采用引用次数策略的实现中,当函数执行完毕后,objA和objB还将继续存在,由于它们的引用次数永远不会是0。加入这个函数被重复屡次调用,就会致使大量内存没法回收

还要注意的是,咱们大部分人时刻都在写着循环引用的代码,看下面这个例子,相信你们都这样写过:

var el = document.getElementById('#el');
el.onclick = function (event) {
    console.log('element was clicked');
}

复制代码

咱们为一个元素的点击事件绑定了一个匿名函数,咱们经过event参数是能够拿到相应元素el的信息的。

你们想一想,这是否是就是一个循环引用呢? el有一个属性onclick引用了一个函数(其实也是个对象),函数里面的参数又引用了el,这样el的引用次数一直是2,即便当前这个页面关闭了,也没法进行垃圾回收。

若是这样的写法不少不少,就会形成内存泄露。咱们能够经过在页面卸载时清除事件引用,这样就能够被回收了

var el = document.getElementById('#el');
el.onclick = function (event) {
    console.log('element was clicked');
}

// ...
// ...

// 页面卸载时将绑定的事件清空
window.onbeforeunload = function(){
    el.onclick = null;
}

复制代码
V8垃圾回收策略
自动垃圾回收有不少算法,因为不一样对象的生存周期不一样,因此没法只用一种回收策略来解决问题,这样效率会很低。

因此,V8采用了一种代回收的策略,将内存分为两个生代:新生代(new generation)老生代(old generation)

新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象,分别对新老生代采用不一样的垃圾回收算法来提升效率,对象最开始都会先被分配到新生代(若是新生代内存空间不够,直接分配到老生代),新生代中的对象会在知足某些条件后,被移动到老生代,这个过程也叫晋升,后面我会详细说明。

分代内存

默认状况下,32位系统新生代内存大小为16MB,老生代内存大小为700MB,64位系统下,新生代内存大小为32MB,老生代内存大小为1.4GB。

新生代平均分红两块相等的内存空间,叫作semispace,每块内存大小8MB(32位)或16MB(64位)。

分配方式

新生代存的都是生存周期短的对象,分配内存也很容易,只保存一个指向内存空间的指针,根据分配对象的大小递增指针就能够了,当存储空间快要满时,就进行一次垃圾回收。

算法

新生代采用Scavenge垃圾回收算法,在算法实现时主要采用Cheney算法。 Cheney算法将内存一分为二,叫作semispace,一块处于使用状态,一块处于闲置状态。

162c3526b85b16a7.png

处于使用状态的semispace称为From空间,处于闲置状态的semispace称为To空间

接下来我会结合流程图来详细说明Cheney算法是怎么工做的。 垃圾回收在下面我统称为 GC(Garbage Collection)step1. 在From空间中分配了3个对象A、B、C

162c3526d601da9e.png

step2. GC进来判断对象B没有其余引用,能够回收,对象A和C依然为活跃对象

162c3526ee73c6b5.png

step3. 将活跃对象A、C从From空间复制到To空间

162c3526f003cd95.png

step4. 清空From空间的所有内存

162c3527027f9a35.png

step5. 交换From空间和To空间

162c352706984982.png

step6. 在From空间中又新增了2个对象D、E

162c3527047d8e26.png

step7. 下一轮GC进来发现对象D没有引用了,作标记

162c3527073d80a9.png

step8. 将活跃对象A、C、E从From空间复制到To空间

162c3527099bae4b.png

step9. 清空From空间所有内存

162c35270c3a80b6.webp

step10. 继续交换From空间和To空间,开始下一轮

162c35271dd2cfd7.png

经过上面的流程图,咱们能够很清楚的看到,进行From和To交换,就是为了让活跃对象始终保持在一块semispace中,另外一块semispace始终保持空闲的状态。

Scavenge因为只复制存活的对象,而且对于生命周期短的场景存活对象只占少部分,因此它在时间效率上有优异的体现。Scavenge的缺点是只能使用堆内存的一半,这是由划分空间和复制机制所决定的。

因为Scavenge是典型的牺牲空间换取时间的算法,因此没法大规模的应用到全部的垃圾回收中。但咱们能够看到,Scavenge很是适合应用在新生代中,由于新生代中对象的生命周期较短,偏偏适合这个算法。

晋升
当一个对象通过屡次复制仍然存活时,它就会被认为是生命周期较长的对象。这种较长生命周期的对象随后会被移动到老生代中,采用新的算法进行管理。

对象重新生代移动到老生代的过程叫做晋升

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

  1. 对象从From空间复制到To空间时,会检查它的内存地址来判断这个对象是否已经经历过一次Scavenge回收。若是已经经历过了,会将该对象从From空间移动到老生代空间中,若是没有,则复制到To空间。总结来讲,若是一个对象是第二次经历从From空间复制到To空间,那么这个对象会被移动到老生代中
  2. 当要从From空间复制一个对象到To空间时,若是To空间已经使用了超过25%,则这个对象直接晋升到老生代中。设置25%这个阈值的缘由是当此次Scavenge回收完成后,这个To空间会变为From空间,接下来的内存分配将在这个空间中进行。若是占比太高,会影响后续的内存分配
老生代
在老生代中,存活对象占较大比重,若是继续采用Scavenge算法进行管理,就会存在两个问题:
  1. 因为存活对象较多,复制存活对象的效率会很低。
  2. 采用Scavenge算法会浪费一半内存,因为老生代所占堆内存远大于新生代,因此浪费会很严重。

因此,V8在老生代中主要采用了Mark-SweepMark-Sweep相结合的方式进行垃圾回收。

Mark-Sweep
Mark-Sweep是标记清除的意思,它分为标记和清除两个阶段。

与Scavenge不一样,Mark-Sweep并不会将内存分为两份,因此不存在浪费一半空间的行为。Mark-Sweep在标记阶段遍历堆内存中的全部对象,并标记活着的对象,在随后的清除阶段,只清除没有被标记的对象。

也就是说,Scavenge只复制活着的对象,而Mark-Sweep只清除死了的对象。活对象在新生代中只占较少部分,死对象在老生代中只占较少部分,这就是两种回收方式都能高效处理的缘由。

step1. 老生代中有对象A、B、C、D、E、F 162c35271e20f9ab.png

step2. GC进入标记阶段,将A、C、E标记为存活对象

162c3527204267ca.png step3. GC进入清除阶段,回收掉死亡的B、D、F对象所占用的内存空间

162c3527267e7eae.png

能够看到,Mark-Sweep最大的问题就是,在进行一次清除回收之后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配形成问题。

若是出现须要分配一个大内存的状况,因为剩余的碎片空间不足以完成这次分配,就会提早触发垃圾回收,而此次回收是没必要要的。

Mark-Compact
为了解决Mark-Sweep的内存碎片问题,Mark-Compact就被提出来了。

**Mark-Compact是标记整理的意思,**是在Mark-Sweep的基础上演变而来的。Mark-Compact在标记完存活对象之后,会将活着的对象向内存空间的一端移动,移动完成后,直接清理掉边界外的全部内存。以下图所示: step1. 老生代中有对象A、B、C、D、E、F(和Mark—Sweep同样)

162c3527267a55b2.png step2. GC进入标记阶段,将A、C、E标记为存活对象(和Mark—Sweep同样)

162c3527204267ca.png step3. GC进入整理阶段,将全部存活对象向内存空间的一侧移动,灰色部分为移动后空出来的空间

162c35272976bf46.png step4. GC进入清除阶段,将边界另外一侧的内存一次性所有回收

162c352731840c87.png

二者结合

在V8的回收策略中,Mark-Sweep和Mark-Conpact二者是结合使用的。

因为Mark-Conpact须要移动对象,因此它的执行速度不可能很快,在取舍上,V8主要使用Mark-Sweep,在空间不足以对重新生代中晋升过来的对象进行分配时,才使用Mark-Compact。

总结

V8的垃圾回收机制分为新生代和老生代。

新生代主要使用Scavenge进行管理,主要实现是Cheney算法,将内存平均分为两块,使用空间叫From,闲置空间叫To,新对象都先分配到From空间中,在空间快要占满时将存活对象复制到To空间中,而后清空From的内存空间,此时,调换From空间和To空间,继续进行内存分配,当知足那两个条件时对象会重新生代晋升到老生代。

老生代主要采用Mark-Sweep和Mark-Compact算法,一个是标记清除,一个是标记整理。二者不一样的地方是,Mark-Sweep在垃圾回收后会产生碎片内存,而Mark-Compact在清除前会进行一步整理,将存活对象向一侧移动,随后清空边界的另外一侧内存,这样空闲的内存都是连续的,可是带来的问题就是速度会慢一些。在V8中,老生代是Mark-Sweep和Mark-Compact二者共同进行管理的。

以上就是本文的所有内容,书写过程当中参考了不少中外文章,参考书籍包括朴大大的《深刻浅出NodeJS》以及《JavaScript高级程序设计》等。咱们这里并无对具体的算法实现进行探讨,感兴趣的朋友能够继续深刻研究一下。

最后,谢谢你们可以读到这里,若是文中有任何不明确或错误的地方,欢迎给我留言~~

4 执行环境、执行上下文、活动对象、this

执行环境

执行环境是js中重要的一个概念。执行环境定义了变量和函数有权访问其余变量,决定了他们的各自行为,每一个函数执行都有本身的执行环境,当执行流入一个函数,函数的执行环境就会给推到当前执行栈中,函数执行完毕,函数的执行环境就会被弹出,执行权交给当前栈,这就是js的执行流

变量对象

每一个执行环境都有一个与之关联变量对象,环境中定义的全部的变量和函数都保存在这个变量中,虽然咱们编写代码没法访问这个对象,可是解析器可以在处理数据的时会在后台使用。

全局执行环境

全局执行环境,是最外围的一个执行环境。根据ecmascript实现所在的宿主不一样,表示执行环境也不同,web全局执行环境被认为是window,所以全局全部的变量和函数都被认为是window的属性和函数被建立,某个执行环境的中的代码执行完毕后,该环境就会给销毁,该环境变量对象也会被销毁

做用域链

当代码在执行环境中执行时,会建立变量对象的一个做用域链,做用域链的用途,是保证执行环境有权访问全部的变量和函数有序访问,做用域的最前端是当前执行环境的的变量对象,若是这个环境是函数,就将其 **活动对象** ,做为变量对象,活动对象刚开始就只包含一个变量就是arguments对象(这个对象在全局是不存在的),做用域的下一个变量对象来之与当前函数所在的执行栈的变量对象(能够理解为当前函数的执行栈),下一个的下一个就是当前函数执行栈的执行栈,这样一直延续到全局执行环境中的变量对象,为做用域的末端。

标识符解析(变量查找),是按照做用域链一级一级的操做,查找顺序是从当前变量对象开始,知道找到为止,若是找不到就会一般会有异常

var color = "blue";
function changeColor() {
var otherColor = "red";
function swapColor() {
 var tempColor = otherColor;
 otherColor = color;
 color = tempColor;
 // 这里能够访问 tempColor otherColor color
}
swapColor();
// 这里能够访问  otherColor color swapColor
}
changeColor();
// 这里能够访问 changeColor  color
复制代码

看图

image.png

this是什么

通常对this的误解分为两个方面
  • 1 this是指向当前函数的自己
  • 2 this 指向的是当前函数的 做用域

this是指向当前函数的自己

下面代码中你们要理解函数的多面性,多个身份

  • 普通的函数
  • 普通的对象
  • 构造函数

接下来说用到函数的是两个身份普通函数、普通对象, 看代码()

function foo(){
    this.count++
}
var count=0;
foo.count=0;
for(var i=0;i<5;i++){
    
    foo()
}
console.log(foo.count)//0
console.log(count)//5

复制代码

从打印的结果上来看显然,this指向的不是自己函数,固然我们通常看到这类的问题我们就会绕道而行,看代码

function foo(){
    this.count++
}
var bar={
    count:0
}
foo.count=0;
for(var i=0;i<5;i++){
    
    foo.call(bar)
}
console.log(bar.count)//5
console.log(count)//0

复制代码

虽然这种解决方案很好,也会有其余的解决方案,可是咱们仍是不理解this的问题,内心仍是有种不安之感

this 指向的是当前函数的 做用域

接下来说用到函数的是两个身份普通函数、普通对象, 看代码()

function foo(){
     var num=2;
     console.log(this.num)
 }
 var num=0;
 foo()//0

复制代码

我们看到代码的执行结果后,发现this指向的并非该函数的做用域。

this究竟是什么

this是在函数调用的时候绑定,不是在函数定义的时候绑定。它的上下文取决于函数调用时的各类条件,函数执行的时候会建立一个活动记录,这个记录里面包含了该函数中定义的参数和参数,包含函数在哪里被调用(调用栈)...,this就是其中的一个属性。 来看图

图中我们看到this是在函数执行的时候建立的。


全面解析this


前面几步我们已经肯定的this的建立和this的指向的误区,接下啦我们要看看this的绑定的规则,分为4个规则。

  • 默认绑定
  • 隐式绑定(上下文绑定)
  • 显式绑定
  • new 绑定


默认绑定


默认绑定的字面意思就是,不知足其余的绑定方式,而执行的绑定规则。默认绑定会把this绑定到全局对象(是一个危险的操做,文章后面会说为何) 看代码

function foo(){
     var num=2;
     this.num++
     console.log(this.num)
 }
 var num=0;
 foo()//1
复制代码

上面代码中就实现了默认绑定,在foo方法的代码块中操做的是window.num++。


隐式绑定(上下文绑定)


定义:
函数被调用的位置有上下文,或者是该函数的引用地址是否是被某个对象的属性引用,并经过对象的属性直接运行该函数。若是出现上述的状况,就会触发this的隐式绑定,this就会被绑定成当前对象 看代码

function foo(){
    console.log(this.name)
}
var bar={
    name:'shiny',
    foo:foo
}
bar.foo()//shiny
复制代码

要须要补充一点,无论你的对象嵌套多深,this只会绑定为直接引用该函数的地址属性的对象,看代码

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny',
    foo:foo
}
var red={
    name:'red',
    obj:shiny
    
}
red.obj.foo()//shiny
复制代码


隐式绑定的丢失


先看代码

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny',
    foo:foo
}
function doFoo(fn){
    fn()
}
doFoo(shiny.foo)//undefind
复制代码

你们知道函数参数在函数执行的时候,其实有一个赋值的操做,我来解释一下上面的,当函数doFoo执行的时候会开辟一个新的栈并被推入到全局栈中执行,在执行的过程当中会建立一个活动对象,这个活动对象会被赋值传入的参数以及在函数中定义的变量函数,在函数执行时用到的变量和函数直接从该活动对象上面取值使用。 看图 doFoo的执行栈

fn的执行栈

看下面原理和上面同样经过赋值,致使隐式绑定的丢失,看代码

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny',
    foo:foo
}
var bar = shiny.foo
bar()//undefined
复制代码

你们是否是已经明白了为何是undefined,来解释一波,其实shiny的foo属性是引用了foo函数的引用内存地址,那么有把foo的引用地址赋值给了 bar 那么如今的bar的引用地址个shiny.foo的引用地址是一个,那么执行bar的时候也会触发默认绑定规则由于没有其余规则能够匹配,bar函数执行时,函数内部的this绑定的是全局变量。

看下满的引用地址赋值是出现的,奇葩 隐式绑定丢失,看代码

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny',
    foo:foo
}
var red={
    name:'red'
}
(red.foo=shiny.foo)()//undefined
复制代码

赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,所以调用位置是 foo() 而不是 p.foo() 或者 o.foo()。根据咱们以前说过的,这里会应用默认绑定。


显式绑定


call、apply绑定


javascript,在Function的porpertype上提供了3个方法来强行修改this,分别是 call、apply、bind,你们常常用的莫过于call和apply了,这两个函数的第一个参数,都是须要执行函数绑定的this,对于apply只有连个参数,第二个参数是一个数组,这个数组是要传入执行函数的参数,而call能够跟不少参数,从第二个参数起都会被传入到要执行函数的参数中

看代码

function foo(){
   console.log(this.age)
}
var shiny={
   age:20
}
foo.call(shiny)//20

function bar(){
console.log(this.age)
}
var red={
age:18
}
bar.apply(red)//18
复制代码

这两个方法都是显式的绑定了tihs

硬绑定:


相似与 bind方法行为,是显式绑定的一种方式

function foo(b){
  return this.a+b
}
var obj={
  a:2
}
function bind(fn,obj){
  return function(){
     return fn.apply(obj,arguments)
  }
}
bind(foo,obj)(3)//5
复制代码

语言解释: 经过apply + 闭包机制 实现bind方法,实现强行绑定规则

API调用的“上下文” 第三方库或者寄生在环境,以及js内置的一些方法都提供了一下 content 上下文参数,他的做用和 bind同样,就是确保回调函数的this被绑定

function foo (el){
  console.log(el,this.id)
}
var obj ={
 id:'some one'
};
[1,2,4].forEach(foo,obj)
// 1 some one 2 some one 4 some one
复制代码


new 绑定


说道new 你们都会想到js的构造函数,我们想不用着急new 绑定this的问题,我们先看看我们对js的构造函数的误解,传统面向类的语言中的构函数和js的构造函数时不同

  • 传统面向类的语言中的构函数,是在使用new操做符实例化类的时候,会调用类中的一些特殊方法(构造函数)

  • 不少人认为js中的new操做符和传统面向类语言的构造函数是同样的,其实有很大的差异

  • 重新认识一下js中的构造函数,js中的构造函数 在被new操做符调用时,这个构造函数不属于每一个类,也不会创造一个类,它就是一个函数,只是被new操做符调用。

  • 使用new操做符调用 构造函数时会执行4步

    • 建立一个全新的对象
    • 对全新的对象的__proto__属性地址进行修改为构造函数的原型(prototype)的引用地址
    • 构造函数的this被绑定为这个全新的对象
    • 若是构造函数有返回值而且这个返回值是一个对象,则返回该对象,不然返回当前新对象

我们了解了js new 操做符调用构造函数时都作了些什么,哪么我们就知道构造函数里面的this是谁了

代码实现

function Foo(a){
  this.a=a
}
var F = new Foo(2)
console.log(F.a)//2
复制代码


绑定规则的顺序


我们在上面了解this绑定的4大规则,那么我们就看看这4大绑定规则的优先级。

默认绑定

我们根据字面意思,都能理解只有其他的3个绑定规则没法触发的时候就会触发默认绑定,没有比较意义


显式绑定 VS 隐式绑定


看代码

function foo(){
    console.log(this.name)
}
var  shiny={
    name:'shiny',
    foo:foo
}
var red={
    name:'red'
}

shiny.foo()//shiny
shiny.foo.call(red)// red
shiny.foo.apply(red)// red
shiny.foo.bind(red)()//red
复制代码

显然在这场绑定this比赛中,显式绑定赢了隐式绑定


隐式绑定 VS new 操做符绑定


看代码

function  foo(name){
    this.name=name
}
var shiny={
    foo:foo
}
shiny.foo('shiny')
console.log(shiny.name)//shiny

var red = new shiny.foo('red')
console.log(red.name)//red
复制代码

显然在这场绑定this比赛中new 操做符绑定赢了隐式绑定


显式绑定(硬绑定) VS new 操做符绑定


使用call、apply方法不能结合new操做符会报错误

可是我们能够是bind绑定this来比较 显式绑定和new操做符的绑定this优先级。 看代码

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny'
}

var bar = foo.bind(shiny)
var obj = new bar();
console.log(obj.name)// undefind
复制代码

显然 new操做符绑定 打败了 显式绑定


this的判断


我们在上面已经了解 4个绑定this的优先级。我们能够列举出来

  • 1 判断该函数是否是被new操做符调用,有的话 this就是 构造函数运行时建立的新对象 var f = new foo()
  • 2 判断 函数是否是使用显式绑定 call、apply、bind,若是有,那么该函数的this就是 这个三个方法的第一个参数

foo.call(window)

  • 3 判断该函数是否是被一个对象的属性引用了地址,该函数有上下文(隐式绑定),在函数执行的时候是经过该对象属性的引用触发,这个函数的this就是当前对象的。

obj.foo();

  • 4 上面的三种都没有的话,就是默认绑定,该函数的this就是全局对象或undefined(严格模式下)


绑定例外


😁 规则老是会有意外的,this绑定也是会有的,某些场面的绑定也是会出乎意料的,有可能触发了默认绑定 看代码

function foo(){
    console.log(name)
}
var name ='shiny'
foo.call(null)//shiny
foo.call(undefined)//shiny
var bar = foo.bind(null)
var baz = foo.bind(undefined)
bar()//siny
baz()//siny
复制代码

把 null、undefined经过 apply、call、bind 显式绑定,虽然实现可默认绑定,可是建议这么作由于在非严格的模式下会给全局对象添加属性,有时候会形成不可必要的bug。


更安全的this


我们从上面知道在非严格模式下 默认绑定是并操做this的话会该全局对象添加属性,这样的操做是有风险性的

function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 咱们的空对象
var ø = Object.create( null );
// 把数组展开成参数
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3

复制代码


es6中的this


在es5及一下版本,咱们被this深深的困惑,可是看完了上面的文章,应该判断this没有关系,可是 重点来了 es6的this能够经过箭头函数直接绑定在该函数的执行的做用域上。 看代码

function foo(){
     return ()=>{
          console.log(this.name)
     }
 }
 var obj ={
     name:'obj'
 }
  var shiny ={
     name:'shiny'
 }
 var bar = foo.call(obj);
 bar.call(shiny)// foo
 

复制代码

咱们看到箭头函数的this被绑定到该函数执行的做用域上。

我们在看看 js内部提供内置函数使用箭头函数

function foo() {
    setTimeout(() => {
    // 这里的 this 在此法上继承自 foo()
    console.log( this.a );
    },100);
}
var obj = {
    a:2
};
foo.call( obj ); // 2
复制代码

箭头函数能够像 bind(..) 同样确保函数的 this 被绑定到指定对象,此外,其重要性还体 如今它用更常见的词法做用域取代了传统的 this 机制。实际上,在 ES6 以前咱们就已经 在使用一种几乎和箭头函数彻底同样的模式。

function foo() {
var self = this; // lexical capture of this
setTimeout( function(){
    console.log( self.a );
    }, 100 );
}
var obj = {
    a: 2
};
foo.call( obj ); // 2
复制代码

虽然 self = this 和箭头函数看起来均可以取代 bind(..),可是从本质上来讲,它们想替 代的是 this 机制。 若是你常常编写 this 风格的代码,可是绝大部分时候都会使用 self = this 或者箭头函数。 若是彻底采用 this 风格,在必要时使用 bind(..),尽可能避免使用 self = this 和箭头函数。

5 闭包的造成

闭包

有关如何建立做用域链以及做用域链有什么做用的细节,对完全 理解闭包相当重要。当某个函数被调用时,会建立一个执行环境(execution context)及相应的做用域链。 而后,使用 arguments 和其余命名参数的值来初始化函数的活动对象(activation object)。但在做用域 链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至做为做用域链终点的全局执行环境

在函数执行过程当中,为读取和写入变量的值,就须要在做用域链中查找变量。来看下面的例子。

function compare(value1, value2) {
if (value1 < value2) {
 return -1;
} else if (value1 > value2) {
 return 1;
} else {
 return 0;
}
}
var result = compare(5, 10);
复制代码

以上代码先定义了 compare()函数,而后又在全局做用域中调用了它。当调用 compare()时,会 建立一个包含 arguments、value1 和 value2 的活动对象。全局执行环境的变量对象(包含 result 和 compare)在 compare()执行环境的做用域链中则处于第二位。图片 展现了包含上述关系的 compare()函数执行时的做用域链。

image.png 后台的每一个执行环境都有一个表示变量的对象——变量对象。全局环境的变量对象始终存在,而像 compare()函数这样的局部环境的变量对象,则只在函数执行的过程当中存在。在建立 compare()函数 时,会建立一个预先包含全局变量对象的做用域链,这个做用域链被保存在内部的[[Scope]]属性中。 当调用 compare()函数时,会为函数建立一个执行环境,而后经过复制函数的[[Scope]]属性中的对 象构建起执行环境的做用域链。此后,又有一个活动对象(在此做为变量对象使用)被建立并被推入执 行环境做用域链的前端。对于这个例子中 compare()函数的执行环境而言,其做用域链中包含两个变 量对象:本地活动对象和全局变量对象。显然,做用域链本质上是一个指向变量对象的指针列表,它只 引用但不实际包含变量对象。 不管何时在函数中访问一个变量时,就会从做用域链中搜索具备相应名字的变量。通常来说, 当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局做用域(全局执行环境的变量对象)。 可是,闭包的状况又有所不一样。

在看一个案例

function createComparisonFunction(propertyName) {
  return function (object1, object2) {
    var value1 = object1[propertyName];
    var value2 = object2[propertyName];

    if (value1 < value2) {
      return -1;
    } else if (value1 > value2) {
      return 1;
    } else {
      return 0;
    }
  };
}
复制代码

在这个例子中,object1[propertyName] object2[propertyName] 两行代码是内部函数(一个匿名函数)中的代码,这两行代码访问了外部 函数中的变量 propertyName。即便这个内部函数被返回了,并且是在其余地方被调用了,但它仍然可 以访问变量 propertyName。之因此还可以访问这个变量,是由于内部函数的做用域链中包含 createComparisonFunction()的做用域。要完全搞清楚其中的细节,必须从理解函数被调用的时候 都会发生什么入手。

当某个函数被调用时,会建立一个执行环境(execution context)及相应的做用域链。 而后,使用 arguments 和其余命名参数的值来初始化函数的活动对象(activation object)。但在做用域 链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至做为做用域链终点的全局执行环境

看图

image.png

闭包与变量

做用域链的这种配置机制引出了一个值得注意的反作用,即闭包只能取得包含函数中任何变量的最 后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量。下面这个例子能够清晰地说 明这个问题。
function createFunctions() {
  var result = new Array();
  for (var i = 0; i < 10; i++) {
    result[i] = function () {
      return i;
    };
  }
  return result;
}

复制代码

image.png

这个函数会返回一个函数数组。表面上看,彷佛每一个函数都应该返本身的索引值,即位置 0 的函数 返回 0,位置 1 的函数返回 1,以此类推。但实际上,每一个函数都返回 10。由于每一个函数的做用域链中 都保存着 createFunctions() 函数的活动对象,因此它们引用的都是同一个变量 i 。 当 createFunctions()函数返回后,变量 i 的值是 10,此时每一个函数都引用着保存变量 i 的同一个变量 对象,因此在每一个函数内部 i 的值都是 10。可是,咱们能够经过建立另外一个匿名函数强制让闭包的行为 符合预期,以下所示。

function createFunctions() {
  var result = new Array();
  for (var i = 0; i < 10; i++) {
    result[i] = (function (num) {
      return function () {
        return num;
      };
    })(i);
  }
  return result;
}
复制代码

下载 (1).png 在重写了前面的 createFunctions()函数后,每一个函数就会返回各自不一样的索引值了。在这个版 本中,咱们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将当即执行该匿名函数的结果赋 给数组。这里的匿名函数有一个参数 num,也就是最终的函数要返回的值。在调用每一个匿名函数时,我 们传入了变量 i。因为函数参数是按值传递的,因此就会将变量 i 的当前值复制给参数 num。而在这个 匿名函数内部,又建立并返回了一个访问 num 的闭包。这样一来,result 数组中的每一个函数都有本身 num 变量的一个副本,所以就能够返回各自不一样的数值了。

关于this

在闭包中使用 this 对象也可能会致使一些问题。咱们知道,this 对象是在运行时基于函数的执 行环境绑定的:在全局函数中,this 等于 window,而当函数被做为某个对象的方法调用时,this 等 于那个对象。不过,匿名函数的执行环境具备全局性,所以其 this 对象一般指向 window。但有时候 因为编写闭包的方式不一样,这一点可能不会那么明显。下面来看一个例子。
var name = "The Window";
var object = {
  name: "My Object",
  getNameFunc: function () {
    return function () {
      return this.name;
    };
  },
};
alert(object.getNameFunc()()); //"The Window"
复制代码

每一个函数在被调用时都会自动取得两个特殊变量:this 和 arguments。内部函 数在搜索这两个变量时,只会搜索到其活动对象为止,所以永远不可能直接访问外部函数中的这两个变 量。不过,把外部做用域中的 this 对象保存在一个闭包可以访问 到的变量里,就可让闭包访问该对象了。

var name = "The Window";
var object = {
  name: "My Object",
  getNameFunc: function () {
    var that = this;
    return function () {
      return that.name;
    };
  },
};
alert(object.getNameFunc()()); //"My Object"
复制代码

在几种特殊状况下,this 的值可能会意外地改变。好比,下面的代码是修改前面例子的结果。

var name = "The Window";
var object = {
  name: "My Object",
  getName: function () {
    return this.name;
  },
};

复制代码

第一行代码跟日常同样调用了 object.getName(),返回的是"My Object",由于 this.name 就是 object.name。第二行代码在调用这个方法前先给它加上了括号。虽然加上括号以后,就好像只 是在引用一个函数,但 this 的值获得了维持,由于 object.getName 和(object.getName)的定义 是相同的。第三行代码先执行了一条赋值语句,而后再调用赋值后的结果。由于这个赋值表达式的值是 函数自己,因此 this 的值不能获得维持,结果就返回了"The Window"。 固然,你不大可能会像第二行和第三行代码同样调用这个方法。不过,这个例子有助于说明即便是 语法的细微变化,都有可能意外改变 this 的值。

6 深copy的实现

深拷贝和浅拷贝的定义

深拷贝已是一个老生常谈的话题了,也是如今前端面试的高频题目,可是令我吃惊的是有不少同窗尚未搞懂深拷贝和浅拷贝的区别和定义

浅拷贝:

16ce894a1f1b5c32.png

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

深拷贝:

16ce893a54f6c13d.png

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

乞丐版

在不使用第三方库的状况下,咱们想要深拷贝一个对象,用的最多的就是下面这个方法。
JSON.parse(JSON.stringify());
复制代码

这种写法很是简单,并且能够应对大部分的应用场景,可是它仍是有很大缺陷的,好比拷贝其余引用类型、拷贝函数、循环引用等状况。

基础版本

function clone(target) {
   if (typeof target === 'object') {
       let cloneTarget = {};
       for (const key in target) {
           cloneTarget[key] = clone(target[key]);
       }
       return cloneTarget;
   } else {
       return target;
   }
};

复制代码

image.png

这是一个最基础版本的深拷贝,这段代码可让你向面试官展现你能够用递归解决问题,可是显然,他还有很是多的缺陷,好比,尚未考虑数组。

考虑数组

在上面的版本中,咱们的初始化结果只考虑了普通的object,下面咱们只须要把初始化代码稍微一变,就能够兼容数组了:

module.exports = function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8]
};



复制代码

image.png OK,没有问题,你的代码又向合格迈进了一小步。

循环引用

const target = {
   field1: 1,
   field2: undefined,
   field3: {
       child: 'child'
   },
   field4: [2, 4, 8]
};
target.target = target;
复制代码

image.png 很明显,由于递归进入死循环致使栈内存溢出了。

缘由就是上面的对象存在循环引用的状况,即对象的属性间接或直接的引用了自身的状况:

解决循环引用问题,咱们能够额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当须要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,若是有的话直接返回,若是没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这个存储空间,须要能够存储key-value形式的数据,且key能够是一个引用类型,咱们能够选择Map这种数据结构:

  • 检查map中有无克隆过的对象
  • 有 - 直接返回
  • 没有 - 将当前对象做为key,克隆对象做为value进行存储
  • 继续克隆
function clone(target, map = new Map()) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);
        for (const key in target) {
            cloneTarget[key] = clone(target[key], map);
        }
        return cloneTarget;
    } else {
        return target;
    }
};


复制代码

image.png

接下来,咱们可使用,WeakMap提代Map来使代码达到画龙点睛的做用。

function clone(target, map = new WeakMap()) {
    // ...
};

复制代码

为何要这样作呢?,先来看看WeakMap的做用:

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值能够是任意的。

什么是弱引用呢?

在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并所以可能在任什么时候刻被回收。

举个例子:

let obj = { name : 'ConardLi'}
const target = new Map();
target.set(obj,'code秘密花园');
obj = null;

复制代码

虽然咱们手动将obj,进行释放,然是target依然对obj存在强引用关系,因此这部份内存依然没法被释放。

再来看WeakMap

let obj = { name : 'ConardLi'}
const target = new WeakMap();
target.set(obj,'code秘密花园');
obj = null;

复制代码

若是是WeakMap的话,targetobj存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。

设想一下,若是咱们要拷贝的对象很是庞大时,使用Map会对内存形成很是大的额外消耗,并且咱们须要手动清除Map的属性才能释放这块内存,而WeakMap会帮咱们巧妙化解这个问题。

相关文章
相关标签/搜索