理解JavaScript中的作用域和上下文分析解析
- 格式:doc
- 大小:109.00 KB
- 文档页数:14
js作⽤域和词法分析 都知道js中不存在类似于c++等语⾔的块级作⽤域,例如for循环中定义的变量,其实是属于当前对象下的属性,同⼀对象下可以随便访问。
只有函数可以限定⼀个变量的作⽤范围,即函数才是变量的作⽤域。
对于函数的变量访问时遵循作⽤域链的,即当前函数运⾏时会有⼀个当前作⽤域,当饮⽤某个变量时,会先查找当前作⽤域内是否存在该变量的定义,如果不存在则根据作⽤域链向上去查找⽗函数的作⽤域,有则拿来使⽤,没有则继续向上直到全局作⽤域。
关于作⽤域链这⾥就不仔细描述,简单⽽⾔,类似原型链,从全局函数直到当前函数的作⽤域存在⼀种相互包含的关系,⼦可以向上访问,但是⽗不可以向下访问⼦函数的变量,这样层层嵌套的关系链。
类似这样的: var num = 10; function a (){ alert(num); } a() //结果alert(10),a⾥没有num所以向上查找外层的作⽤域,有且等于10所以弹出10⽽不是undefined. 但是,下⾯的代码就是undefined了: var num = 10;var num = 10; function a (){ alert(num); var num = 11; } 为什么呢? a()执⾏时虽然num=11没有赋值但是⽗级作⽤域⾥是有num=10的,不应该是undefined呀,js是按顺序执⾏的,此时的var num = 11;根本没有执⾏,所以应该是10!!你是不是也是这么认为的,就和我当初⼀样。
可以确定这样是对的,不信的⼩伙伴可以⾃⼰去试⼀试,如果你试的结果是10的话,那么⼀定是你写的不太⼀样。
来深究⼀下原因,这⾥就牵扯到js词法分析这个东西了。
总所周知js代码⾃上⽽下执⾏,但是在js代码执⾏前,会先进⾏词法分析。
所以js运⾏要分为词法分析和执⾏两个阶段。
js词法分析主要分为3个步骤: 1,分析形参:如果函数有形参,则给当前活动对象增加形参属性,默认为undefined。
JS核⼼知识梳理前⾔本⽂⽬标从JS的运⾏,设计,数据,应⽤四个⾓度来梳理JS核⼼的知识点主题⼤纲1. JS运⾏变量提升执⾏上下⽂作⽤域let作⽤域链闭包事件循环2. JS设计原型原型链thiscallapplybindnew继承3. JS数据数据类型数据的存储(深浅拷贝)数据类型判断(隐式转换,相等和全等,两个对象相等)数据的操作(数组遍历,对象遍历)数据的计算(计算误差)4. JS应⽤防抖,节流,柯⾥化⼀. JS运⾏⼤概分为四个阶段1. 词法分析:将js代码中的字符串分割为有意义的代码块,称为词法单元浏览器刚拿到⼀个JS⽂件或者⼀个script代码段的时候,它会认为⾥⾯是⼀个长长的字符串这是⽆法理解的,所以要分割成有意义的代码块,⽐如: var a = 12. 语法分析:将词法单元流转换成⼀颗抽象语法树(AST),并对⽣成的AST树节点进⾏处理,⽐如使⽤了ES6语法,⽤到了let,const,就要转换成var。
为什么需要抽象语法树呢?抽象语法树是不依赖于具体的,不依赖于语⾔的细节,⽅便做很多的操作另⼀⽅⾯说,现在有许多语⾔,C,C++,Java,Javascript等等,他们有不同的语⾔规范但是转化成抽象语法树后就都是⼀致的了,⽅便编译器对其进⾏进⼀步的增删改查等操作,3. 预解析阶段:会确定作⽤域规则变量和函数提升4. 执⾏阶段:创建执⾏上下⽂,⽣成执⾏上下⽂栈执⾏可执⾏代码,依据事件循环1.作⽤域指定了函数和变量的作⽤范围分为全局作⽤域和函数作⽤域,JS不像C,JAVA语⾔⼀样,没有块级作⽤域,简单说就是花括号的范围2.变量和函数提升全局变量和函数声明会提升函数声明⽅式有三种,function foo() {}var foo = function () {}var foo = new Function()可归为两类,直接创建和变量赋值变量赋值函数和赋值普通变量的优先级按位置来,变量名相同前者被覆盖函数直接创建优先级⾼于变量赋值,同名取前者,与位置⽆关,也就是说函数直接创建即使再变量声明后⾯,也是优先级最⾼3. 执⾏上下⽂有不同的作⽤域,就有不同的执⾏环境,我们需要来管理这些上下⽂的变量执⾏环境分为三种,执⾏上下⽂对应执⾏环境全局执⾏环境函数执⾏环境eval执⾏环境(性能问题不提)1. 全局执⾏上下⽂先找变量声明,再找函数声明2. 函数执⾏上下⽂先找函数形参,和变量声明把实参赋值给形参找函数声明多个函数嵌套,就会有多个执⾏上下⽂,这需要执⾏上下⽂栈来维护,后进先出执⾏上下⽂⾥包含着变量环境和词法环境变量环境⾥就包含着当前环境⾥可使⽤的变量当前环境没有⽤哪的, 这就说到了作⽤域链4. 作⽤域链引⽤JS⾼程的定义:作⽤域链来保证对执⾏环境有权访问的变量和函数的有序访问变量的查找顺序不是按执⾏上下⽂栈的顺序,⽽是由词法作⽤域决定的词法作⽤域也就是静态作⽤域,由函数声明的位置决定,和函数在哪调⽤⽆关,也就js这么特殊5. 静态作⽤域和动态作⽤域词法作⽤域是在写代码或者定义时确定的⽽动态作⽤域是在运⾏时确定的(this也是!)var a = 2;function foo() {console.log(a); // 静态2 动态3}function bar() {var a = 3;foo();}bar();复制代码闭包由于作⽤域的限制,我们⽆法在函数作⽤域外部访问到函数内部定义的变量,⽽实际需求需要,这⾥就⽤到了闭包引⽤JS权威指南定义:闭包是指有权访问另⼀个函数作⽤域中的变量的函数1. 闭包作⽤for循环遍历进⾏事件绑定输出i值时为for循环的长度+1这结果显⽰不是我们想要的, 因为JS没有块级作⽤域,var定义的i值,没有销毁,存储与全局变量环境中在事件具体执⾏的时候取的i值,就是全局变量中经过多次计算后的i值for(var i = 0;i < 3;i++){document.getElementById(`item${i+1}`).onclick = function() {console.log(i);//3,3,3}}复制代码闭包特性:外部函数已经执⾏结束,内部函数引⽤外部函数的变量依然保存在内存中,变量的集合可称为闭包在编译过程中,对于内部函数,JS引擎会做⼀次此法扫描,如果引⽤了外部函数的变量,堆空间创建换⼀个Closure的对象,⽤来存储闭包变量利⽤此特性给⽅法增加⼀层闭包存储当时的i值,将事件绑定在新增的匿名函数返回的函数上for(var i = 0;i < 3;i++){document.getElementById(`item${i+1}`).onclick = make(i);}function make(e) {return function() {console.log(e)//0,1,2};复制代码闭包注意我们在不经意间就写成了闭包,内部函数引⽤外部函数的变量依然保存在内存中,该销毁的没有销毁,由于疏忽或错误造成程序未能释放已经不再使⽤的内存,就造成了内存泄漏同时注意闭包不会造成内存泄漏,我们错误的使⽤闭包才是内存泄漏事件循环JS代码执⾏依据事件循环JS是单线程,通过异步保证执⾏不被阻塞1. 执⾏机制简单说就是,⼀个执⾏栈,两个任务队列发现宏任务就放⼊宏任务队列,发现微任务就放⼊微任务队列,执⾏栈为空时,执⾏微任务队列所有微任务,再取宏任务队列⼀个宏任务执⾏如此循环2. 宏&微任务 macroTask: setTimeout, setInterval, I/O, UI rendering microTask: Promise.then⼆. JS设计1. 原型1. JS的设计有new操作符,构造函数,却没有类的概念,⽽是使⽤原型来模拟类来实现继承2. JS设计⼼路历程JS在设计之初,给的时间较短,并且定义为简单的⽹页脚本语⾔,不⽤太复杂,且想要模仿Java的理念,(这也是为什么JS叫JavaScript的原因)因此就借鉴了Java的对象、构造函数、new操作符理念,⽽抛弃掉了了复杂的class(类)概念3. 继承机制需要有⼀种继承的机制,来把所有对象联系起来,就可以使⽤构造函数但是构造函数⽣成实例对象的缺点就是⽆法共享属性和⽅法4. prototype属性为解决上⾯问题,就引⼊了prototype属性,就是我们常说的原型为构造函数设置⼀个prototype属性,实例对象需要共享的⽅法,都放在此对象上,整个核⼼设计完成后,后⾯的API也就顺理成章原型每⼀个js对象在创建的时候就会与之关联另⼀个对象这个对象就是原型,每个对象都会从原型继承属性proto每个对象都有⼀个属性叫proto,该属性指向对象的原型构造函数的prototype属性等于实例化对象的proto属性此属性并不是ES5 中的规范属性,只是为了在浏览器中⽅便获取原型⽽做的⼀个语法糖,我们可以使⽤Object.getPrototype()⽅法获取原型constructor 原型没有指向实例,因为⼀个构造函数可以有多个对象实例但是原型指向构造函数是有的,每个原型都有⼀个constructor属性指向关联的构造函数function Per() {} // 构造函数const chi = new Per() // 实例对象chi.__proto__ === Per.prototype // 获取对象的原型也是就构造函数的prototype属性Per.prototype.constructor === Per // constructor属性获取当前原型关联的构造函数复制代码实例与原型读取实例属性找不到时,就会查找与对象关联的原型的属性,⼀直向上查找,这种实例与原型之间的链条关系,这就形成了原型链function Foo() {} = 'tom'const foo = new Foo() = 'Jerry'console.log(); // Jerrydelete console.log(); // tom复制代码2.原型链⾸先亮出⼤家熟悉的⽹图就是实例与构造函数,原型之间的链条关系实例的 proto 指向原型构造函数的 prototype 属性指向原型原型的 constructor 属性指向构造函数所有构造函数的 proto 指向 Function.prototypeFunction.prototype proto 指向 Object.prototypeObject.prototype proto 指向 null函数对象原型(Function.prototype)是负责造构造函数的机器,包含Object、String、Number、Boolean、Array,Function。
Javascript学习---2、执行环境,作用域作者:名刘天下来源:博客园发布时间:2010-12-10 17:03 阅读:155 次原文链接[收藏]在javascript的学习中,执行环境、作用域是2个非常非常重要和基本的概念,理解了这2个概念对于javsacript中很多脚本的运行结果就能明白其中的道理了,比如搞清作用域和执行环境对于闭包的理解至关重要。
一、执行环境(exection context,也有称之为执行上下文)所有JavaScript 代码都是在一个执行环境中被执行的。
执行环境是一个概念,一种机制,用来完成JavaScript运行时在作用域、生存期等方面的处理,它定义了变量或函数是否有权访问其他数据,决定各自行为。
在javascript中,可执行的JavaScript代码分三种类型:1. Global Code,即全局的、不在任何函数里面的代码,例如:一个js文件、嵌入在HTML页面中的js代码等。
2. Eval Code,即使用eval()函数动态执行的JS代码。
3. Function Code,即用户自定义函数中的函数体JS代码。
不同类型的JavaScript代码具有不同的执行环境,这里我们不考虑evel code,对应于global code和function code存在2种执行环境:全局执行环境和函数执行环境。
在一个页面中,第一次载入JS代码时创建一个全局执行环境,全局执行环境是最外围的执行环境,在Web浏览器中,全局执行环境被认为是window对象。
因此,所有的全局变量和函数都是作为window对象的属性和方法创建的。
当调用一个JavaScript 函数时,该函数就会进入与该函数相对应的执行环境。
如果又调用了另外一个函数(或者递归地调用同一个函数),则又会创建一个新的执行环境,并且在函数调用期间执行过程都处于该环境中。
当调用的函数返回后,执行过程会返回原始执行环境。
js基础知识点js基础知识点1、javascript概述JavaScript是一种弱类型,以对象为基础,定义的脚本语言,它可以给网页添加各种动态特性,JavaScript不需要编译就能运行,有丰富的内置函数和对象,可以在Web中读写数据,用于在Web中编写客户端脚本。
2、javascript应用JavaScript可以用在各种浏览器上,可以实现网页中的事件处理、表单处理、客户端检验、图形界面效果等。
通常JavaScript使用的目的是增强用户体验,提高网页的交互性和可用性3、javascript变量JavaScript变量有全局变量和局部变量。
它们的区别在于:1)全局变量在JavaScript脚本的所有部分都可以访问,而局部变量只能在声明它的函数内部访问。
2)局部变量只在函数内部有效,函数外部无法访问,而全局变量在函数内部外部都可以访问。
3)全局变量在整个网页的所有脚本都可以访问,只要这个网页被打开就可以,而局部变量只能在它声明的函数或语句块内访问。
4、javascript数据类型JavaScript支持6种数据类型:Undefined、Null、Boolean、Number、String 和Object。
Undefined 表示未定义,它的值是undefined。
Null 表示空值,它的值是null。
Boolean 表示布尔值,它的值是true或false。
Number表示数字,它的值是整数或小数String 表示字符串,它的值是由一系列字符组成的文本Object 表示对象,它的值是一组数据和功能的集合5、javascript函数JavaScript函数可以看作是一段可以重复使用的代码,它由一系列语句组成,用于完成特定的任务。
函数有四个特点:1)可以重复使用2)可以接收参数3)可以返回值4)可以封装函数的声明和调用函数可以使用function来声明:function functionName (parameters) {// code to be executed}函数使用functionName()来调用:functionName(parameters);函数默认有返回值,如果使用return语句来返回值,则忽略默认值,否则,函数返回值为undefined。
深⼊理解JavaScript,这⼀篇就够了前⾔ JavaScript 是我接触到的第⼆门编程语⾔,第⼀门是 C 语⾔。
然后才是 C++、Java 还有其它⼀些什么。
所以我对 JavaScript 是⾮常有感情的,毕竟使⽤它有⼗多年了。
早就想写⼀篇关于 JavaScript ⽅⾯的东西,但是在博客园中,写 JavaScript 的⽂章是最多的,从⼊门的学习笔记到⾼⼿的⼼得体会⼀应俱全,不管我怎么写,都难免落⼊俗套,所以迟迟没有动笔。
另外⼀个原因,也是因为在 Ubuntu 环境中⼀直没有找到很好的 JavaScript 开发⼯具,这种困境直到 Node.js 和 Visual Studio Code 的出现才完全解除。
⼗多年前,对 JavaScript 的介绍都是说他是基于对象的编程语⾔,⽽从没有哪本书会说 JavaScript 是⼀门⾯向对象的编程语⾔。
基于对象很好理解,毕竟在 JavaScript 中⼀切都是对象,我们随时可以使⽤点号操作符来调⽤某个对象的⽅法。
但是⼗多年前,我们编写 JavaScript 程序时,都是像 C 语⾔那样使⽤函数来组织我们的程序的,只有在论坛的某个⾓落中,有少数的⾼⼿会偶尔提到你可以通过修改某个对象的prototype来让你的函数达到更⾼层次的复⽤,直到 Flash 的 ActionScript 出现时,才有⼈系统介绍基于原型的继承。
⼗余年后的现在,使⽤JavaScript 的原型链和闭包来模拟经典的⾯向对象程序设计已经是⼴为流传的⽅案,所以,说 JavaScript 是⼀门⾯向对象的编程语⾔也丝毫不为过。
我喜欢 JavaScript,是因为它⾮常具有表现⼒,你可以在其中发挥你的想象⼒来组织各种不可思议的程序写法。
也许 JavaScript 语⾔并不完美,它有很多缺陷和陷阱,⽽正是这些很有特⾊的语⾔特性,让 JavaScript 的世界出现了很多奇技淫巧。
对象和原型链 JavaScript 是⼀门基于对象的编程语⾔,在 JavaScript 中⼀切都是对象,包括函数,也是被当成第⼀等的对象对待,这正是 JavaScript 极其富有表现⼒的原因。
JS作⽤域与作⽤域链详解(1)作⽤域⼀个变量的作⽤域(scope)是程序源代码中定义的这个变量的区域。
1. 在JS中使⽤的是词法作⽤域(lexical scope)不在任何函数内声明的变量(函数内省略var的也算全局)称作全局变量(global scope)在函数内声明的变量具有函数作⽤域(function scope),属于局部变量局部变量优先级⾼于全局变量复制代码代码如下:var name="one";function test(){var name="two";console.log(name); //two}test();函数内省略var的,会影响全局变量,因为它实际上已经被重写成了全局变量复制代码代码如下:var name="one";function test(){name="two";}test();console.log(name); //two函数作⽤域,就是说函数是⼀个作⽤域的基本单位,js不像c/c++那样具有块级作⽤域⽐如 if for 等复制代码代码如下:function test(){for(var i=0;i<10;i++){if(i==5){var name = "one";}}console.log(name); //one}test(); //因为是函数级作⽤域,所以可以访问到name="one"当然了,js⾥边还使⽤到了⾼阶函数,其实可以理解成嵌套函数复制代码代码如下:function test1(){var name = "one";return function (){console.log(name);}}test1()();test1()之后将调⽤外层函数,返回了⼀个内层函数,再继续(),就相应调⽤执⾏了内层函数,所以就输出 ”one"嵌套函数涉及到了闭包,后⾯再谈..这⾥内层函数可以访问到外层函数中声明的变量name,这就涉及到了作⽤域链机制2. JS中的声明提前js中的函数作⽤域是指在函数内声明的所有变量在函数体内始终是可见的。
4个作用域的作用范围作用域是JavaScript中非常重要的一个概念,有着不同的作用范围,涵盖了变量和函数的可见度。
作用域可以分为全局作用域、函数作用域、块级作用域和动态作用域,每一个作用域都有其独特的作用范围。
1.全局作用域全局作用域是最外层的作用域,它的作用范围是整个程序。
在全局作用域中定义的变量和函数都是全局可见的,可以在程序的任何地方调用和访问。
利用全局作用域可以定义一些在整个程序中使用的全局变量和函数,比如说:var globalVariable = "Hello, world!";function globalFunc() {console.log("This is a global function.");}2.函数作用域函数作用域是在函数内部定义的作用域,它的作用范围仅限于该函数内部。
在函数作用域中定义的变量和函数只可以在函数内部调用和访问,不可以在函数外部使用。
利用函数作用域可以在函数内部定义一些局部变量和函数,保持变量的封装性,不会对其他函数或全局造成影响,比如:function localFunc() {var localVariable = "This is a local variable.";console.log(localVariable);}localFunc();3.块级作用域块级作用域是在花括号“{}”内定义的作用域,它的作用范围仅限于该块级作用域内部。
在块级作用域中定义的变量和函数只可以在该块级作用域内部或块级作用域内部嵌套的作用域中调用和访问,不可以在块级作用域外部使用。
利用块级作用域可以避免变量污染,保持变量的局部性,比如:{let blockVariable = "This is a block-scoped variable.";console.log(blockVariable);}4.动态作用域动态作用域是在函数执行时才确定的作用域,其作用范围是函数调用栈。
javascript的运行原理JavaScript的运行原理什么是JavaScriptJavaScript是一种用于构建Web应用程序的高级编程语言。
它是一种解释性语言,意味着代码在运行时逐行被解释器解释执行。
JavaScript的运行环境JavaScript代码可以在多个环境中运行,最常见的是浏览器环境和环境。
浏览器环境在浏览器环境中,JavaScript代码被嵌入在HTML文档中,并由浏览器的JavaScript引擎处理和执行。
每个浏览器都有自己的JavaScript引擎,例如Chrome浏览器使用V8引擎。
环境是一个基于Chrome V8引擎的JavaScript运行时环境。
它允许JavaScript代码在服务器端运行,可以执行文件读写、网络请求等操作。
JavaScript的解释执行过程JavaScript的解释执行过程包括以下几个步骤:1.词法分析:将代码分解为一系列的词法单元(token),包括关键字、标识符、运算符等。
2.语法分析:将词法单元转换为抽象语法树(AbstractSyntax Tree, AST)。
语法分析器会根据语法规则检查代码的合法性,并构建语法树。
3.解释执行:遍历语法树,执行每个语法节点的操作。
解释器根据操作符执行相应的指令,包括赋值操作、函数调用等。
JavaScript的变量和作用域JavaScript使用var、let和const关键字声明变量。
变量的作用域可以分为全局作用域和函数作用域。
全局作用域在全局作用域中声明的变量可以在代码的任何地方被访问。
全局作用域中的变量在脚本的整个生命周期内都存在。
函数作用域在函数内部声明的变量只能在函数内部被访问。
函数作用域可以避免变量污染和命名冲突。
JavaScript的事件循环JavaScript是单线程语言,但它可以通过事件循环机制实现异步操作。
事件循环是JavaScript执行模型的重要组成部分。
任务队列任务队列存储待执行的任务。
作用域的名词解释作用域(Scope)是程序中一块定义变量的区域,在这个区域内,该变量是可见且可访问的。
作用域可以帮助我们更好地组织和管理变量,防止命名冲突和变量泄露,提升代码的可读性和可维护性。
作用域的概念是现代编程语言中一个重要的概念,几乎每种编程语言都有自己对作用域的实现方式。
在程序中,每个变量都有一个声明的位置,这个位置决定了变量的作用域。
作用域规定了变量可以被访问的范围。
在不同的作用域中,同名的变量可以有不同的值或指向不同的地址。
一般来说,作用域可以分为以下几种类型:全局作用域(Global Scope)、局部作用域(Local Scope)、块级作用域(Block Scope)和函数作用域(Function Scope)。
全局作用域是程序中最外层的作用域,它定义的变量在整个程序中都是可见且可访问的。
在全局作用域中定义的变量可以在程序的任何地方使用,但是需要注意的是,在函数内部也可以声明一个同名的局部变量与全局变量重名,这时函数内部会优先使用局部变量。
局部作用域是在函数或者代码块中定义的作用域,它只在定义它们的函数或代码块内部可见。
例如,在函数中定义的变量只能在函数内部访问和使用,函数外部无法访问它们。
这样,我们就可以使用同名的变量,而不会与外部作用域中的变量冲突。
块级作用域是一种特殊的作用域,它是在控制流程的大括号({})内声明的,例如在if语句、for循环、while循环、try-catch语句等。
在ES6之前,JavaScript 只有全局作用域和函数作用域,没有块级作用域,这就导致了一些问题,比如在循环中定义的变量可能会泄漏到循环外部。
但是在ES6引入的块级作用域能够解决这个问题,使得我们可以在块级作用域内部定义变量,而不会对外部造成影响。
函数作用域是指在函数内部定义的作用域,函数作用域可以看作是一种局部作用域的特例。
在函数内部定义的变量对外部是不可见的,但是函数外部定义的变量在函数内部是可见且可访问的。
javascript说课稿《Javascript 说课稿》尊敬的各位评委、老师:大家好!今天我说课的题目是《Javascript》。
下面我将从教材分析、学情分析、教学目标、教学重难点、教学方法、教学过程以及教学反思这几个方面来展开我的说课。
一、教材分析本次授课所选用的教材是_____出版社出版的《_____》。
Javascript 是 Web 开发中非常重要的一门编程语言,在教材中处于重要的地位。
本教材对Javascript 的讲解系统全面,涵盖了基本语法、数据类型、控制结构、函数、对象等核心内容。
通过实例和练习,帮助学生逐步掌握 Javascript 的编程技能,提高解决实际问题的能力。
二、学情分析本次授课的对象是_____专业的学生,他们已经具备了一定的计算机基础知识和编程思维。
但对于 Javascript 这门语言,大多数学生还处于初步接触的阶段。
学生在学习过程中可能会遇到以下困难:一是对 Javascript 中一些抽象的概念理解困难,如对象、原型链等;二是在实际编程中,难以将所学的知识灵活运用,解决复杂的问题。
然而,学生具有较强的好奇心和求知欲,对实际应用感兴趣。
如果能够引导他们通过实践来理解和掌握知识,将能够激发他们的学习积极性。
三、教学目标基于对教材和学情的分析,我制定了以下教学目标:1、知识与技能目标学生能够掌握 Javascript 的基本语法,包括变量、数据类型、运算符、控制结构等。
学生能够熟练使用函数进行代码的封装和复用。
学生能够理解对象和数组的概念,并能够进行基本的操作。
2、过程与方法目标通过实际案例的分析和实践,培养学生的逻辑思维和问题解决能力。
引导学生通过自主探究和合作学习,提高学生的学习能力和团队协作能力。
3、情感态度与价值观目标激发学生对 Javascript 编程的兴趣,培养学生的创新精神和实践能力。
让学生在编程过程中体验到成功的喜悦,增强学生的自信心和学习动力。
优就业JS教程-理解JavaScript中的作用域和上下文 Java对于作用域(Scope)和上下文(Context)的实现是这门语言的一个非常独到的地方,部分归功于其独特的灵活性。 函数可以接收不同的的上下文和作用域。这些概念为Java中的很多强大的设计模式提供了坚实的基础。 然而这也概念也非常容易给开发人员带来困惑。为此,本文将全面的剖析这些概念,并阐述不同的设计模式是如何利用它们的。 Statement 作者: 景庄,Web开发者,主要关注Java、Node.js、React、Docker等。 上下文(Context)和作用域(Scope) 首先需要知道的是,上下文和作用域是两个完全不同的概念。多年来,我发现很多开发者会混淆这两个概念(包括我自己), 错误的将两个概念混淆了。平心而论,这些年来很多术语都被混乱的使用了。 函数的每次调用都有与之紧密相关的作用域和上下文。从根本上来说,作用域是基于函数的,而上下文是基于对象的。 换句话说,作用域涉及到所被调用函数中的变量访问,并且不同的调用场景是不一样的。上下文始终是this关键字的值, 它是拥有(控制)当前所执行代码的对象的引用。 变量作用域 一个变量可以被定义在局部或者全局作用域中,这建立了在运行时(runtime)期间变量的访问性的不同作用域范围。 任何被定义的全局变量,意味着它需要在函数体的外部被声明,并且存活于整个运行时(runtime),并且在任何作用域中都可以被访问到。 在ES6之 前,局部变量只能存在于函数体中,并且函数的每次调用它们都拥有不同的作用域范围。 局部变量只能在其被调用期的作用域范围内被赋值、检索、操纵。 需要注意,在ES6之前,Java不支持块级作用域,这意味着在if语句、switch语句、for循环、while循环中无法支持块级作用域。 也就是说,ES6之前的Java并不能构建类似于Java中的那样的块级作用域(变量不能在语句块外被访问到)。但是, 从ES6开始,你可以通过let关键字来定义变量,它修正了var关键字的缺点,能够让你像Java语言那样定义变量,并且支持块级作用域。看两个例子: ES6之前,我们使用var关键字定义变量: function func() { if (true) { var tmp = 123; } console.log(tmp); // 123 } 之所以能够访问,是因为var关键字声明的变量有一个变量提升的过程。而在ES6场景,推荐使用let关键字定义变量: function func() { if (true) { let tmp = 123; } console.log(tmp); // ReferenceError: tmp is not defined } 这种方式,能够避免很多错误。 什么是this上下文 上下文通常取决于函数是如何被调用的。当一个函数被作为对象中的一个方法被调用的时候,this被设置为调用该方法的对象上: var obj = { foo: function(){ alert(this === obj); } }; obj.foo(); // true 这个准则也适用于当调用函数时使用new操作符来创建对象的实例的情况下。在这种情况下,在函数的作用域内部this的值被设置为新创建的实例: function foo(){ alert(this); } new foo() // foo foo() // window 当调用一个为绑定函数时,this默认情况下是全局上下文,在浏览器中它指向window对象。需要注意的是,ES5引入了严格模式的概念, 如果启用了严格模式,此时上下文默认为undefined。 执行环境(execution context) Java是一个单线程语言,意味着同一时间只能执行一个任务。当Java解释器初始化执行代码时, 它首先默认进入全局执行环境(execution context),从此刻开始,函数的每次调用都会创建一个新的执行环境。 这里会经常引起新手的困惑,这里提到了一个新的术语——执行环境(execution context),它定义了变量或函数有权访问的其他数据,决定了它们各自的行为。 它更偏向于作用域的作用,而不是我们前面讨论的上下文(Context)。请务必仔细的区分执行环境和上下文这两个概念(注:英文容易造成混淆)。 说实话,这是个非常糟糕的命名约定,但是它是ECMA规范制定的,你还是遵守吧。 每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中(execution stack)。在函数执行完后,栈将其环境弹出, 把控制权返回给之前的执行环境。ECMA程序中的执行流正是由这个便利的机制控制着。 执行环境可以分为创建和执行两个阶段。在创建阶段,解析器首先会创建一个变量对象(variable object,也称为活动对象 activation object), 它由定义在执行环境中的变量、函数声明、和参数组成。在这个阶段,作用域链会被初始化,this的值也会被最终确定。 在执行阶段,代码被解释执行。 每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。 需要知道,我们无法手动访问这个对象,只有解析器才能访问它。 作用域链(The Scope Chain) 当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。 作用域链包含了在环境栈中的每个执行环境对应的变量对象。通过作用域链,可以决定变量的访问和标识符的解 析。 注意,全局执行环境的变量对象始终都是作用域链的最后一个对象。我们来看一个例子: var color = "blue"; function changeColor(){ var anotherColor = "red"; function swapColors(){ var tempColor = anotherColor; anotherColor = color; color = tempColor; // 这里可以访问color, anotherColor, 和 tempColor } // 这里可以访问color 和 anotherColor,但是不能访问 tempColor swapColors(); } changeColor(); // 这里只能访问color console.log("Color is now " + color); 上述代码一共包括三个执行环境:全局环境、changeColor()的局部环境、swapColors()的局部环境。 上述程序的作用域链如下图所示: 从上图发现。内部环境可以通过作用域链访问所有的外部环境,但是外部环境不能访问内部环境中的任何变量和函数。 这些环境之间的联系是线性的、有次序的。 对于标识符解析(变量名或函数名搜索)是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始, 然后逐级地向后(全局执行环境)回溯,直到找到标识符为止。 闭包 闭包是指有权访问另一函数作用域中的变量的函数。换句话说,在函数内定义一个嵌套的函数时,就构成了一个闭包, 它允许嵌套函数访问外层函数的变量。通过返回嵌套函数,允许你维护对外部函数中局部变量、参数、和内函数声明的访问。 这种封装允许你在外部作用域中隐藏和保护执行环境,并且暴露公共接口,进而通过公共接口执行进一步的操作。可以看个简单的例子: function foo(){ var localVariable = 'private variable'; return function bar(){ return localVariable; } } var getLocalVariable = foo(); getLocalVariable() // private variable 模块模式最流行的闭包类型之一,它允许你模拟公共的、私有的、和特权成员: var Module = (function(){ var privateProperty = 'foo'; function privateMethod(args){ // do something } return { publicProperty: '', publicMethod: function(args){ // do something }, privilegedMethod: function(args){ return privateMethod(args); } }; })(); 模块类似于一个单例对象。由于在上面的代码中我们利用了(function() { ... })();的匿名函数形式,因此当编译器解析它的时候会立即执行。 在闭包的执行上下文的外部唯一可以访问的对象是位于返回对象中的公共方法和属性。然而,因为执行上下文被保存的缘故, 所 有的私有属性和方法将一直存在于应用的整个生命周期,这意味着我们只有通过公共方法才可以与它们交互。 另一种类型的闭包被称为立即执行的函数表达式(IIFE)。其实它很简单,只不过是一个在全局环境中自执行的匿名函数而已: (function(window){ var foo, bar; function private(){ // do something } window.Module = { public: function(){ // do something } }; })(this); 对于保护全局命名空间免受变量污染而言,这种表达式非常有用,它通过构建函数作用域的形式将变量与全局命名空间隔离, 并通过闭包的形式让它们存在于整个运行时(runtime)。在很多的应用和框架中,这种封装源代码的方式用处非常的流行, 通常都是通过暴露一个单一的全局接口的方式与外部进行交互。 Call和Apply