作用域

域,表示的是一个范围,作用域,就是作用范围。

作用域说明的是一个变量可以在什么地方被使用,什么地方不能被使用。

块级作用域

JavaScript中没有块级作用域

{
var num = 123;
{
console.log( num );
}
}
console.log( num );

上面这段代码在JavaScript中是不会报错的,但是在其他的编程语言中(C#、C、JAVA)会报错。

这是因为,在JavaScript中没有块级作用域,使用{}标记出来的代码块中声明的变量num,是可以被{}外面访问到的。

但是在其他的编程语言中,有块级作用域,那么{}中声明的变量num,是不能在代码块外部访问的,所以报错。

词法作用域

什么是词法作用域?

词法( 代码 )作用域, 就是代码在编写过程中体现出来的作用范围. 代码一旦写好, 不用执行, 作用范围就已经确定好了. 这个就是所谓词法作用域.

在 js 中词法作用域规则:

  • 函数允许访问函数外的数据.

  • 整个代码结构中只有函数可以限定作用域.

  • 作用域规则首先使用提升规则分析

  • 如果当前作用规则中有名字了, 就不考虑外面的名字

例子1:

var num = 123;

function foo() {

    console.log( num );

}

foo();

例子2:

if ( false ) {
var num = 123;
}
console.log( num ); // undefiend

例子3:

var num = 123;
function foo() {
var num = 456;
function func() {
console.log( num );
} func();
} foo();

练习:

var num1 = 123;
function foo1() {
var num1 = 456;
function foo2() {
num1 = 789;
function foo3 () {
console.log( num1 );
}
foo3();
}
foo2();
}
foo1();
console.log( num1 );

面试题

var num = 123;
function func1(){
console.log(num);
} function func2(){
var num = 456;
func1();
}

JavaScript是解释型的语言,但是他并不是真的在运行的时候逐句的往下解析执行。

我们来看下面这个例子:

func();

function func(){
alert("Funciton has been called");
}

在上面这段代码中,函数func的调用是在其声明之前,如果说JavaScript代码真的是逐句的解析执行,那么在第一句调用的时候就会出错,然而事实并非如此,上面的代码可以正常执行,并且alert出来Function has been called

所以,可以得出结论,JavaScript并非仅在运行时简简单单的逐句解析执行!

JavaScript 预解析

JavaScript引擎在对JavaScript代码进行解释执行之前,会对JavaScript代码进行预解析,在预解析阶段,会将以关键字varfunction开头的语句块提前进行处理。

关键问题是怎么处理呢?

当变量和函数的声明处在作用域比较靠后的位置的时候,变量和函数的声明会被提升到作用域的开头。

重新来看上面的那段代码

func();
function func(){
alert("Funciton has been called");
}

由于JavaScript的预解析机制,上面的代码就等效于:

function func(){
alert("Funciton has been called");
}
func();

看完函数声明的提升,再来看一个变量声明提升的例子:

alert(a);
var a = 1;

由于JavaScript的预解析机制,上面这段代码,alert出来的值是undefined,如果没有预解析,代码应该会直接报错a is not defined,而不是输出值。

Wait a minute, 不是说要提前的吗?那不是应该alert出来1,为什么是undefined?

那么在这里有必要说一下声明定义初始化的区别。其实这几个概念是C系语言的人应该都比较了解的。

行为 说明
声明 告诉编译器/解析器有这个变量存在,这个行为是不分配内存空间的,在JavaScript中,声明一个变量的操作为:var a;
定义 为变量分配内存空间,在C语言中,一般声明就包含了定义,比如:int a;,但是在JavaScript中,var a;这种形式就只是声明了。
初始化 在定义变量之后,系统为变量分配的空间内存储的值是不确定的,所以需要对这个空间进行初始化,以确保程序的安全性和确定性
赋值 赋值就是变量在分配空间之后的某个时间里,对变量的值进行的刷新操作(修改存储空间内的数据)

所以我们说的提升,是声明的提升。

那么再回过头看,上面的代码就等效于:

var a; //这里是声明
alert(a);//变量声明之后并未有初始化和赋值操作,所以这里是 undefined
a = 1;

复杂点的情况分析

通过上一小节的内容,我们对变量、函数声明提升已经有了一个最基本的理解。那么接下来,我们就来分析一些略复杂的情况。

函数同名

观察下面这段代码:

func1();
function func1(){
console.log('This is func1');
} func1();
function func1(){
console.log('This is last func1');
}

输出结果为:

This is last func1
This is last func1

原因分析:由于预解析机制,func1的声明会被提升,提升之后的代码为:

function func1(){
console.log('This is func1');
} function func1(){
console.log('This is last func1');
} func1();
func1();

同名的函数,后面的会覆盖前面的,所以两次输出结果都是This is last func1

变量和函数同名

alert(foo);
function foo(){}
var foo = 2;

当出现变量声明和函数同名的时候,只会对函数声明进行提升,变量会被忽略。所以上面的代码的输出结果为

function foo(){}

我们还是来吧预解析之后的代码展现出来:

function foo(){};
alert(foo);
foo = 2;

再来看一种

var num = 1;
function num () {
alert( num );
}
num();

代码执行结果为:

Uncaught TypeError: num is not a function

直接上预解析后的代码:

function num(){
alert(num);
} num = 1;
num();

预解析是分作用域的

声明提升并不是将所有的声明都提升到window对象下面,提升原则是提升到变量运行的环境(作用域)中去。

function showMsg()
{
var msg = 'This is message';
}
alert(msg); // msg未定义

还是直接把预解析之后的代码写出来:

function showMsg()
{
var msg;
msg = 'This is message';
}
alert(msg); // msg未定义

预解析是分段的

分段,其实就分script标签的

<script>
func(); // 输出 AA2;
function func(){
console.log('AA1');
} function func(){
console.log('AA2');
}
</script> <script>
function func(){
console.log('AA3');
}
</script>

在上面代码中,第一个script标签中的两个func进行了提升,第二个func覆盖了第一个func,但是第二个script标签中的func并没有覆盖上面的第二个func。所以说预解析是分段的。

tip:但是要注意,分段只是单纯的针对函数,变量并不会分段预解析。

函数表达式并不会被提升

func();
var func = function(){
alert("我被提升了");
};

这里会直接报错,func is not a function,原因就是函数表达式,并不会被提升。只是简单地当做变量声明进行了处理,如下:

var func;
func();
func = function(){
alert("我被提升了");
}

条件式函数声明

console.log(typeof func);
if(true){
function(){
return 1;
}
}
console.log(typeof func);

上面这段代码,就是所谓的条件式函数声明,这段代码在Gecko引擎中打印"undefined""function";而在其他浏览器中则打印"function""function"

原因在于Gecko加入了ECMAScript以外的一个feature:条件式函数声明。

Conditionally created functions Functions can be conditionally declared, that is, a function declaration can be nested within an if statement.

Note: Although this kind of function looks like a function declaration, it is actually an expression (or statement), since it is nested within another statement. See differences between function declarations and function expressions.

Note中的文字说明,条件式函数声明的处理和函数表达式的处理方式一样,所以条件式函数声明没有声明提升的特性。

作用域链

什么是作用域链

只有函数可以制造作用域结构, 那么只要是代码,就至少有一个作用域, 即全局作用域。

凡是代码中有函数,那么这个函数就构成另一个作用域。如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域。

将这样的所有的作用域列出来,可以有一个结构: 函数内指向函数外的链式结构。就称作作用域链。

例如:

function f1() {
function f2() {
}
} var num = 456;
function f3() {
function f4() {
}
}

绘制作用域链的步骤:

  1. 看整个全局是一条链, 即顶级链, 记为 0 级链

  2. 看全局作用域中, 有什么成员声明, 就以方格的形式绘制到 0 级练上

  3. 再找函数, 只有函数可以限制作用域, 因此从函数中引入新链, 标记为 1 级链

  4. 然后在每一个 1 级链中再次往复刚才的行为

变量的访问规则

  • 首先看变量在第几条链上, 在该链上看是否有变量的定义与赋值, 如果有直接使用

  • 如果没有到上一级链上找( n - 1 级链 ), 如果有直接用, 停止继续查找.

  • 如果还没有再次往上刚找... 直到全局链( 0 级 ), 还没有就是 is not defined

  • 注意,同级的链不可混合查找

练习:绘制作用域链

function f1() {
var num = 123;
function f2() {
console.log( num );
}
f2();
} var num = 456;
f1();

 

如何分析代码

  1. 在分析代码的时候切记从代码的运行进度上来分析, 如果代码给变量赋值了, 一定要标记到图中
  2. 如果代码比较复杂, 可以在图中描述代码的内容, 有事甚至需要将原型图与作用域图合并分析

练习

var num = 123;
function f1() {
console.log( num );
} function f2() {
var num = 456;
f1();
}
f2();

 

补充

声明变量使用`var`, 如果不使用`var`声明的变量就是全局变量( 禁用 )

因为在任何代码结构中都可以使用该语法. 那么再代码维护的时候会有问题. 所以除非特殊原因不要这么用.

下面的代码的错误

function foo () {
var i1 = 1 // 局部
i2 = 2, // 全局
i3 = 3; // 全局
}

此时注意

var arr = [];
for ( var i = 0; i < 10; i++ ) {
arr.push( i );
} for ( var i = 0; i < 10; i++ ) {
console.log( arr[ i ] );
} // 一般都是将变量的声明全部放到开始的位置, 避免出现因为提升而造成的错误
var arr = [],
i = 0; for ( ; i < 10; i++ ) {
arr.push( i );
} for ( i = 0; i < 10; i++ ) {
console.log( arr[ i ] );
}

闭包

闭包的概念

闭包从字面意思理解就是闭合, 包起来.

简单的来说闭包就是,一个具有封闭的对外不公开的, 包裹结构, 或空间.

在JavaScript中函数可以构成闭包. 一般函数是一个代码结构的封闭结构, 即包裹的特性, 同时根据作用域规则, 只允许函数访问外部的数据, 外部无法访问函数内部的数据, 即封闭的对外不公开的特性. 因此说函数可以构成闭包.

闭包要解决什么问题?

  1. 闭包内的数据不允许外界访问
  2. 要解决的问题就是间接访问该数据

函数就可以构成闭包, 要解决的问题就是访问到函数内部的数据

我们观察下面的函数foo,在foo内部有一个变量num,能否在函数外部访问到这个变量num呢?

function foo () {
var num = 123;
return num;
} var res = foo();
console.log( res ); // => 123

分析:

在上面的代码中,确实可以访问到num这个函数内部的变量。但是能不能多次访问呢?

不能,因为每次访问都得重新调用一次foo函数,每次调用都会重新创建一个num = 123,然后返回。

解决思路

函数内的数据不能直接在函数外被访问,是因为作用域的关系,上级作用域不能直接访问下级作用域中的数据。

但是如果反过来,下级作用域可以直接访问上级作用域中的数据。那么如果在函数foo内定义一个函数,那么在这个内部函数中是可以直接访问foo中的num的。

function foo() {
var num = Math.random();
function func() {
return num;
}
return func;
} var f = foo();
// f可以直接访问num,而且多次访问,访问的也是同一个,并不会返回新的num
var res1 = f();
var res2 = f();

如何获得超过一个数据

函数的返回值只能有一个,那按照上面的方法,我们只能对函数内部的一个数据进行操作。怎么操作函数内的多个数据呢?

可以使用对象,代码如下:

function foo () {
var num1 = Math.random();
var num2 = Math.random();
//可以将多个函数包含在一个对象内进行返回,这样就能在函数外部操作当前函数内的多个变量
return {
num1: function () {
return num1;
},
num2: function () {
return num2;
}
}
}

如何完成读取一个数据和修改这个数据

前面讲的都是如何去获取函数内部的数据,接下来我们考虑如何修改函数内部的数据。

同样,也是使用内部的函数进行操作。

function foo() {
var num = Math.random();
//分别定义get和set函数,使用对象进行返回
return {
//get_num负责获取数据
get_num: function() {
return num;
},
//set_num负责设置数据
set_num: function(value) {
num = value;
}
}
}

闭包的基本结构

一般闭包要解决的的问题就是要想办法间接的获得函数内数据的使用权. 那么我们的可以总结出一个基本的使用模型.

  1. 写一个函数, 函数内定义一个新函数, 返回新函数, 用新函数获得函数内的数据
  2. 写一个函数, 函数内定义一个对象, 对象中绑定多个函数( 方法 ), 返回对象, 利用对象的方法访问函数内的数据

JS高级. 05 词法作用域、变量名提升、作用域链、闭包的更多相关文章

  1. javascript变量名提升

    预解析的过程 代码的执行过程 程序在执行过程,会先将代码读取到内存中检查,会将所有的声明在此时进行标记.所谓的标记就是让js解释器直到有这个名字,后面在使用名字的时候,不会出现未定义的错误,这个标记就 ...

  2. 【JavaScript高级进阶】JavaScript变量/函数提升的细节总结

    // 测试1 console.log('----------test1--------------'); console.log(global); // undefined var global = ...

  3. 读JS高级(兼容&&BOM&&私有变量&&面向对象)

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  4. JavaScript高级之词法作用域和作用域链

    主要内容: 分析JavaScript的词法作用域的含义 解析变量的作用域链 变量名提升时什么 一.关于块级作用域         说到JavaScript的变量作用域,与咱们平时使用的类C语言不同. ...

  5. js中不能做变量名的字符

    JavaScript中不能作为变量名的关键字和保留字总结: 1.js中的关键字: break case catch continue default delete do else finally fo ...

  6. JavaScript:声明变量名的语法规则

    一.语法规则 1.变量必须使用字母.下划线(_)或者美元符($)开始. 2.然后可以使用任意多个英文字母.数字.下划线(_)或者美元符($)组成. 3.不能使用JS关键词与保留字. 二.示例 var ...

  7. JS高级——变量提升

    JS执行过程 1.首先是预解析:预解析过程最重要的是提升,在JavaScript代码在预解析阶段,会对以var声明的变量名,和function开头的语句块,进行提升操作 2.执行操作 全局中解析和执行 ...

  8. JS高级——词法作用域

    作用域 1.js中没有块级作用域 2.如果有块级作用域,那么下面代码将会是undefined undefined <script> for (var i = 0; i < 10; i ...

  9. js高级程序设计(四)变量、作用域和内存问题

    基本类型和引用类型的值 ECMAScript 变量可能包含两种不同数据类型的值:基本类型值和引用类型值.基本类型值指的是 Undefined . Null . Boolean . Number 和 S ...

随机推荐

  1. 201521123035《Java程序设计》第二周学习总结

    1.本周学习总结 这周学习了各种类与对象,还有基本类型的打包器,最主要的是字符串对象,老师还特地花了一节课时间讲解代码与习题. 2.书面作业 1.使用Eclipse关联jdk源代码,并查看String ...

  2. 201521123031 《Java程序设计》第一周学习总结

    1. 本周学习总结 a.使用notepad++和eclipse编写程序b.对jav的运行环境jdk.jre有了初步的认识c.学习如何使用码云代码库 2. 书面作业 Q1.为什么java程序可以跨平台运 ...

  3. Oracle数据泵的导入导出

    说明:数据泵技术是Oracle Database 10g 中的新技术,它比原来导入/导出(imp,exp)技术快15-45倍.速度的提高源于使用了并行技术来读写导出转储文件. expdp导出 1.以s ...

  4. java向前引用

    根据看书和看得文章,引出了一个关于"向前引用"的问题: public class InstanceInitTest { static { // { a = 6; System.ou ...

  5. Could not instantiate bean class [org.springframework.web.multipart.MultipartFile]: Specified class

    如果在使用SpringMVC中使用文件上传的MultipartFile对象时,出现了以下的错误: Could not instantiate bean class [org.springframewo ...

  6. weblogic-部署web应用

    1, weblogic 安装介质的获取: oracle 官方weblogic下载 :   http://www.oracle.com/technetwork/middleware/weblogic/d ...

  7. java乱码问题处理

    java乱码问题处理 java乱码出现的问题有很多,这里主要解释tomcat,jsp,html,http(get,post请求乱码处理).常见的问题可能是tomcat,http请求乱码问题,对于jsp ...

  8. myeclipse一些快捷键 错了或者没说到补充下

    Ctrl + 1 快速修复Ctrl + D  删除当前行 Ctrl + Alt + ↓ 复制当前行到下一行(复制增加)Ctrl + Alt + ↑ 复制当前行到上一行(复制增加)Alt + ↓ 当前行 ...

  9. Sql Server——数据的增删改

    所谓数据的增删改就是在创建好数据库和表后向表中添加数据.删除表中的数据.更改表中的一些数据. 新增数据: 语法一: insert into 表名 values (数据内容)        --这里需要 ...

  10. Spring 学习——基于Spring WebSocket 和STOMP实现简单的聊天功能

    本篇主要讲解如何使用Spring websocket 和STOMP搭建一个简单的聊天功能项目,里面使用到的技术,如websocket和STOMP等会简单介绍,不会太深,如果对相关介绍不是很了解的,请自 ...