最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 「JavaScript 基础知识 - 中 | 创作者训练营 」

    正文概述 掘金(李永宁)   2020-12-27   504

    传送门

    • JavaScript 基础知识 - 上

    • JavaScript 基础知识 - 中

    变量、作用域与内存

    相比于其它语言,JavaScript 的变量是松散类型的,变量的值和数据类型在脚本生命周期内可以任意被改变。这很强大,但也会带来不少问题。

    原始值与引用值

    ECMAScript 包含两种类型的数据:原始值和引用值。原始值就是最简单的数据,引用值则是有多个值构成的对象。

    原始值包括 Undefined、Null、Boolean、Number、String 和 Synbol 这六种。原始值是按值访问的,操作的就是存储在变量中的实际值。

    引用值是保存在内存中的对象。JavaScript 不允许直接访问内存,所以,操作对象时,操作的是对该对象的引用而非实际的对象。因此,引用值的变量是按引用访问的。

    动态属性

    动态属性是指引用类型变量创建以后可向其动态的添加属性。原始值不能有属性,虽然给原始值添加属性不会报错。

    // 对象
    const obj = new Object();
    // 动态添加属性
    obj.name = 'lyn'
    console.log(obj.name)	// lyn
    // 原始值
    const vari = 'test'
    vari.name = 'lyn'
    console.log(vari.name)	// undefined
    

    复制值

    原始值和引用值除了存储方式不同之外,在变量复制时也有所不同。由于复制时,复制的是变量本身,所以原始值会被完整的复制一份,复制后的两个变量互不干扰;但是引用值变量存储的是一个内存地址,所以实际上复制的一个指针,复制后新旧两个变量会指定同一个存储在堆内存中的对象。

    原始值

    // 复制后的两个变量可独立使用,互不干扰
    let num1 = 5;
    let num2 = num1;
    console.log(num1, num2)	// 5 5
    num1 = 6;
    num2 = 8
    console.log(num1, num2)	// 6 8
    
    「JavaScript 基础知识 - 中 | 创作者训练营 」

    引用值

    // 复制后两个变量操作的是同一个对象
    const obj1 = { t: 't' }
    const obj2 = obj1
    console.log(obj1, obj2)	// {t: "t"} {t: "t"}
    obj1.t = 'tt'
    console.log(obj1, obj2)	// {t: "tt"} {t: "tt"}
    
    「JavaScript 基础知识 - 中 | 创作者训练营 」

    传递参数

    ECMAScript 变量的访问分:按值访问和按引用访问,但函数传参都是按值传递的,包括引用类型的值。

    按值传递参数时,值会被复制到函数的局部变量(命名参数,或者用 ECMAScript 的话来说,就是 arguments 对象中的一个槽位)。用示例来解释:

    const gNum = 10
    function fn(num) {
      num += 10;
      console.log(num)	// 20,局部变量 num 的值改
      console.log(gNum) // 10,外层的 num 没有变
    }
    fn(num)
    

    这个示例说明原始值是按值传递的

    const gObj = { t: 't' }
    function fn(obj) {
      console.log(obj) // { t: 't' }
      obj.t = 'tt'
      // 这里访问外层的 gObj 的值变了只能说明 obj 和 gObj 的值保存的是同一个指针
      console.log(gObj) // { t: 'tt' }
      // 覆写 obj 变量的值
      obj = { c: 'c' }
      // 这里两个不一样,只能说明 obj 存储的值(指针)变量
      console.log(obj) // { c: 'c' }
      console.log(gObj) // { t: 'tt' }
    }
    
    fn(obj)
    

    这个示例足以说明引用类型的变量在函数传参时也是按值传递的,因为如果是按引用传递的话 obj 被覆写时 gObj 也应该被覆写。

    其实上面说这些都不重要,重要的是明白原始值变量和引用类型变量内部存储的是什么,明白了这个上面说的就都明白了。

    确定类型

    ECMAScript 中确定一个变量的类型有 5 种方法:typeof 运算符、instanceof 运算符、Array.isArray() 方法、constructor 属性、Object.prototype.toString() 方法。

    typeof 在判断字符串、数值、布尔值和undefined 和 函数的类型是很好用,但是对象、null、数组 等就不好用了。

    instanceof 和 constructor 一般用于确定指定变量是否为某种个对象的实例。

    Array.isArray(variable) 方法用于判断指定变量是否为数组,可以解决夸框架的问题

    String.prototype.toString() 方法,完美判断变量类型的方法。

    // typeof
    console.log(typeof 'test')	// string
    console.log(typeof 2)	// number
    console.log(typeof true)	// boolean
    console.log(typeof undefined)	// undefined
    // 以下类型用 typeof 不好使
    console.log(typeof null)	// object
    console.log(typeof {})	// object
    console.log(typeof [])	// object
    console.log(typeof new Date())	// object
    console.log(typeof function () {})	// function
    console.log(typeof Array)	// function
    
    // instanceof 和 constructor
    const d = new Date()
    console.log(d instanceof Date)	// true
    console.log(d instanceof Array) // false
    console.log(d.constructor === Date)	// true
    console.log(d.constructor === Array) // false
    
    // Object.prototype.toString.apply(variable),返回 [object Xxx]
    console.log(Object.prototype.toString.apply(2))	// [object Number]
    console.log(Object.prototype.toString.apply(null))	// [object Null]
    console.log(Object.prototype.toString.apply(Array))	// [object Function]
    console.log(Object.prototype.toString.apply({})) // [object Object]
    console.log(Object.prototype.toString.apply(d)) // [object Date]
    

    执行上下文和作用域

    执行上下文(以下简称“上下文”)决定变量和函数可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的 变量对象,上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台处理数据时会会用到它。上下文分为全局上下文、函数上下文以及 eval 调用内部的上下文 3 种。

    实现 ECMAScript 的宿主环境不同,全局上下文的对象可能不一样,比如在浏览器,全局上下文就是 window 对象,所有通过 var 声明的全局变量和函数都会成为 window 对象的属性和方法。使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果一样。上下文会在其左右代码执行结束后销毁,全局上下文会在应用程序退出前会被销毁,比如关闭网页和浏览器。

    每个函数都有自己的上下文。当代码执行进入函数时,函数的上下文就会被推到上下文栈上。在函数执行完毕后,上下文栈会弹出该函数的上下文,将控制权返还给之前的执行上下文。ECMAScript 程序的执行流就是通过这个上下文栈进行控制的。

    eval 函数在调用时可以设置全局上下文,从而形成一个独立的上下文。比如 qiankun 框架的 JS 沙箱就用到了 eval 函数。

    上下文的代码在执行的时候,会创建变量对象的一个 作用域链。全局上下文的变量对象始终位于作用域链的最后端,当前正在执行的上下文的变量对象位于作用域链的最前端。代码执行时的标识符解析是通过沿作用域链从最前端开始逐级向后搜索来完成的(作用域链中的对象也有原型对象,因此搜索可能涉及每个对象的原型链),如果搜索至全局上下文的变量对象,还没有找到标识符,说明其未声明。

    在作用域链上查找标识符会有一定的性能开销。访问局部变量比访问全局变量要快,因为不用切换作用域。不过,JavaScript 引擎在标识符查找上做了很多的优化工作,将来这个差异可能就微不足道了。

    var color = "blue";
    
    function changeColor() {
      let anotherColor = "red";
    
      function swapColors() {
        let tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
    
        // 这里可以访问color、anotherColor和tempColor
      }
    
      // 这里可以访问color和anotherColor,但访问不到tempColor
      swapColors();
    }
    
    // 这里只能访问color
    changeColor();
    
    
    「JavaScript 基础知识 - 中 | 创作者训练营 」

    每个矩形表示一个上下文,内部上下文可以通过作用域链访问外部上下文的一切,但外部上下文无法访问内部上下文的任何东西。

    作用域链增强(延长)

    以下两个语句在执行时会在作用域链的最前端添加一个临时的上下文(延长作用域链),这个上下文在代码执行结束后会被删除。

    • try / catch 语句的 catch 块
    • with 语句
    function fn() {
      const qs = '?dev=true'
      
      with(location) {
        // var 无法声明块级作用域变量,所以 url 会成为 fn 函数上下文的一部分
        var fnUrl = href + qs + '&vartest=1'
        // const 不存在 var 的问题,因为 const 和 let 可以声明块级作用域变量
        const urlTest = href + qs
        console.log(urlTest)
      }
      // 可以正常访问
      console.log(fnUrl)
      // ReferenceError: urlTest is not defined
      // console.log(urlTest)
    }
    fn()
    

    垃圾回收

    在 C 和 C++ 等语言中,内存的管理需要由开发者自己来完成。JavaScript 是使用垃圾回收的语言,通过自动内存管理实现内存的分配和闲置资源回收。思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。这是一个周期性的过程,垃圾回收程序每隔一段时间(或者说代码执行过程中某个预定的收集时间)就会自动运行。

    函数中的局部变量会在函数执行期间存在。此时,栈(堆)内存会分配空间保存相应的变量值。函数在内部使用了变量然后退出。此时局部变量就不再需要了,那么它占用的内存就可以释放。然后并不是所有时候都这么明显。垃圾回收程序必须跟踪记录哪个变量还会使用,哪个不会再使用,以便回收内存。如何标记未使用的变量有不同的实现方式。不过,在浏览器的发展史上,主要用到过两种标记策略:标记清理和引用计数。

    标记清理

    垃圾回收程序运行的时候,会标记内存中的所有变量(标记方法有很多中,比如维护两个在和不在上下文的变量列表、变量进入上下文时反转某一位等)。然后,它将所有在上下文中的变量,以及被上下文中变量引用的变量的标记去掉。在此之后还存在标记的变量就可以删除了,原因是上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有变量并回收它们的内存。

    目前,基本上所有浏览器都在自己的 JavaScript 实现中采用了标记清理(或其变体),只是运行垃圾回收程序的频率有所差异。

    引用计数

    目前已经没有浏览器使用这种标记策略了。它的思路是记录每个值被引用的次数(声明一个变量并给它赋一个值,这个值的引用数就为 1)。当一个值的引用数为 0 时,就说明这个值已经没用了,可以回收其内存。垃圾回收程序下次运行时就会释放引用数为 0 的值的内存。

    引用计数有一个严重的问题:循环引用。所谓循环引用,就是对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A。比如:

    function probleFn() {
    	const objA = new Object();
        const objB = {}
        objA.key = objB
        objB.key = objA
    }
    
    

    示例中,objA 和 objB 通过各自的属性引用对方,意味着引用数永远都为 2。在标记清除策略下没问题,因为函数运行结束后,这两个对象都不在作用域了,可以被垃圾回收程序回收。而在引用计数策略下,objA 和 objB 在函数结束后还会存在,因为引用数永远不会为 0。如果函数被大量调用,会导致大量的内存永远不会被释放。造成内存泄漏

    性能

    垃圾回收程序会周期性的执行,它的时间调度策略很重要,一个不合适的调度策略不仅不会提升性能,反而会明显拖慢渲染的速度和帧速率。开发者不知道垃圾回收程序什么时候会被执行,因此最好的办法是在写代码时做到:无论什么时候开始收集垃圾,都能让它尽快结束工作,即变量使用完以后手动释放(置为 null)。

    现代垃圾回收程序会基于对 JavaScript 运行时环境的探测来决定何时执行。探测机制因引擎而异,但基本上都是根据已分配对象的大小和数量来判断。比如,V8 团队 2016 年的博文说到:在一次完整的垃圾回收之后,V8 的堆增长策略会根据活跃对象的数量外加一些余量来确定何时再次垃圾回收。

    内存管理

    JavaScript 运行在一个内存管理和垃圾回收都很特殊的环境。分配给浏览器的内存通常要比桌面软件少的多,移动浏览器就更少了。这是为安全考虑,防止运行大量 JavaScript 的网页耗尽系统内存导致操作系统崩溃。

    在有限的内存情况下,在执行的代中只保存必要的数据,可以给页面带来更好的性能。如果数据不再使用,就手动将其设置为 null,从而解除引用。这个操作很适合全局变量和全局对象以及它的属性。局部变量在超出作用域后就会被自动解除引用。

    function fn() {
      // 函数执行结束,num 自动解除引用
      const num = 3
      return num
    }
    const fnReturn = fn()
    console.log(fnReturn)
    // 全局变量使用完以后手动解除引用
    fnReturn = null
    

    解除对一个值的应用不会导致相关内存自动被释放。解除引用的关键在于确保值已经不在上下文中,因此它在下次的垃圾回收时就会被回收。

    通过 const 和 let 声明提升性能

    const 和 let 声明的变量以块为作用域,相比于 var,在块级作用域比函数作用域更早终止的情况下,使用这两个关键字声明的变量可以更早的让垃圾回收程序介入,尽早回收相关内存。

    隐藏类和删除操作

    JavaScript 的动态性往往会破坏浏览器的“隐藏类”优化策略,比如动态添加属性和删除属性。

    V8 在将解释后的 JavaScript 代码编译为机器码时会利用 “隐藏类”。运行期间,V8 会将创建的对象和隐藏类关联起来,以跟踪它们的属性特征。能够共用相同隐藏类的对象性能会更好。

    function Article() {
      this.title = 'test'
    }
    
    const obj1 = new Article()
    const obj2 = new Article()
    

    V8 会在后台配置,让 obj1 和 obj2 这两个实例共享相同的隐藏类,因为两个实例共享同一个构造函数和原型。

    隐藏类是 V8 自己在后台声明的一个匿名类,配合它的快速访问模式以提升性能。详情参考。目前可以简单的理解为一个 class

    如果这个时候添加了下面这行代码:

    obj2.author = 'Jack'
    

    此时两个 Article 实例就会对应两个不同的隐藏类(多了一个类)。如果类似这种操作的频率高和隐藏类的比较大大,就可能对性能产生明显的影响。而且会破坏快速访问模式带来的优化(由静态类型退化为动态类型)。使用 delete 动态删除属性也会带来同样的问题。

    所以 最佳实践 是:在构造函数中一次性声明所有属性,避免先创建再补充属性的情况;需要使用 delete.key 的地方,手动将其设置为 null,比如: obj.title = null

    内存泄漏

    JavaScript 中的内存泄漏大部分是由不合理的引用导致的,比如:

    • 意外声明的全局变量

      function fn() {
        t = 'test'
      }
      

      由于声明变量 t 时没有用 const、let、var 关键字,所以 t 会作为 window 对象的属性存在,本意是声明局部变量,函数运行结束后就销毁,现在却变成了一个知道应用程序退出才可以结束的全局变量。

    • 定时器也可能会导致内存泄漏,比如定时器的回调函数通过密保引用了外部变量

      function fn() {
        const t = 'test'
        setInterval(() => {
          console.log(t)
        }, 1000)
      }
      fn()
      

      这个 1s 运行一次的定时器会导致函数的局部变量 t 在函数运行结束以后无法被释放

    • 闭包会不知不觉间造成内存泄漏

      let outer = function(){
        const t = 'test'
        return function() {
          console.log(t)
        }
      }()
        
      // 这行没有执行的话会导致变量 t 的内存始终无法被回收
      // outer = null
      

      outer 的存在导致函数的局部变量 t 的内存空间始终无法被释放,因为闭包一直引用着它。

    以上说的点虽然看起来很小,但是当你把其中涉及到的数据量无限放大时就会发现问题的严重性远比想象的要严重的多。

    静态分配与对象池

    通过合理使用分配的内存,避免多余的垃圾回收,从而保住因释放内存而损失的性能。

    浏览器决定何时运行垃圾回收程序的一个标准就是对象的更迭速度。如果有 很多 对象初始化,然后一下又都超出了作用域,那么浏览器就会采用更激进的方式调度垃圾回收程序运行,这样就会因为垃圾回收程序的大量运行导致一部分性能的损失。

    这时候的策略就是可以通过一个对象池来管理一组可回收的对象。应用程序可以向这个对象池请求对象、设置属性、使用它,然后在操作完成后还给对象池。由于整个过程没有发生对象初始化和销毁,垃圾回收就无法探测到对象的更替,因此垃圾回收程序就不会被频繁的执行。

    使用数组来维护对象池是一个不错的选择,不过必须要注意不要招致额外的垃圾回收,比如:

    const objArr = new Array(100)
    // 假设这时如果已经存储了100个对象
    const obj = new Object()
    // 这里会带来额外的垃圾回收
    objArr.push(obj)
    

    由于 JavaScript 数组的大小是动态可变的,引擎会先删除原来大小为 100 的数组,然后再创建一个新的大小为 200 的数组。垃圾回收程序看到这个删除操作,很快就会跑来收一次垃圾。所以,要尽量避免这种动态分配的操作,可以在初始化时就创建一个大小够用的数组,从而避免额外的垃圾回收。

    静态分配是一种极端的优化方式。如果你的应用被垃圾回收程序严重的拖了后腿,可以利用它来提升性能。但这种情况并不多见,大多数情况下,这都属于过早优化,因此不必考虑。

    链接

    • JavaScript 基础知识 - 上

    • JavaScript 基础知识 - 中

    JavaScript 基础知识 - 中 | 创作者训练营 征文活动正在进行中......


    起源地下载网 » 「JavaScript 基础知识 - 中 | 创作者训练营 」

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元