复习预编译-作用域-闭包-立即执行函数

函数定义

👉点击

【补充】:

  • 注意区分什么是函数声明、什么是函数表示,不要混淆。

  • 命定表达式

    1570686806366

    函数按照上面的方式定义后,只有test才是函数,test1不是一个函数,但是test1是test的name,可以通过test.name得到。

参数、返回值

👉点击



预编译

JavaScript执行的三部曲:

​ 1、语法分析:先通篇扫描一下看有没有语法错误,但是不执行。

​ 2、预编译

​ 3、解释执行


【函数声明整体提升,变量的声明提升,变量的赋值不提升】

我们知道JavaScript是解释一行执行一行的,看下面的代码:

1
2
3
4
test();
function test(){
console.log(666)
}

它是可以成功执行打印出666的。因为函数声明整体提升了,它会把函数的声明提升到逻辑的最前面。

再看下面的代码:

1
console.log(a);

报错,a未定义。

1
2
console.log(a);
var a = 123;

可执行,但是打印结果为undefined。这说明了 a 是已经声明但是尚未赋值。这一现象是变量的声明提升

但是,这两个口诀治标不治本,必须把预编译搞明白。


imply global 暗示全局变量

任何变量,如果变量未经声明就赋值,此变量就为全局对象window所有。

1
2
3
<script>
a = 10; //未经声明就赋值,相当于window.a = 10;
</script>

再来一个栗子:

1
2
3
4
5
6
function test() {
var a = b = 10;
}
test();
console.log(a);
console.log(b);

这个执行结果是报错的。

var a = b= 10是从右往左赋值的,先将10赋值给b,但是b没有声明所以它是一个全局变量,之后再把b赋值给a,a有声明变量,它是函数test里面的一个局部变量。外界是无法访问a的,所以报错。但是如果打印b是可以正确输出10的。


一切声明的全局变量,都归window所有,都是window的属性

1
2
3
<script>
var a = 10; //相当于window.a = 10;
</script>

函数预编译

函数预编译发生在函数执行的前一刻。过程如下:

  • 创建AO对象(Activation Object 执行期上下文,它的作用就是我们理解的作用域)
  • 找形参和变量声明,将变量和形参名作为AO对象的属性名,值为undefined
  • 将实参值和形参统一
  • 找函数声明,将函数名也作为AO对象的属性名挂起来,将该声明的函数体作为值。

举个栗子👇

1570706022488

这段代码执行结果为?

栗子1👇

1
2
3
4
5
6
7
8
9
10
11
12
function test(a, b) {
console.log(a);
c = 0;
var c;
a = 3;
b = 2;
console.log(b);
function b() { }
function d() { }
console.log(b);
}
test(1);

栗子2👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function test(a, b) {
console.log(a);
console.log(b);
var b = 234;
console.log(b);
a = 123;
console.log(a);
function a() { }
var a;
b = 234;
var b = function () { }
console.log(a);
console.log(b);
}

test(1);

全局预编译

和函数的预编译类似,同时它产生的叫做GO,GO就是window

  • 创建GO对象(global object 全局上下文,它的作用就是我们理解的全局作用域)
  • 找形参和变量声明,将形参名、变量名作为GO对象的属性名,值为undefined
  • 找函数声明,将函数名也作为GO对象的属性名挂起来,将该声明的函数体作为值。

举个栗子👇

1570758880702

上面的栗子改动一下👇

1570759070170

来练练👇

1
2
3
4
5
6
7
8
9
10
11
console.log(test);
function test(test) {
console.log(test);
var test = 234;
console.log(test);
function test() { }

}
test(1);
var test = 123;
console.log(test);
1
2
3
4
5
6
7
8
9
console.log(bar());
function bar() {
foo = 10;
function foo() {

}
var foo = 11;
return foo;
}
1
2
3
4
5
6
7
8
9
function bar() {
return foo;
foo = 10;
function foo() {

}
var foo = 11;
}
console.log(bar());


作用域

每个JavaScript函数都是一个对象,对象中有些属性我们可以访问。也有些是不可以访问的,这种属性仅供JavaScript引擎存取,[[scope]]就是其中之一。

[[scope]]指的就是作用域,其中存储了运行期上下文的集合。

[[scope]]中所存储的执行期上下文对象的集合,这种集合呈链式链接,称作是作用域链


执行期上下文:当函数执行时,会创建一个称为执行期上下文的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用同一个函数会导致创建多个执行上下文,当函数执行完毕,它所产生的执行上下文就销毁。爽完了就抛弃,渣男!

1
2
3
4
5
6
7
function test(){
...
}
test() //第一个执行test函数,创建了一个它的执行上下文AO
//执行完毕后,AO就被销毁了
test() //再一次执行test函数,又需要创建一个执行上下文AO
//爽完之后再次抛弃(╬▔皿▔)凸



举个栗子👇

1570772661596

这段代码首先是函数a的声明,a函数被定义的时候就产生了如下环节:

1570772746072

a的[[scope]]里面存的是scope chain即作用域链,这时候作用域链里只有一个全局执行上下文GO,紧接着执行代码var glob = 100更新GO。紧接着执行函数a()。

a函数执行的时候就产生了它的执行期上下文AO,并把AO放到作用域链的最顶端,现前的GO就往后稍稍。

1570773023375

一旦我们再a里面查找一个变量的话,它会从a的作用域链的顶端——AO依次向下查找。

紧接着,由于a的执行产生了一个过程——b函数的定义,b作为函数也有自己的[[scope]],但是b刚出生时它的环境就是a给它的,所以b可以捡现成的[[scope]]。

1570773293755

即b刚刚被定义的时候,它保存的就是a的劳动成果。

重点在于b函数被执行的时候,它也产生了它的执行上下文AO,将其放在作用域链的最顶端,之前a的AO和GO就又往后稍稍了。

1570773491976

这时如果在b里面找一个变量的话,也是从作用域链的最顶端自顶向下查找。

b()执行完成之后,b自己的AO就被销毁了,b.[[[scope]]回到了b一开始被定义的时候;

接着b()执行完说明a()也执行完成了,a自己的AO也被销毁了,a.[[scope]]回到了a一开始定义的时候。



再举个栗子👇

1
2
3
4
5
6
7
8
9
10
function a() {
function b() {
function c() {

}
c();
}
b();
}
a()

执行a(),得到作用域链如下:

1570775695313

在任何一个函数里面想要访问一个变量,找它的作用域链就完事了。想要访问变量一定是看在这个函数执行时产生的作用域链,而且是自顶向下访问。

假如我们要在b函数里面访问c函数里定义的cc变量,那么这时候我们是站在【b executing b.[[scoep]]】这个作用域链上的,而这个作用域链里面根本没有cAO,也就访问不到c函数里面定义的cc变量了。这就是为什么函数外部无法访问函数内部的变量的原因。

ps:上面这段代码如果a函数没有执行就没有【a executing 。。。】及其之后的作用域链。



闭包

一个栗子👇

1
2
3
4
5
6
7
8
9
10
11
12
function a() {
function b() {
var bbb = 234;
document.write(aaa);
}

var aaa = 123;
return b;
}
var glob = 100;
var demo = a();
demo();

1570777469422

a执行完毕后销毁了aAO,讲道理,里面的aaa也应该销毁了,但是a函数把b返回了出来并且赋给了demo,导致demo保留了aAO,从而可以访问到aaa。最终结果是打印出123。

1570777779515

这就是闭包。

但凡函数内部的函数,被保存到了外面,一定生成闭包。


再来个详细一点的图文:

1570780727395

1570784350992


当内部函数被保存到外部时,将会生成闭包。

闭包的作用

  • 实现公有变量

    • 栗子:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
       function add() {
      var count = 0;
      function demo() {
      count++;
      console.log(count);
      }
      return demo
      }
      //这样就把count这个变量公有化了
      var counter = add();

      setInterval(counter, 1000)
  • 可以做缓存(存储结构)

  • 可以实现封装,属性私有化

  • 模块化开发,防止污染全局变量

【闭包的缺点】闭包会导致原有作用域链不释放,占用内存,造成内存泄漏。



立即执行函数

执行完立即销毁

两种格式:

  • (function () { })()
  • (function () { }())


1
2
3
4
5
function test(){
console.log(666);
}

test()//这样是可以成功执行的

而且打印test结果就是test函数的函数体。那么我这样写行不行呢?👇

1
2
3
function test() {
console.log(666);
}()

⚠很遗憾,不行!!!

这里记住:【只有表达式才能被执行符号()执行】

所以上面栗子中function test(){}它是函数声明,不是表达式,不可以被执行;

test()test就算单独杵着它也是一个表达式,不是说非要a+b之类的才叫表达式,单独的123;test也是表达式了。

那么!函数表达式能不能被执行呢?

1
2
3
var test = function () {
console.log(666);
}()

都叫表达式了它就一定能被执行。

【奇技淫巧】

记住表达式能被立即执行则表达式的名字不存在,举个栗子👇

1
2
3
var test = function () {
console.log(666);
}

在控制台操作如下:

1570789411336

这个test的名字还代表着这个函数。

但是,我们在函数表达式后边加一个括号,这个表达式就可以被立即执行了:

1
2
3
var test = function () {
console.log(666);
}();

控制台中打印test为undefined,它就不再代表函数了,function(){console.log(666)}被永久的放弃了,这也符合立即执行函数的特点,用完就销毁。

1570789509076

1
2
3
+ function test() {
console.log(666);
}();

函数声明后面加上括号是不可以被执行的,但是在函数声明前面加上一个加号,就会使得function test(){}有一个转化为number类型的趋势,整个+ function(){}就变成了一个表达式,,从而可以被括号执行。同样的减号也可以办到。但是乘号除号不可以。

以下的运算符都可以办到:

1
2
3
4
5
6
7
8
9
10
11
0 || function test() {
console.log(666);
}();

1 && function test() {
console.log(666);
}();

!function test() {
console.log(666);
}();

被执行了之后,test就不再代表function (){}了,变成了undefined。

以上的核心步骤就是【将函数声明变成表达式,就可以被立即执行了】

那么括号()也是运算符,将函数声明用括号包起来,就能将函数声明变成表达式了!因此可以被立即执行。这就是立即执行函数的原理。

好,来个迷惑又恶心的 栗子👇

1
2
3
function test(a, b, c, d) {
console.log(a + b + c + d);
} (1, 2, 3, 4);

这个居然不报错!因为系统将其理解成了:

1
2
3
4
5
function test(a, b, c, d) {
console.log(a + b + c + d);
}

(1, 2, 3, 4);

虽然不报错,但也没执行test函数。

1570795567350

再来一个👇:

1
2
3
4
5
6
7
8
9
10
var f = (
function f() {
return "1";
},
function g() {
return 2;
}
)();
console.log(f);
console.log(typeof f);

1570807949996

再来一个难的栗子👇:

1
2
3
4
5
var x = 1;
if (function f() { }) {
x += typeof f;
}
console.log(x);

1570810489773


【一颗经典的栗子】👇:

1
2
3
4
5
6
7
8
9
10
11
12
13
function test() {
var arr = [];
for (var i = 0; i < 10; i++) {
arr[i] = function () {
console.log(i);
}
}
return arr;
}

test().forEach(element => {
element()
});

我们想要的结果是输出从0到9,但是结果是输出了十个10。为啥呢?

在for循环的过程中,把函数体function(){console.log(i)}赋给了arr[0]、arr[1]、arr[2]。。。。。arr[9],注意不是把function(){console.log(0)}赋给了arr[0]、把function(){console.log(1)}赋给arr[1]。。。把function(){console.log(9)}赋给arr[9]。这点要搞清楚。所以循环结束,是把十个相同的函数体赋给了数组arr,而在执行arr里的每一个元素即每一个函数体的时候,才会给函数体里的 i 进行赋值,而赋值的时候可以看到 i 的值为10!所以最终打印除了十个10。

{建议手写GO、AO作用域链,可以更理解清晰)

解决方案:

方法一:利用立即执行函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function test() {
var arr = [];
for (var i = 0; i < 10; i++) {
(function (j) {
arr[j] = function () {
console.log(j);
}
})(i)
}
return arr;
}

test().forEach(element => {
element()
});

方案二:另一种立即执行函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function test() {
var arr = [];
var foo = function (ele) {
console.log(ele);
}
for (var i = 0; i < 10; i++) {
arr[i] = foo(i);//参数传入时foo函数就执行了
}
return arr;
}

test().forEach(element => {
element
});

以上栗子的实际应用如下👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 给每个li添加点击事件,点击时打印该li的顺序
function test() {
var liCollection = document.getElementsByTagName('li');

for (var i = 0; i < liCollection.length; i++) {
(function (j) {
liCollection[j].onclick = function () {
console.log(j);
}
})(i)
}
}

test()



with

with 可以改变作用域链

1
2
3
4
5
6
7
8
9
10
11
12
13
var obj = {
name: 'obj-name'
}

var name = "window"

function test() {
var name = 'scope';
with (obj) {
console.log(name);
}
}
test()

with(obj){…}括号里填入对象obj,会把这个对象obj当作with圈定的代码体的作用域链的最顶端,即把obj当作最顶端的AO(执行上下文),所以打印出来的name就是obj里的name。

💡应用场景

之前说过命名空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var org = {
dp1: {
jc: {
name: 'jicheng',
age: 22
},
deng: {
name: "laodeng",
age: 30
}
},
dp2: {

}
}

// 当dp1部门的jc要开发的时候,通过如下方式获取并编程
// org.dp1.jc.age
// org.dp1.jc.name
// org.dp1.jc.xxx....
// 这样太麻烦了,可以先var jc = org.dp1.jc 从而简化代码
// 实际上开发时使用with的,这样最简便
with (org.dp1.jc) {
console.log(name);
console.log(age);
}

⚠使用with会降低效率,所以
ES5严格模式禁止使用它。