执行环境,作用域理解
JS核心知识梳理

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。
js中VO解析

js中VO解析执⾏环境(环境)执⾏环境的⽤处执⾏环境定义了变量或函数有权访问的其他数据,每⼀个执⾏环境都存在⼀个关联的变量对象(VO),代码⽆法访问,内部解析器会使⽤它,如果环境为函数,则将函数的AO作为VO,函数执⾏时,会创建⼀个以当前函数VO为前端的作⽤域链,以保证执⾏环境有权访问的所有变量和函数的有序访问。
顶级AO对象全局全局对象(Global object) 是在进⼊任何执⾏上下⽂之前就已经创建了的对象;这个对象只存在⼀份,它的属性在程序中任何地⽅都可以访问,全局对象的⽣命周期终⽌于程序退出那⼀刻。
全局对象初始化全局对象初始创建阶段将Math、String、Date、parseInt作为⾃⾝属性,等属性初始化,同样也可以有额外创建的其它对象作为属性(其可以指向到全局对象⾃⾝)。
例如,在DOM中,全局对象的window属性就可以引⽤全局对象⾃⾝(当然,并不是所有的具体实现都是这样)global = {Math: <...>,String: <...>......window: global //引⽤⾃⾝};证据Chrome打印global对象时,显⽰了Window类型对象,⾥⾯有⼀个属性叫window就是global本⾝可以看到global和global.window是指向同⼀对象全局对象访问// 相当于 global.String(10)String(10);// 相当于 global.window.a = 10 === global.a = 10window.a = 10;// 相当于 // global.b = 20;this.b = 20;函数的VO对象AO函数AO对象⽣成过程函数执⾏前1.分析参数1.1 函数接收参数,添加到AO的属性上⾯,值被初始化为undefined1.2 接收实参,形成AO对应的属性值2.分析变量声明,如 var a,2.1:如果AO上还没有a属性键名,则添加a属性键名并且初始化为undefined2.2:如果AO 上⾯已经有了a属性,则不做任何操作3.分析函数的声明,如果 funcion a(){},3.1:⽆论是否该函数名存在,都会覆盖当前函数名对应的属性函数执⾏时4.对已经存在的变量声明进⾏赋值操作实例function test(a, b) {var c = 10;function d() {}var e = function _e() {};(function x() {});}test(10); // call1.分析参数1.1 函数接收参数,添加到AO的属性上⾯,值被初始化为undefinedAO(test) = {a: undefined,b: undefined}1.2 接收实参,形成AO对应的属性值AO(test) = {a: 10,b: undefined}2.分析变量声明,如 var a,2.1:如果AO上还没有a属性键名,则添加a属性键名并且初始化为undefined AO(test) = {a: 10,b: undefined,c: undefined,e: undefined}2.2:如果AO 上⾯已经有了a属性,则不做任何操作3.分析函数的声明,如果 funcion a(){},3.1:⽆论是否该函数名存在,都会覆盖当前函数名对应的属性AO(test) = {a: 10,b: undefined,c: undefined,d: <reference to FunctionDeclaration "d">,e: undefined}函数执⾏时4.对已经存在的变量声明进⾏赋值操作AO(test) = {a: 10,b: undefined,c: 10,d: <reference to FunctionDeclaration "d">,e: <reference to FunctionExpression "_e">}。
js底层原理

js底层原理JavaScript(简称js)是一种轻量级、解释型的编程语言,主要用于在网页上实现交互功能。
在学习和使用JavaScript的过程中,了解其底层原理是非常重要的。
本文将介绍JavaScript底层原理的相关知识,希望能够帮助读者更好地理解和运用JavaScript。
首先,让我们来了解一下JavaScript的执行环境。
在浏览器中,JavaScript引擎负责解释和执行JavaScript代码。
常见的浏览器中的JavaScript引擎包括V8(Chrome浏览器)、SpiderMonkey(Firefox浏览器)、Chakra(Edge浏览器)等。
这些引擎在执行JavaScript代码时,会将其转换为字节码或机器码,然后由计算机进行执行。
接下来,我们来看一下JavaScript的数据类型。
JavaScript中的数据类型分为基本数据类型和引用数据类型。
基本数据类型包括undefined、null、boolean、number、string和symbol(ES6新增),它们存储在栈内存中,通过按值访问。
引用数据类型包括object、array、function等,它们存储在堆内存中,通过引用地址访问。
了解数据类型的存储方式有助于我们更好地理解JavaScript的内存管理和数据操作。
此外,JavaScript的作用域和闭包也是我们需要了解的重要内容。
作用域指的是变量的可访问范围,JavaScript采用的是词法作用域,也就是静态作用域。
而闭包是指函数和函数内部引用的变量组成的组合,它可以让我们访问外部函数的变量,实现一些特殊的功能。
理解作用域和闭包有助于我们写出更加优雅和高效的JavaScript代码。
另外,JavaScript的原型和原型链也是JavaScript底层原理中的重要内容。
JavaScript中的对象是基于原型的,每个对象都有一个原型对象,而原型对象又有自己的原型对象,形成了原型链。
javascript 核心原理pdf

javascript 核心原理pdf全文共四篇示例,供读者参考第一篇示例:JavaScript是一种广泛应用于前端开发的编程语言,它是实现Web页面交互性的重要工具。
要想掌握JavaScript编程,了解其核心原理至关重要。
本文将深入探讨JavaScript的核心原理,并提供一份《JavaScript核心原理PDF》供读者参考。
JavaScript的核心原理主要包括以下几个方面:数据类型、变量、运算符、控制流程、函数、对象、数组、闭包等。
首先我们来介绍JavaScript的数据类型。
JavaScript有七种基本数据类型,分别是字符串(String)、数字(Number)、布尔值(Boolean)、undefined、null、Symbol和BigInt。
除了基本数据类型外,JavaScript还有一种复杂数据类型——对象(Object),对象是一种无序的数据集合,包含键值对。
变量是存储数据的容器,使用var、let或const关键字声明一个变量。
JavaScript支持赋值运算符(=)、算术运算符(+、-、*、/等)、比较运算符(==、!=、>、<等)、逻辑运算符(&&、||、!等)等。
控制流程是编程中的基本元素,主要包括条件语句(if、else if、else)、循环语句(for、while、do while)、跳转语句(break、continue、return)等。
函数是JavaScript中的重要概念,函数是一段可重复使用的代码块,可以接受参数并返回值。
JavaScript中的函数可以嵌套定义,函数也是一种对象,因此函数可以作为对象的属性。
JavaScript中还有一种特殊的函数——匿名函数,匿名函数没有名称,通常用于定义回调函数。
对象是JavaScript编程中的核心概念,对象是一种复杂数据类型,是由键值对组成的无序集合。
JavaScript中的对象可以是内置对象(如Array、Math、Date等)、宿主对象(由JavaScript外部环境提供的对象,如浏览器对象)或自定义对象。
Web前端开发技术学习方式探讨

1前端开发常用技术概述HTML、CSS、JavaScript是Web前端开发的3大技术核心,简单说就是用HTML进行页面的结构排版,用CSS进行页面布局和样式的设计,用JavaScript实现页面的交互和数据请求。
1.1HTML5HTML5是万维网的核心与通用语言,HTML5在2008年正式发布,在2012年形成稳定版本,相比HTML4, HTML5增加了图形绘制、多媒体等元素和功能, HTML5是Web前端开发者需要掌握的首要技术。
1.2CSS3CSS即层叠样式表,它可以对网页布局、元素等进行精确控制,CSS3是CSS技术的升级版本,主要包括盒子模型、多栏目布局等模块[2]。
Div+CSS布局的优点非常多,比如可以有效减少页面代码量并提高页面显示速度,代码结构清晰明了,更容易被搜索引擎收录[3]。
1.3JavaScriptJavaScript是一种Web前端的弱语言类型、解释型语言,是一种基于事件和对象驱动的、安全性好的脚本语言。
它在客户端运行(node.js例外),是一种通用的,不需要在专门的语言环境下运行,在插入HTML页面后,可由所有的现代浏览器执行[4]。
1.4jQueryjQuery是一个快速、轻量级、兼容多浏览器且功能丰富的JavaScript库。
它使用户的像操作文档对象、选择文档对象模型元素、创建动画效果、处理事件、以及开发Ajax程序等的网页操作变得更加容易,同时赋予了开发人员提供了在网页上创建插件的能力。
1.5Bootstrap框架Bootstrap框架简洁灵活,兼容绝大部分的JQuery 插件,形成了一套独特清新的网站显示风格并风靡一时。
Bootstrap最精彩的部分就是响应式布局,通过响应式布局可以实现只用一套代码就可以完美支持PC端、PAD及手机移动端的页面访问[5]。
1.6AjaxAjax即异步的JavaScript和XML,它是按需加载数据,而不再像传统网页一样要更新一点点内容,就得重新加载整个网页。
JSinDeep1_探索执行环境_ES3

JSinDeep1:探索执行环境(Execution Context)-ES3篇声明JSinDeep系列文章主要内容为作者对ECMA-262标准中一些概念的理解和探究,同时意在帮助大家快速理解。
本着严谨的态度,同时又需避免API式的枯燥细节罗列。
文章会以适当插图、例子去诠释概念,致力于通俗易懂。
更具体、严谨、完整的描述建议阅读ECMA262文档。
碍于作者水平有限,文中若有错误,欢迎大家批评指正。
概要在我们写JavaScript代码时会定义一些变量、函数等。
解释器在执行这些代码时是如何处理并找到我们定义的这些数据的?在程序执行时,引用这些变量等操作的背后都发生了什么?本文主要探讨ECMA-262-3标准中的执行环境(Execution Context)及与之相关的一些内部机制和模型。
定义当程序执行的控制权转移至ECMAScript可执行代码时,会进入到一个执行环境中(Execution Context,缩写为EC)。
在一个EC内也可能进入到一个新的EC,这些EC逻辑上○1会形成一个栈(Stack)。
EC是程序运行时动态创建的。
例如:每一个函数在被调用时都会创建一个EC,重复调用函数(包含递归调用的情形)会重新创建新的EC,而后放置在逻辑栈中。
逻辑栈会在程序运行时随着新的函数调用、函数return、未处理的异常抛出等情况动态变化,但逻辑栈的最顶部总是当前正运行的EC,它的最底部总是全局EC(Global Context)。
图:运行时的逻辑EC栈○1这里的“逻辑上”是因ECMA262标准避免限制实现者的思路,具体实现在遵循标准的前提下不受其它限制。
因此这里所说的逻辑上的栈在具体实现时未必是通常意义上的栈。
后面我们简称为:“逻辑栈”。
可执行代码的分类由定义部分可知,“每一段可执行代码都有对应的EC”,为方便按不同情况讨论,先了解几种可执行代码的类型。
a). 全局代码全局代码是指在任何被解析为函数体的代码以外的最外层的代码。
详细图解作用域链与闭包

详细图解作⽤域链与闭包因此本⽂的⽬的就在于,能够清晰明了得把闭包说清楚。
⼀、作⽤域与作⽤域链在详细讲解作⽤域链之前,我默认你已经⼤概明⽩了JavaScript中的下⾯这些重要概念。
这些概念将会⾮常有帮助。
基础数据类型与引⽤数据类型内存空间垃圾回收机制执⾏上下⽂变量对象与活动对象作⽤域在JavaScript中,我们可以将作⽤域定义为⼀套规则,这套规则⽤来管理引擎如何在当前作⽤域以及嵌套的⼦作⽤域中根据标识符名称进⾏变量查找。
这⾥的标识符,指的是变量名或者函数名JavaScript中只有全局作⽤域与函数作⽤域(因为eval我们平时开发中⼏乎不会⽤到它,这⾥不讨论)。
作⽤域与执⾏上下⽂是完全不同的两个概念。
我知道很多⼈会混淆他们,但是⼀定要仔细区分。
JavaScript代码的整个执⾏过程,分为两个阶段,代码编译阶段与代码执⾏阶段。
编译阶段由编译器完成,将代码翻译成可执⾏代码,这个阶段作⽤域规则会确定。
执⾏阶段由引擎完成,主要任务是执⾏可执⾏代码,执⾏上下⽂在这个阶段创建。
作⽤域链⽣命周期,如下图。
执⾏上下⽂⽣命周期我们发现,作⽤域链是在执⾏上下⽂的创建阶段⽣成的。
这个就奇怪了。
上⾯我们刚刚说作⽤域在编译阶段确定规则,可是为什么作⽤域链却在执⾏阶段确定呢?之所有有这个疑问,是因为⼤家对作⽤域和作⽤域链有⼀个误解。
我们上⾯说了,作⽤域是⼀套规则,那么作⽤域链是什么呢?是这套规则的具体实现。
所以这就是作⽤域与作⽤域链的关系,相信⼤家都应该明⽩了吧。
我们知道函数在调⽤激活时,会开始创建对应的执⾏上下⽂,在执⾏上下⽂⽣成的过程中,变量对象,作⽤域链,以及this的值会分别被确定。
之前⼀篇⽂章我们详细说明了变量对象,⽽这⾥,我们将详细说明作⽤域链。
作⽤域链,是由当前环境与上层环境的⼀系列变量对象组成,它保证了当前执⾏环境对符合访问权限的变量和函数的有序访问。
为了帮助⼤家理解作⽤域链,我我们先结合⼀个例⼦,以及相应的图⽰来说明。
作用域链的理解

作用域链的理解作用域链是JavaScript中一个重要的概念,是许多程序员的福音。
它有助于我们更好地理解JavaScript代码的执行过程。
在本文中,我们将向您介绍作用域链及其相关概念,帮助您更好地理解它们的工作原理。
步骤1:理解作用域作用域是一个定义变量和函数的区域。
在JavaScript中,有局部作用域和全局作用域。
函数内定义的变量只能在该函数内部访问,称为局部变量,而在函数外部定义的变量和函数则称为全局变量和函数。
步骤2:理解词法作用域词法作用域是由代码中变量和函数声明的位置决定的,也称为静态作用域。
在JavaScript中,使用var关键字声明的变量,会被限制在其定义所在的函数内部或全局作用域中。
而使用let、const关键字声明的变量,则受块级作用域限制。
步骤3:了解作用域链在JavaScript中,每个函数都有一个作用域,作用域中包含局部变量、参数、全局变量和外部封闭函数的变量。
当函数访问一个变量时,JavaScript会首先查找当前函数作用域中是否有该变量;如果没有找到,则会沿着函数创建时的作用域链向上查找,直到找到该变量所在的作用域为止。
如果在全局作用域中也找不到该变量,就会抛出引用错误。
步骤4:深入了解作用域链当一个函数被调用时,JavaScript会创建一个新的执行环境。
该执行环境会包含一个变量对象(VO),其中包含该函数内定义的所有变量和函数。
函数的作用域链是在创建该执行环境时进行设置的。
作用域链中包含了本地变量对象和所有外部封闭函数的变量对象。
在本地变量对象中定义的变量和函数名将优先于外部函数中定义的变量和函数名。
步骤5:结合实例理解作用域链我们来看一个简单示例,帮助您更好地理解作用域链的工作原理:```var name = "Alice";function greet() {var name = "Bob";function sayHello() {var message = "Hello, " + name + "!";console.log(message);}sayHello();}greet();```在这个示例中,我们定义了一个全局变量name,以及一个greet函数和sayHello函数。
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
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 函数时,该函数就会进入与该函数相对应的执行环境。
如果又调用了另外一个函数(或者递归地调用同一个函数),则又会创建一个新的执行环境,并且在函数调用期间执行过程都处于该环境中。
当调用的函数返回后,执行过程会返回原始执行环境。
因而,运行中的JavaScript 代码就构成了一个执行环境栈。
function Fn1(){
function Fn2(){
alert(document.body.tagName);//BODY
//other code...
}
Fn2();
}
Fn1();
//code here
(图1 执行环境栈摘自:笨蛋的座右铭的博文)
程序在进入每个执行环境的时候,JavaScript引擎在内部创建一个对象,叫做变量对象(Variable Object)。
对应函数的每一个参数,在Variable Object上添加一个属性,属性的名字、值与参数的名字、值相同。
函数中每声明一个变量,也会在Variable Object上添加一个属性,名字就是变量名,因此为变量赋值就是给Variable Object 对应的属性赋值。
在函数中访问参数或者局部变量时,就是在variable Object上搜索相应的属性,返回其值。
(另外注意:一般情况下Variable Object是一个内部对象,JS 代码中无法直接访问。
规范中对其实现方式也不做要求,因此它可能只是引擎内部的一种数据结构。
)
大致处理方式就这样,但作用域的概念不只这么简单,例如函数体中可以使用全局变量、函数嵌套定义时情况更复杂点。
这些情况下怎样处理?JavaScript引擎将不同执行位置上的Variable Object按照规则构建一个链表,在访问一个变量时,先在链表的第1个Variable Object上查找,如果没有找到则继续在第2个Variable Object上查找,直到搜索结束。
这就是Scope/Scope Chain的大致概念。
二、Scope/Scope Chain(作用域/作用域链)
当代码在一个环境中执行时,都会创建基于Variable Object的一个作用域链。
作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。
整个作用域链是由不同执行位置上的Variable Object按照规则所构建一个链表。
作用域链的最前端,始终是当前正在执行的代码所在环境的Variable Object。
如果这个环境是函数(比如Fn2),则将其活动对象(activation object)作为变量对象。
活动对象在最开始时只包含一个变量,就是函数内部的arguments对象。
作用域链中的下一个Variable Object来自该函数(Fn2)的包含环境(也就是Fn1),而再下一个Variable object来自再下一个包含环境。
这样,一直延续到全局执行环境,全局执行环境的Variable Object始终是作用域链中的最后一个对象。
如上所述,作用域链感觉就是一个Variable Object链表,当访问一个变量时,先在链表的第一个Variable Object(最前端)上查找,如果没有找到则继续在第二个Variable Object上查找,直到搜索结束,也就是搜索到全局执行环境的Variable Object中。
这也就形成了Scope Chain的概念。
与上面Code1代码对应的作用域链图如下所示(摘自:笨蛋的座右铭的博文,这个图感觉不是很理想,不如下面的图3更形象,把右侧这部分调个个就好了。
)
(图2 作用域链图)
如上图所示,对于Code1代码中的函数Fn2所对应的作用域链为:Fn2 Variable Object->Fn1 variable Object->全局对象。
也就是说,在Fn2函数中进行变量访问时,首先会在Fn2 Variable object访问内进行寻找,如果没有找到,则向上,搜索Fn1 Variable Object中是否存在,直至找到,停止搜索。
再拿《javascript高级程序设计第二版》中提到的例子来说明一下。
代码如下所示:
在Code2代码中,涉及3个执行环境全局环境、changecolor函数的局部环境和swapcolor局部环境。
该段代码的作用域链如下图所示。
(图3摘自:《javascript高级程序设计第二版》)
上图中,
∙-》全局环境有1个变量color和1个函数changecolor()。
∙-》changecolor()函数的局部环境中具有1个anothercolor属性和1个swapcolors函数,当然,changecolor函数中可以访问自身以及它外围(也
就是全局环境)中的变量。
∙-》swapcolor()函数的局部环境中具有1个变量tempcolor。
在该函数内部可以访问上面的2个环境(changecolor和window)中的所有变量,因为那2个环境都是它的父执行环境。
通过上面的分析,我们可以得知内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。
这些环境之间是线性、有次序的。
每个环境都可以向上搜索作用域链,以便查询变量和函数名;但任何环境不能通过向下搜索作用域链条而进入另一个执行环境。
对于上述例子的swapcolor()函数而言,其作用域链包括:swapcolor()的变量对象、changecolor()变量对象和全局对象。
swapcolor()的局部环境开始先在自己的Variable Object中搜索变量和函数名,找不到,则向上搜索changecolor作用域链。
以此类推。
但是,changecolor()函数是无法访问swapcolor中的变量。
关于作用域总结以下几条:
1、javascript没有块级作用域。
上述代码运行后会返回10,为什么呢?如果是同样的java或是c#代码,则不会是10,可能会提示运行错误,因为i只存在于for循环体重,在运行完for循环后,for 中的所有变量就被销毁了。
而在javascript中则不是这样的,在for中的变量声明将会
添加到当前的执行环境中(这里是全局执行环境),因此在for循环完后,变量i依旧存在于循环外部的执行环境。
因此,会输出10。
2、声明变量
使用var声明变量时,这个变量将被自动添加到距离最近的可用环境中。
对于函数而言,自然声明的变量就会被添加到函数的局部环境中,变量在整个函数环境内都是可用的。
但是,如果变量没有是用var进行声明,将会被添加到全局环境,也就是说成位全局变量了。
所以在函数体内,进行声明时,一般要在开头用var进行声明。
最后出几个小例题:
为什么会是代码中所说明的结果呢?
我认为和2个事情有关:作用域和预解析。
我们可以很容易得出上述代码的作用域链。
window全局环境和rain()函数局部环境。
window全局环境中存在全局变量x 和rain,而rain()函数的局部环境中包括:局部变量x。
因此,在执行rain()函数时,不可能去访问全局变量x的了,因为在当前的rain()函数内已经有局部变量x。
所以alert出1,
但为什么第一次alert出undefined呢?这可能和预解析有关。
在代码执行时,首先会解析出变量和函数的定义,上述代码等价于下面的代码。