• 浏览器中的 JavaScript 执行机制


    思维导图

    本文为反复学习极客时间-《浏览器的工作原理与实践》-[浏览器中的 JavaScript 执行机制]章节中的一些思考与记录。

    极客时间

    一些重要概念

    变量提升

    • 所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。
    • 变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。
    • 从概念的字面意义上来看,“变量提升”意味着变量和函数的声明会在物理层面移动到代码的最前面,正如我们所模拟的那样。
    • 但,这并不准确。实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被 JavaScript 引擎放入内存中。

    执行上下文

    分类

    • 全局执行上下文
    • 函数执行上下文
    • eval 执行上下文

    调用栈

    • 调用栈就是用来管理函数调用关系的一种数据结构
    • 在执行上下文创建好后, JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈

    作用域 Scope

    • 定义:作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。
    • 通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

    分类-ES6 之前

    • 全局作用域。全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
    • 函数作用域。函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

    ES6 新增

    • 块级作用域:let/const

    作用域链

    • 在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为outer。
    • 当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量
    • 如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找
    • 把这个查找的链条就称为作用域链
    • 在 JavaScript 执行过程中,其作用域链是由词法作用域决定的

    词法作用域

    • 指作用域是由代码中函数声明的位置来决定的
    • 词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
    • 词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系

    闭包

    • 在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。
    • 比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。

    闭包是怎么回收的

    • 如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭,但如果这个闭包以后不再使用的话,就会造成内存泄漏,
    • 如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。
    • 原则---如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

    this

    • 首先明确,作用域链和 this 是两套不同的系统,它们之间基本没太多联系

    分类

    1. 全局执行上下文中的 this,指向 window 对象
    2. 函数执行上下文中的 this,
    • 通过函数的 call/apply/bind 方法设置,this 指向 call/apply/bind 方法中的第一个参数
    • 通过对象调用方法设置,this 指向对象本身
    • 通过构造函数中设置,this 指向 new 的实例对象
    1. eval 中的 this

    this 的设计缺陷以及应对方案

    1. 嵌套函数中的 this 不会从外层函数中继承
    • 在函数中声明一个变量 self 用来保存 this
    • 使用 ES6 中的箭头函数,这是因为 ES6 中的箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数
    1. 普通函数中的 this 默认指向全局对象 window
    • 如果要让函数执行上下文中的 this 指向某个对象,最好的方式是通过 call 方法来显示调用。
    • 通过设置 JavaScript 的“严格模式”,在严格模式下,默认执行一个函数,其函数的执行上下文中的 this 值是 undefined

    闭包

    疑惑

    关于李兵老师在课程中对闭包的定义和讲解中,我有一些疑惑

    疑惑一:

    在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。

    当通过调用一个外部函数返回一个内部函数后

    一定需要这样的条件吗?

    答案是:不一定。

    《你不知道的 JavaScript》(上卷) 中有一个简单的例子

    function foo() {
    	var a = 2;
    
    	function bar() {
    		console.log(a);
    	}
    	bar();
    }
    foo();
    

    4

    这里使用了闭包,不过按照作者的想法,这并不能体现出闭包的价值,因为这完全可以用词法作用域的查找规则来解释打印出的结果。

    疑惑二

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

    闭包

    对于这个例子的疑惑,为什么闭包中的变量 没有 test2

    于是我进行了一点点改动。

    2

    查找 MDN 中关于 闭包 的定义
    MDN-Closure

    函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。
    也就是说,闭包可以让你从内部函数访问外部函数作用域。
    在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。

    《你不知道的 JavaScript》P44 中的定义:

    当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

    我的理解

    • 虽然从学术上来说,闭包发生在定义时,包含了作用域内所有变量
    • 但在浏览器处理时,只将被利用到的变量存放在相应的位置,所以在控制台也只看到被使用的变量存放在闭包中。

    学习小体会

    • 笔者曾经几次打开《你不知道的 JavaScript》(上卷),翻了翻目录,感觉里面的知识点都见过,都知道呀,就匆匆关上了,放到一边。

    • 今天终于因为对闭包理解中的一些疑问,认真地看了这本书,发现了其中魅力之处。作者的写作方式也让读者感到 interesting,然后不禁发出和书中同样的感慨:“妈妈快来看呀,这就是闭包!”

    • 另外,序言中 Shane Hudson 提到“想弄清楚事物的工作原理”,让我意识到学习知识应该要有探索的精神,不仅要知其然,还要知其所以然。前段时间有些浮躁、有些急功近利,看了一大堆面试题,很多都是浅尝辄止,流于表面。我想这应该是很多人都在走的路,但我们不能一直这样,面试题可以帮助我们完善知识体系,查漏补缺,但很多知识点都需要自己去针对性地学习、去实践,别人几句话总结出来的答案更需要我们带着批判性精神去审视,不能盲目吸收。

    • 如果你想从这篇文章中完全掌握闭包,那么笔者只能说:“小朋友,你还是涉世未深~”。

    • 想想曾经的自己,以为会背红宝书 P178 中对闭包的定义:“闭包是指有权访问另外一个函数作用域中的变量的函数”,就洋洋得意,以为拿下了闭包这个大 BOSS。不管面试官听了作何感想,反正现在看来,是自己都说服不了自己。

    • 学东西还是得踏踏实实的,现在能比较正确地理解和使用闭包,首先得益于极客时间-《浏览器的工作原理与实践》-[浏览器中的 JavaScript 执行机制]章节,让我以前很多模糊的概念有了清晰的认知。题外话,这个课程真的是强推,走过路过千万不要错过。然后是在一些技术博客上看到关于闭包的分析,心中存有一些疑问,于是再读《浏览器的工作原理与实践》对应内容,但并未得到解答。看到评论区中有推荐《你不知道的 JavaScript》(上卷)作为补充学习的建议,于是开始仔细阅读这本书,有了今日的收获。

    • 所以笔者建议认真阅读《你不知道的 JavaScript》或者《浏览器的工作原理与实践》来理解闭包,还可以参考 MDN 中对闭包的说明,然后整理出自己的学习笔记。

    上卷

    整理思路

    1.闭包是什么

    按照 MDN-closure 的描述:
    MDN-Closure

    函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。

    也就是说,闭包可以让你从内部函数访问外部函数作用域。

    在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。

    2.闭包的常见产生方式

    (1).函数作为返回值被返回

    function create() {
    	let a = 100;
    	return function() {
    		console.log(a)
    	}
    }
    let func = create();
    let a = 200;
    func();
    

    (2).函数作为参数

    function print(fn) {
    	let b = 200;
    	fn();
    }
    let b = 100;
    
    function fn() {
    	console.log(b);
    }
    print(fn);
    

    (3).在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

    场景题

    说明以下代码输出的结果,并解释原因

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

    输出结果:6 6 6 6 6

    原因:这里涉及到 JS 中 eventLoop 机制, 简单来说, 虽然循环中的五个函数是在各个迭代中分别定义的, 但是它们都被封闭在一个共享的全局作用域中, 因此实际只有一个i;并且延迟函数的回调在循环结束时才执行, 所以每次输出 6.

    追问:如何改进代码,让输出数字1-5

    1. 立即执行函数
    for (var i = 1; i <= 5; i++) {
    	(function() {
    		var j = i;
    		setTimeout(function timer() {
    			console.log(j);
    		}, j * 1000);
    	})();
    }
    // 将上面代码进行一些改进:
    for (var i = 1; i <= 5; i++) {
    	(function(j) {
    		setTimeout(function timer() {
    			console.log(j);
    		}, j * 1000);
    	})(i);
    }
    // 在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域, 使得延迟函数的回调可以将新的作用域封闭在每个迭代内部, 
    // 每个迭代中都会含有一个具有正确值的变量供我们访问!
    
    1. 给定时器传入第3个参数, 作为 timer 函数的第一个函数参数
    for (var i = 1; i <= 5; i++) {
    	setTimeout(function timer(j) {
    		console.log(j);
    	}, i * 1000, i)
    }
    
    1. 块作用域
    for (let i = 1; i <= 5; i++) {
    	setTimeout(function timer() {
    		console.log(i);
    	}, i * 1000);
    }
    

    闭包的作用

    • 可以访问另一个函数内部的局部变量;
    • 让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。
    • 同一个闭包机制可以创建多个闭包函数出来,它们彼此没有联系,都是独立的,并且每个闭包函数可以保存自己个性化的信息。

    this

    极客时间-《浏览器的工作原理与实践》-[浏览器中的 JavaScript 执行机制]

    ---11.this:从JavaScript执行上下文的视角讲清楚this

    this-李兵

    《你不知道的JavaScript》(上卷) 中对 this 的相关介绍:

    this-你不知道的JavaScript

    判断this

    可以参考 yck《前端面试手册》中的建议:

    this

    小结

  • 相关阅读:
    Mybatis详解(二)
    Mybatis详解(一)
    Java集合
    Java基础之IO
    Java异常知识点!
    HTTP状态码
    ajax传字符串时出现乱码问题的解决
    Json 文件 : 出现 Expected value at 1:0 问题的解决
    java @XmlTransient与@Transient区别
    文件的上传和回显
  • 原文地址:https://www.cnblogs.com/chrislinlin/p/12678136.html
Copyright © 2020-2023  润新知