JavaScript函数,作用域以及闭包


1. 函数

(1). 函数定义:函数使用function关键字定义,它可以用在函数定义表达式或者函数声明定义。

a. 函数的两种定义方式:

* function functionName() {}
* var functionName = function(){}

b. 两种函数定义不同之处

1). 声明提前问题

函数声明语句   :声明与函数体一起提前
函数定义表达式 :声明提前,但是函数体不会提前
请看下面图示:绿色线上面实在js初始加载的时候,查看了一下当前作用域内的函数变量所持有的值,可以从红色框内看到


函数声明的变量已经持有函数体本身,但是函数定义表达式只是在当前作用内种有这样一个变量,但是本身不持有任何的值。
所以绿色线下面的部分调用函数的时候报了typeerror,不是referrenceerror。
2). 函数名称问题

函数声明语句函数名必须含有
函数定义表达式的函数名称可选的。

c. 函数命名

函数命名通常是动词或以动词为前缀的词组,通常函数的第一个字符为小写。当拥有多个单词时,一种是以下划线分隔,另外一种就是除了第一个单词其他的单词首字母全部大写。内部函数或者私有函数,这种函数名通常以一条下划线为前缀。

d. 函数返回值

return 没有对应的表达式或者没有return语句的时候返回undefined给调用者。

(2)函数调用

a. 调用的四种调用方式

* 作为函数
* 作为方法
* 作为构造函数
* 通过call和apply函数间接调用

b. 函数调用

* 函数形式的调用通常是不用this关键字,非严格模式下this(调用上下文,后面会有文章介绍)的值是全局对象,严格模式下则是undefined
* var strict = (function() {return !this;}()); 判断是否工作在严格模式下
* 不能对this进行赋值。

c. 方法调用

1) 一个方法无非是保存在一个对象的属性里的Javascript函数。
2) 方法调用时调用上下文this表示调用该方法的对象。

var functionCallByObject = {_name: 'function',toString: function(){console.log(this._name);}
}functionCallByObject.toString(); //输出: function

note: 方法和this关键字是面向对象编程范例的核心,任何函数只要作为方法调用实际上都会传入一个隐式的实参-方法调用的母体对象。


3) this知识点
this是一个关键字,不是变量,也不是属性名,JavaScript中不允许给this赋值。
this没有作用域的限制,它的值只和调用的方式有关
方法调用: this指向调用方法的母体
函数调用: 全局对象(非严格模式下)或者undefined(严格模式下)

d. 构造函数调用

函数或者方法调用之前带有new,它就构成函数调用。构造函数的调用与普通的函数(方法)调用存在着不同。

1)实参的不同

构造函数没有形参的情况下,JavaScript构造函数调用的方法是允许省略实参列表和圆括号的。
var o = new Object() => var 0 = new Object;

2) 返回值

有返回值的情况下: 返回值为对象返回这个对象,返回值为原始值,则忽略这个返回值,返回新创建的对象
没有返回值的情况: 返回新创建的对象。
请看图示:

3) this

this指的是当前构造函数内部的心创建的对象。对于this的内容后面会有单独的文章说明。

e. 间接调用

我们可以通过使用call(), apply()来间接调用函数。
javascript中: 任何函数可以作为任何对象的方法来调用,哪怕这个函数不是那个对象的。这里只是先说明一下,后面会有讲解。

(3) 函数的实参和形参

JavaScript中的函数定义并未制定函数形参的类型,函数调用也未对传入的实参值做任何的类型检查,实际上,javascript甚至不会检查传进来的形参的个数。函数对于参数的定义非常慷慨,所以,这也成为了出错的可能。

a. 可选形参

1)当调用函数的时候传入的实参比函数声明是指定的形参个数少,剩下的形参都将设置成undefined。

function TestVariable(name, /*optional*/ age){console.log('name: ' + name + ', age: ' + age);
}TestVariable('rodchen'); //输出: name: rodchen, age: undefined

这个地方为什么会是undefined呢? 因为在function的内部,对于参数在执行的时候会有类似步骤: var name = 'rodchen'; 所以当age没有实参的时候,就只是一个var age,那你说这个值是啥,只能是undefined的啊。
note:为什么name是用var创建的变量,因为对于形参变量,我们是不能delete的,但是没有通过var创建的变量我们是可以删除的。

2) 对于可选参数的处理
对于可选参数,我们应该给一个合理的默认值,就像上面的age, age = age || [];
note: 对于一些项目中核心的方法,我们应该检测传进来的值,一旦不符合定义,就抛出error并处理掉。

3) 可变长的实参列表: 
实参对象arguments,指向实参对象的应用,实参对象是一个类数组对象,可以通过下标来访问传入函数逇实参值。
请看图示:

arguments是实际传到函数内部的参数对象,而不是形参对象,而且大家可以看到原型对象是object,而不是array。

note: callee属性和caller属性
arguments.callee:当前执行的函数
arguments.callee.caller:指代调用当前执行函数的函数
arguments.callee.caller.arguments:指代调用当前执行函数的函数的参数对象

function testVariable(name, /*optional*/ age, /*optional*/ company){testArguments();
}function testArguments() {console.log(arguments.callee);console.log(arguments.callee.caller);console.log(arguments.callee.caller.arguments);
}testVariable('rodchen', 12);

结果请看图示:note(上面的代码在最新的chorme,firefox,ie均可运行), callee用来进行递归函数调用非常有用。

(4). 自定义函数属性

JavaScript的函数不是原始值,而是一种特殊的对象,也就是说,函数可以拥有属性。
图示:

(5). 作为命名函数空间的函数

a. 例子

console.log(sayHello);var sayHello = function() {console.log('Hello var');
};function sayHello(name) {console.log('Hello function');
};sayHello(); //输出Hello var。

root cause: 前面的文章也有提到过,对于方法,function声明语句创建的函数在当前作用域内的声明和函数体均会提前。请看下图


所以当js执行第一行代码的时候,sayHello已经持有function sayHello(){}的内容,然后执行var sayHello = function(){}, 所以最后执行的sayHello其实是后面var的赋值。所以在JavaScript中全局变量经常会引起命名冲突,有时候重写变量也不一定按照我们想象中的顺序来的,从上面的例子中可以看到。

b. 命名空间

其实每一个方法函数的内部都是一个独立的命名空间,所以我们可以创建命名空间如下:

/*js 通过函数(function)创建*/
var NameSpace = window.NameSpace || {};
NameSpace.Hello = function() {this.name = 'world';
};
NameSpace.Hello.prototype.sayHello = function(_name) {return 'Hello ' + (_name || this.name);
};/*Object 通过JSON对象创建Object*/
var NameSpace = window.NameSpace || {};
NameSpace.Hello = {name: 'world', sayHello: function(_name) {return 'Hello ' + (_name || this.name);}
};/*Object和闭包的改进型写法*/
var NameSpace = window.NameSpace || {};
NameSpace.Hello = (function() {var name = 'world';var sayHello = function(_name) {return 'Hello ' + (_name || name);};return {sayHello: sayHello};
}());

c. 绑定事件的时候,如果想给element绑定两个click事件,可以使用命名空间去绑定。

$('#btn').off('click.namespace1');
$('#btn').on('click.namespace1', function() {console.log('This is the click function in namespace1');
})$('#btn').off('click.namespace2');
$('#btn').on('click.namespace2', function() {console.log('This is the click function in namespace2');
})

这样我们点击button的时候会执行两次绑定的方法。

(6). 函数属性、方法和构造函数

a. length属性

函数的length属性是只读属性,代表着形参的个数。

b. prototype属性

constructor属性是当前函数的对象
请看下关于length和prototype的图示:

c. call()方法和apply()方法

我们可以将call和apply看作是某个对象的方法(实际上不属于),通过调用方法的形式类间接调用。

1). call
function sum(a, b){console.log(a + b);
}var a = {'a': 1,'b': 2
}sum.call(a, a.a, a.b);  //3
/*如果只是关心计算结果,不关心是那个对象,那么可以传入参数就可以了*/
sum.call(null, 12, 23); //35 
2) apply

使用方式与call基本相同,只是传入参数方式不同,这里需要传入的参数是一个数组,或者类数组对象
sum.apply(a, [a.a, a.b]);  //3

/*如果只是关心计算结果,不关心是那个对象,那么我么你可以传入参数就可以了*/
sum.apply(null, [12, 23]); //35

d. bind方法

bind方法是ECMAScript中新增的方法,这个方法的主要作用就是将函数绑定值某个对象。分解一下整个bind步骤。

function f(y) {console.log(this.x + y);
}var o = {x : 1};
var g = f.bind(o, 3);
g(2); //4

var g = f.bind(o, 3);  // 此时g是一个函数,但是这个函数拥有什么值呢
* 从定义中知道我们是将f函数绑定在对象o上,然后返回一个新的函数。绑定的意义在我看来就是对象有了f方法,这样的话,新函数内部的this是什么呢? 肯定是对象o了。
* 然后是传入参数3,对于参数3,我认为是在当前新函数的内部存储了3这个参数,等到真正调用的时候在和新函数传进来的参数一起,这个存在新函数内部作用域中,然后f函数就生活在这个作用域中。
* 最后g(2),就是传入新函数的参数,接着就是和原先已经存在的3参数一起传入到f函数。
请看图示:


从上面图中红色框中可以看到this, 以及y的值, 绿色框中可以看到2参数值的传进。

note: 用apply方法实现bind的方法(只是理论上的,用于理解,不推荐实际使用)

function bind(object, y) {var self = object;var argumentArray = [y];return function(y){this = self;argumentArray.push(y);f.apply(this, argumentArray);}
}

e. Function 构造函数

var scope = "global";function constructFunction() {var scope = "local";return new Function("console.log(scope)"); //global
}
constructFunction()();function constructFunction2() {var scope = "local";return function() {console.log(scope); //local}
}
constructFunction2()();

1). Function()构造函数允许JavaScript在运行时动态的创建并编译函数。
2). 每次调用Function()构造函数都会解析函数体,并创建新的函数对象。如果是在一个循环或者多次调用的函数中执行这个构造函数,执行效率会受到影响。相比之下,循环中的嵌套函数和函数定义表达式则不会每次执行时都重新编译。
3). 最后的一点,也是关于Function()构造函数非常重要的一点,就是它所创建的函数并不使用词法作用域,相反,函数体代码的编译总是会在顶层函数(全局作用域)执行。看完这一点,上面的函数constructFunction()();返回“global”应该很容易理解了吧。

2. 作用域

(1). 编译原理与作用域

尽管通常将JavaScript归类为“动态”或“解释执行”语言,但事实上它是一门编译语言,对于JavaScript来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时间内。在我们所要讨论的作用域背后,JavaScript引擎用尽了各种办法(比如JIT,可以延迟编译甚至实施重编译)来保证性能最佳。
简单地说,任何JavaScript代码片段在执行前都要进行编译(通常就在执行前)。因此,JavaScript编译器首先会对 var a = 2;这段程序进行编译,然后做好执行它的准备,并且通常马上就会执行它。

a. 名词解释

引擎:从头到尾负责整个Javascript程序的编译及执行过程
编译器:引擎的好朋友之一,负责语法分析及代码生成
作用域: 引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限

b. 例子

var a = 2;
编译器:将程序分解成词法单元,然后将词法单元解析成一个树结构。

1)对于var a, 编译器会访问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中生命一个新的变量,并命名为a;
2)接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a的变量。如果否,引擎就会使用这个变量;如果不是,引擎会继续查找该变量
如果引擎最终找到了 a 变量,就会将 2 赋值给它。否则引擎就会抛出一个异常!

总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。

note: 编译器名词解释: 
LHS(Left Hand Side): 左侧查询
LHS查询则是试图找到变量的容器本身,从而可以对其赋值

RHS(Right Hand Side): 右侧查询
RHS查询与简单地查找某个变量的值别无二致,RHS并不是真正意义上的“赋值操作的右侧”,更准确地说是“非左侧”,可以将RHS理解成retrieve his source value(取到它的源值)

function foo(a) {
//存在隐式i步骤: a = 2;
var b = a;
return a + b;
}
var c = foo( 2 );
下面是相关对话:

引擎:作用域大哥,我需要对c进行LHS, 你知道吗?
作用域: 我知道,给你。
引擎:我说作用域,我需要为 foo 进行RHS引用。你见过它吗?
作用域:别说,我还真见过,编译器那小子刚刚声明了它。它是一个函数,给你。
引擎:哥们太够意思了!好吧,我来执行一下 foo 。
引擎:作用域,还有个事儿。我需要为 a 进行LHS引用,这个你见过吗?
作用域:这个也见过,编译器最近把它声名为 foo 的一个形式参数了,拿去吧。
引擎:大恩不言谢,你总是这么棒。现在我要把 2 赋值给 a 。
引擎: 作用域,我需要对a进行LHS, 请把这个值给我好吗!
作用域: Ok,我知道了,给你吧.
引擎:作用域,我需要对a进行RHS, 我相信你肯定知道了.
作用: 当然,给你好了。
引擎: ok, 谢谢你了,现在我要给a赋值了.
引擎:我还有一个步骤, 我需要对ab进行RHS.
作用域: 恩,给你。
引擎:执行代码return a + b; 得到function的结构之后执行赋值语句。

c. 作用域嵌套

词法作用域: 当前作用域 -> 嵌套作用域 -> 全局作用域
如下图:

当在当前作用域找不到结果时,引擎就回到外层嵌套的作用域中查找,一直像最外层作用域查找,知道查找到位置,如果在外层查不到的时候,会抛异常(请看下面(5))

d. 异常

1)如果RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。值
得注意的是, ReferenceError 是非常重要的异常类型。
相较之下,当引擎执行LHS查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域
中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下。

从上面的图片中可以看到LHS查询不到变量是,其实是创建了一个名为c的变量,但是对于RHS,查询到全局作用域也没有,所以抛出了异常。

2)然后再看一下严格模式下的输出:
其实可以看到在对c进行LHS的时候抛出了异常。

3)TypeError 与 ReferenceError的区别

ReferenceError 同作用域判别失败相关
ypeError 则代表作用域判别成功了,但是对结果的操作
是非法或不合理的

(2). 词法作用域

a. 词法作用域就是定义在词法阶段的作用域

换句话说,词法作用域是由你在写代码时将
变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情
况下是这样的)。

b. 例子

1是包含着bar所创建的作用域,其中只有一个标识符c
2是包含着foo所创建的作用域,其中有三个标识符: a, bar, b
3是包含着整个全局作用域,其中只有一个标识符: foo.

词法作用域规定的就是在我们书写代码的时候就已经确定的了,例如在函数bar中访问变量b,在1(当前作用域)的作用域中没有查找到,接着向上一级嵌套的作用域中2查找,然后找到了变量b。

note: 无论函数在哪里调用,也无论函数如何被调用,它的词法作用域都只由函数被定义的时候决定的。
并且此法作用域智慧查找到一级标识符,就是当我们访问属性的时候,词法作用域只会访问对象,具体的对象属性访问会由对象访问规则接管。

c. 欺骗词法作用域

1)eval

JavaScript 中的eval(..) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码. 也就是说在执行代码前后,当前的此法作用域发生了变化。


在严格模式的程序中,eval(..) 在运行时有其自己的词法作用域,意味着其
中的声明无法修改所在的作用域。

2)with:
语句用于设置代码在特定对象中的作用域.减少对通过一个对象多次访问。

(3)函数作用域和块作用域

在上面(2)中例子中产生作用域的是基于函数的作用域,那么会有一个问题,只有函数能够创建作用域吗,其他机构不会创建作用域吗?这个问题后面会说明,先看一下函数作用域吧。

a. 函数中作用域

还是看上面(2)的例子: 可能大家觉得这样词法作用域的工作方式很正常嘛,没有什么问题。如果我们在作用域3中访问a,b,c会有问题,失败是可定的,为什么呢?
answer:函数作用域的含义是指,属于这个函数的全部变量可以在整个函数的范围内使用(包含嵌套函数)。

这样的设计方案非常有用当然也有缺点。
优点:充分利用Javascript变量可以根据需要改变值类型的“动态”特性,也就是当我需要某个变量持有其他的值得时候,可以直接改变,并在这个函数作用域内应用。
缺点:就是可能我在嵌套函数中的改变不是整个函数作用域想要看到的问题,也是变量污染的问题,这个就是闭包存在的意义(后面会提到)。

b. 隐藏内部实现

1)对函数的传统认识就是先声明一个函数,然后再向里面添加代码。但是反过来想也可以带来一些启示:从代码中挑选片段,然后用函数声明对它进行包装,实际上就是把这些代码隐藏起来了。

Question: 为什么隐藏变量和函数是一个有用的技术?
最小特权原则,也叫最小授权或最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的API设计。
这个原则可以延伸到如何选择作用域来包含变量和函数。如果所有变量和函数都在全局作用域中,当然可以在所有的内部嵌套作用域中访问到它们。但这样会破坏前面提到的最小特权原则,因为可能会暴漏过多的变量或函数,而这些变量或函数本应该是私有的,正确的代码应该是可以阻止对这些变量或函数进行访问的
示例:

function doSomething(a) {b = a + doSomethingElse( a * 2);console.log(b * 3);
}function doSomethingElse(a) {return a - 1;
}var b;
doSomething(2);//下面是修改过后的。function doSomething(a) {var b;function doSomethingElse(a) {return a - 1;}b = a + doSomethingElse( a * 2);console.log(b * 3);
}doSomething(2);

c. 变量名冲突

1) 全局变量名冲突

全局作用域中当引入js库时,很容易发生冲突,其实通过我们使用这些库的过程中,其实可以发现一个现象。这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴漏在顶级的词法作用域中。例如jquery暴露$对象 , requirejs暴露require对象

2) 模块管理

另外一种避免冲突的办法和现代的模块机制很接近,就是从众多模块管理器中挑选一个来使用。使用这些工具,任何库都无需将标识符加入到全局作用域中,而是通过依赖管理器的机制将库的标识符显式地导入到另外一个特定的作用域中

d. IIFE 立即执行函数表达式Immediately Invoked Function Expression

我们再看一下上面的例子:

function doSomething(a) {var b;function doSomethingElse(a) {return a - 1;}b = a + doSomethingElse( a * 2);console.log(b * 3);
}doSomething(2);

其实这个地方我们还是污染了全局作用域,doSomething还是存在于全局作用域中,可能我们需要的仅仅是执行代码就ok了,不想之后有其他的操作(当然这在大部分时间里没有意义,我执行代码是需要有输出,就像第三方js库一样,对于jquery不管你内部干什么,我是需要你给我提供方便的,你是需要给我一个入口的,也就是$)。这个地方我们可以使用IIFE,

(function doSomething(a) {var b;function doSomethingElse(a) {return a - 1;}b = a + doSomethingElse( a * 2);console.log(b * 3);
})(2);

包装函数的声明以 (function... 而不仅是以 function... 开始。尽管看上去这并不是一个很显眼的细节,但实际上却是非常重要的区别。函数会被当作函数表达式而不是一个标准的函数声明来处理。这个应用到第三方库的时候例如jquery(function(window){}(window))
这样jqeruy自己处理就好了,搞完了给我一个api入口$.

1). 匿名和具名

顾名思义: 有名字于没名字的区别。
匿名函数的优点:匿名函数表达式书写起来简单快捷
匿名函数的缺点:

匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
如果没有函数名,当函数需要引用自身时只能使用已经过期的 arguments.callee 引用,比如在递归中
匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。
2)(function(){})()
与上面的方式执行效果没有区别

(4). 块作用域

a. with

b. try/catch

c. let

ES6,引入了新的 let 关键字,提供了除 var 以外的另一种变量声明方式。let 关键字可以将变量绑定到所在的任意作用域中(通常是{ .. }内部)。换句话说, let 为其声明的
变量隐式地了所在的块作用域。

var foo = true;if (foo) {let nameForBlock =  'rod';console.log(nameForBlock);
}console.log(nameForBlock); //Uncaught ReferenceError: nameForBlock is not defined

note:  let 进行的声明不会在块作用域中进行提升

d. const

ES6还引入了 const ,同样可以用来创建块作用域变量,但其值是固定的(常量)。之后任何试图修改值的操作都会引起错误。

var foo = true;if (foo) {const nameForBlock =  'rod';console.log(nameForBlock);nameForBlock = 'rod1'; //Uncaught TypeError: Assignment to constant variable.
}console.log(nameForBlock); //Uncaught ReferenceError: nameForBlock is not defined

3. 闭包

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

(1) 闭包

先看一个简单的示例:

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

这个地方按照词法作用域的规则,baz是一个函数,然后这个函数的函数体也就是foo函数内的bar,然后我们在全局作用域里运行,按照道理此时全局作用域里是没有a变量的,这个地方是不会会报错呢,我们看一下结果。

哎,不对吧,为什么会是2呢,我在foo的外部作用域里怎么会访问到a = 2 的作用域呢?
在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很自然地会考虑对其进行回收。而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是 bar() 本身在使用。

Answer: 拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。
bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。因此,在几微秒之后变量 baz 被实际调用(调用内部函数 bar ),不出意料它可以访问定义时的词法作用域,因此它也可以如预期般访问变量 a 。

再看一下对象的闭包:

var object = {_propertyName: 'rod',_propertyCompany: 'Augmenum',toString: function() {console.log("My name is: " + this._propertyName + ", company is " + this._propertyCompany);}
}object.toString(); //My name is: rod, company is Augmenum*-object._propertyName = "It's not rod";
object._propertyCompany = 'no';object.toString(); //My name is: It's not rod, company is no

这个问题就是object对象指向暴露的是toString方法,别人知道这个就可以了,属性值我都标注内部属性,谁这么不懂规矩,随便改我的属性值,好尴尬!

现在我们改一下上面的实现

var object =(function() {var self = this;self._propertyName = 'rod';self. _propertyCompany = 'Augmenum';return {toString: function() {console.log("My name is: " + self._propertyName + ", company is " + self._propertyCompany);}}
})();object.toString(); //My name is: rod, company is Augmenumobject._propertyName = "It's not rod";
object._propertyCompany = 'no';object.toString(); //My name is: rod, company is Augmenum

这样我们就实现了我们想要的结果了。

(2) 循环与闭包

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

正常情况下,我们对这段代码行为的预期是分别输出数字1~5,每秒一次,每次一个。但实际上,这段代码在运行时会以每秒一次的频率输出五次6。这是为什么?
首先解释 6 是从哪里来的。这个循环的终止条件是 i 不再 <=5 。条件首次成立时 i 的值是6。因此,输出显示的是循环结束时 i 的最终值。

主要的原因是setTimeout函数共用同一个作用域,为了解决这个问题那就是我们要setTimeout每次的执行都是在自己独有的作用域下执行就OK了。

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

使用块作用域let来实现

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

当然这种用法还不够好,基于 for 循环头部的 let 声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使
用上一个迭代结束时的值来初始化这个变量。所以我们将上面的代码修改一下:

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

(3) 模块

模块模式需要具备两个必要条件。
*. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
*. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

a. define (require上是基于这种方式定义模块的)

MyModules.define('information', [], function(){var _propertyName = 'rod';var _propertyCompany = 'Augmenum';function toString() {console.log("My name is: " + _propertyName + ", company is " + _propertyCompany);}return {toString: toString}
})

b. exxport (node)

/*information 文件*/
var _propertyName = 'rod';
var _propertyCompany = 'Augmenum';function toString() {return "My name is: " + _propertyName + ", company is " + _propertyCompany;
}export toString;
/*fullInformation 文件*/
import toString from "information"function fullInformation() {console.log(toString() + ", that's all information");
}export fullInformation;

JavaScript函数,作用域以及闭包相关推荐

  1. javascript函数作用域与闭包

    8.8. 函数作用域与闭包        如第四章所述,JavaScript函数的函数体在局部作用域中执行,局部作用域不同于全局作用域.本章将解释这些内容和相关的作用域问题,包括闭包.[*] [*] ...

  2. JavaScript从作用域到闭包

    目录 作用域 全局作用域和局部作用域 块作用域与函数作用域 作用域中的声明提前 作用域链 函数声明与赋值 声明式函数.赋值式函数与匿名函数 代码块 自执行函数 闭包  作用域(scope) 全局作用域 ...

  3. 浅谈JavaScript 函数作用域当中的“提升”现象

    在JavaScript当中,定义变量通过var操作符+变量名.但是不加 var 操作符,直接赋值也是可以的. 例如 : message = "hello JavaScript ! " ...

  4. 函数作用域,闭包,数据类型的题目

    第一题:函数作用域和闭包 var a = 9 function fn(){a = 0return function (b){return b+a++} } var f = fn() console.l ...

  5. JavaScript函数作用域

    JavaScript函数作用域 JavaScript作用域 作用域: 作用域指一个变量的作用范围 JS中一共两种作用域: (1)全局作用域 (2)函数作用域(局部作用域) 1.全局作用域 直接写在sc ...

  6. 你不知道的 JavaScript 笔记——作用域和闭包

    第一章:作用域是什么 程序中变量存储在哪里,需要是怎么找到它,这就需要设计一套存储以及能方便的找到它的规则,这个规则就是作用域 编译原理 JavaScript 是一门编译语言,它与传统编译语言不同,但 ...

  7. JavaScript的作用域与闭包

    JavaScript的作用域以函数为界,不同的函数拥有相对独立的作用域.函数内部可以声明和访问全局变量,也可以声明局部变量(使用var关键字,函数的参数也是局部变量),但函数外部无法访问内部的局部变量 ...

  8. 你不知道的JavaScript上卷-作用域和闭包

    1. LHS引用与RHS引用的区别: RHS:取到源值-得到某某的值 LHS:谁是赋值操作的源头-给谁赋值 function foo(a) {var b = a;return a + b; } var ...

  9. 重温JavaScript(lesson4):作用域和闭包(2)

    在lesson3中我们重温了JS作用域有关的内容,理解了JS作用域再来看闭包就非常easy了. 1.闭包的概念 先来补充一个知识点(PS:如果你觉得不好理解,就看之后代码吧~),词法作用域:" ...

  10. javaScript的作用域、闭包

    前言 JavaScript 中的闭包是相当重要的概念,并且与作用域相关知识的指向密切相关. 那么, JavaScript 中的作用域是什么? 闭包会在哪些场景中使用? 作用域 JavaScript 的 ...

最新文章

  1. Android中的多线程(字节跳动)
  2. 顶级数据库管理系统的性能比较研究(论文翻译)
  3. python零基础怎么学-Python怎么学?零基础如何顺利入门Python?
  4. 课程范例 20150916html1 练习
  5. securecrt配置linux账号密码,配置SecureCRT密钥连接Linux
  6. RTP/RTCP/RTSP协议初探
  7. chrome vue插件_VS Code 前端常用插件推荐
  8. 使用Webtask.io创建一个简单的REST API端点
  9. 文科生必备计算机知识点,文科生计算机知识点调查报告.docx
  10. 团队作业2之选题与评审
  11. 初级算法——删除排序数组中的重复项
  12. As-If-Serial 理解
  13. 从深海带回更多的东西 (汪品先)
  14. web 系统发展历程
  15. EditPlus下载安装和汉化
  16. 标准盒模型和怪异盒模型的区别
  17. 99计数器c语言,数字显示00-99计数器
  18. 青蛙Pro绑定商户号(windows)
  19. 【Error2013:2013 lost connection to MYSQL server在处理大量数据运行中报错解决方法】
  20. 数学在计算机方面的应用论文参考文献,应用数学毕业论文参考文献精选

热门文章

  1. python实现百万富翁遇到一个陌生人契约
  2. CTF题库实验吧女神 (猫流大大发现一个女神,你能告诉我女神的名字么(名字即是flag))
  3. 新闻推荐:谷歌新闻、搜狐新闻、今日头条调研分析
  4. 树莓派控制继电器开关工作
  5. 在html页面中引入腾讯地图,html引入腾讯地图
  6. 计算机网络学习笔记(一)——什么是Internet
  7. Python高效实现滑块验证码自动操纵
  8. ASLR(地址空间随机化)
  9. BITMAP CONVERSION FROM ROWIDS
  10. 机器学习极简入门课程