###函数简介 函数是javascript中一个主要的组成部分,像闭包、this、全局变量和局部变量都和函数息息相关,想要真正了解javascript是如何工作的,首先就得了解函数。
函数能访问内部声明的局部变量,也可以访问那些通过函数参数传递进来的变量,还能访问当前作用域外声明的全局变量,所有的这些都是基于函数的执行环境,一个提供变量可以被函数访问的环境。
要了解清楚函数被调用时所发生的一切细节会相对有些困难,下面所述部分只是技术说明的一个简化,想要了解详情的可以参考ECMA文档。
###执行环境的创建 当函数被调用时,会创建该函数的执行环境,下面来看看这个执行环境是如何构建的,你需要注意每一步的顺序
- 创建arguments属性,这是一个具有整数键的数组式对象(并非是数组),对象引用传入函数调用的值,同时该对象还包括
length
(函数传递进来的参数的数量)和callee
(指向该函数的引用)属性 - 通过[[scope]]属性和执行环境创建函数的作用域。后面会详细说明
- 变量实例化,分为三步
- 执行环境将argument对象中的值赋给函数签名,如果argument中没有,则赋值成undefined
- 扫描函数体,检测是否有函数声明,如果有,将函数名作为环境变量的一个属性
- 扫描函数体,检测是否有变量声明,如果有,将变量名作为函数变量的一个属性,属性值为undefined
- 创建this属性,这个需要依据函数如何被调用
- 作为正常函数被调用,this指向全局对象
- 作为对象的方法被调用,this指向该对象
- 作为构造函数被调用,this指向那个构造函数生成的新对象
- 使用call或者apply调用,this指向call或者apply的第一个参数
- 使用setTimeout或者setInterval调用,this指向全局对象
####示例:
function foo(a, b ,c) {
function z() {
alert('z!');
}
var d = 3;
}
foo('foo', 'bar');
调用foo('foo', 'bar'):
- step1:创建arguments属性
ExecutionContext: {
arguments: {
0: 'foo',
1: 'bar',
length: 2,
callee: function() //Points to foo function
}
}
- step3a: 变量实例化,arguments
ExecutionContext: {
arguments: {
0: 'foo',
1: 'bar',
length: 2,
callee: function() //Points to foo function
},
a: 'foo',
b: 'bar',
c: undefined
}
- step3b:变量实例化,function
ExecutionContext: {
arguments: {
0: 'foo',
1: 'bar',
length: 2,
callee: function() //Points to foo function
},
a: 'foo',
b: 'bar',
c: undefined,
z: function() //Created z() function
}
-step3c:变量实例化,variables
ExecutionContext: {
arguments: {
0: 'foo',
1: 'bar',
length: 2,
callee: function() //Points to foo function
},
a: 'foo',
b: 'bar',
c: undefined,
z: function(), //Created z() function
d: undefined
}
-step4:this赋值
ExecutionContext: {
arguments: {
0: 'foo',
1: 'bar',
length: 2,
callee: function() //Points to foo function
},
a: 'foo',
b: 'bar',
c: undefined,
z: function(), //Created z() function
d: undefined,
this: window
}
###环境栈 函数的作用域创建完之后,函数开始执行代码,从第一行直到函数结束(最后一行或者return),这期间访问变量都是从上面ExecutionContext这个对象中读取。
每个函数都会有一个与之相关联的ExecutionContext,所有的声明都是在ExecutionContext中执行(在全局执行环境中有一个GlobalExecutionContext,和ExecutionContext类似,只是全局执行环境中没有arguments,所以没有第一步)
随着程序的执行,从一个函数进入另一个函数,那么就会创建一个环境栈。我们来看下述代码
function a() {
function b() {
var c = {
d: function() {
alert(1);
}
};
c.d();
}
b.call();
}
a();
当javascript引擎将要执行aler()函数时,执行环境栈是:
- d() Execution context
- b() Execution context
- a() Execution context
- Global execution context
进入一个函数,就会将它的执行环境压入环境栈中,这个环境栈也被称作作用域链,需要注意的是作用域链和函数调用栈大是不同的。
在函数中查找一个变量,javascript引擎会先从作用域链的最前端开始搜索,如果没有找到,会向下一级的作用域链查找,直到作用域链的最底端,如果还没找到,就返回undefined 。
###总结
- this的值是在函数被调用的时候动态绑定,因此具备多重含义
- arguments不是数组,而是一个带有数字作为属性的常规对象
- 变量的定义实际是在步骤3c中定义,当代码执行到达初始化指令时,会发生初始化
- 可以在函数声明之前调用函数
- 不会执行的函数声明依旧会被创建(js作用域提升)
function foo() {
if (false) {
function bar() {
alert(1);
}
}
bar();
}
在执行代码进行if判断之前,bar() 已经在作用域中被创建,所以能正常运行
- 变量覆盖,基于作用域链
- 闭包:子函数会携带父函数的作用域,当我们访问一个变量时,如果当前执行环境中没有找到,但在下一级的作用域链中能找到
- 全局变量在作用域链的最底端,访问全局变量需要遍历整个作用域链,效率很低。
javascript函数的核心其实就是执行环境和作用域链,合理利用这些进行设计,会让程序变得更简单自然,基于mixin的继承系统也很容易实现。很多javascript库,比如jquery,依靠执行环境加载模块,从而不会污染全局变量,尽可能深入的去了解执行环境和作用域链能发挥javascript的强大能力