JS是弱类型语言,所谓弱类型只是表明该语言在表达式运算中不强制校验运算元的数据类型,而并不表明该语言是否具有类型系统。一般而言,JS的变量可从作用域角度分为全局变量和局部变量。
任何程序设计语言都有作用域(scope)的概念,简单来说,作用域就是变量与函数的可访问范围,也就是说,作用域控制着变量与函数的可见性和生命周期。换句话说,函数的变量有其作用域,也就是引用变量时在哪个范围内查找该变量。那么这个范围又在哪里呢?这个范围其实就是活动对象(AO)。在函数调用的瞬间,会产生一个活动对象,活动对象的属性上存储着该函数所能引用到的变量。
作用域
在JS中函数嵌套是非常普遍的,在函数嵌套中,对变量是如何寻找的呢?
var a = 1;
function fn(){
var b = 2;
function func(){
var c = 3;
console.log(a,b,c);// 1 2 3
}
func();
}
JS中对变量的寻找是由内向外,首先在函数内部寻找,若寻找不到,则逐级向外寻找直到全局(window)区域。
声明变量var
的作用是什么呢?
console.log(window.a, window.b);// undefined undefined
function fn(){
a = 1;// 未添加var表示仅仅是一个赋值操作,从fn域内向外寻找变量a直到全局(window)。
var b = 2;
}
fn();
console.log(window.a, window.b);// 1 undefined
var
是在函数运行的上下文(EC)中,声明一个变量。为无var
则仅仅表示一个赋值操作,但不要狭隘地理解为“声明了一个全局变量”。
function fn(){
var a;
function func(){
a = 1;
b = 2;
}
func();
}
fn();
console.log(b);// 2
// 以window.prop引用全局变量,寻找不到时则作为window的某个属性不存在返回undefined。
console.log(window.a);// undefined
// 直接使用变量引用变量,若寻找不到则直接报错。
console.log(a);// Uncaught ReferenceError: a is not defined
一个极容易出错,又非常基础的JS面试题。
var v = 'global';
function fn(){
console.log(v);// global
console.log(l);// undefined
var l = 'local';
}
fn();
JS代码自上而下执行,但是JS代码整体运行分为两个阶段:词法分析期和运行期。在JS代码自上而下执行前,会有一个“词法分析过程”。
var v = 'global';
function fn(){
console.log(v);// global
console.log(l);// Uncaught ReferenceError: l is not defined
l = 'local';
}
fn();
词法分析
JS代码运行前,有一个类似编译的过程即词法分析,词法分析主要有3个步骤:分析函数参数、分析变量声明、分析函数声明。
function fn(func){
console.log(func);// func(){console.log(func);}
function func(){
console.log(func);// func(){console.log(func);}
}
func();
}
fn(1);
词法分析的具体步骤:在函数运行的瞬间,会生成一个活动对象(AO, Active Object)。
- 分析函数参数
- 将函数的形参添加为AO属性,属性的默认值为
undefined
。 - 接收函数的实参,并覆盖原属性值。
- 分析变量声明/分析局部变量
- 若AO中不存在与声明的变量所对应的属性,则添加AO属性为
undefined
。 - 若AO中已存在与声明的变量所对应的属性,则不做任何修改。
- 分析函数声明
- 若AO中存在与函数名所对应的属性,则覆盖原属性为一个函数表达式。
function fn(func){
console.log(func);// func(){console.log(func);}
var func = 100;
function func(){
console.log(func);// Uncaught TypeError: func is not a function
}
// func();
console.log(func);// 100
}
fn(1);
具体分析如下
function fn(func){
console.log(func);
}
/*分析函数参数*/
// 将函数的形参添加为AO属性,属性的默认值为undefined。
fn();// undefined
// 接收函数的实参,并覆盖原属性值。
fn(1);// 1
function fn(func){
console.log(func);// 1
/*分析变量声明/分析局部变量*/
// 若AO中已存在与声明的变量所对应的属性,则不做任何修改。
var func = 100;// 执行过程:对变量进行赋值
console.log(func);// 100
}
fn(1);
------------------------------------------------------------------------------------------------------------
AO = {}
分析函数参数
AO = {func:undefined}
AO = {func:1}
分析局部变量
AO = {func:1}
运行阶段赋值
AO = {func:100}
function fn(func){
console.log(func);// func(){}
/*分析变量声明/分析局部变量*/
// 若AO中已存在与声明的变量所对应的属性,则不做任何修改。
var func = 100;// 执行过程:对变量进行赋值
console.log(func);// 100
/*分析函数声明*/
// 若AO中存在与函数名所对应的属性,则覆盖原属性为一个函数表达式。
function func(){
}
console.log(func); // 100
}
fn(1);
------------------------------------------------------------------------------------------------------------
AO = {}
分析函数参数
AO = {func:undefined}
AO = {func:1}
分析局部变量
AO = {func:1}
运行阶段赋值
AO = {func:100}
分析函数声明
AO = {func:function(){}}
函数声明与函数表达式
JS被称为“披着C外衣的List语言”,List语言是一种强大的函数式语言。函数式语言中函数是一等公民,函数可作为赋值给变量、函数也可以作为参数来传递。
// 函数声明,全局内得到了一个fn变量且值为function。
function fn(){
}
// 函数表达式,仅仅是一个变量赋值的过程,值是谁呢?
// 是右侧函数表达式返回的结果
var fn = function(){
}
在词法分析时,函数声明和函数表达式有着本质的区别。函数声明在词法分析阶段就后发挥作用。而函数表达式只有到运行阶段才会发挥作用。
// jQuery源码分析:
// 声明一个内层表达式返回值是一个函数,然后立即调用函数。
// 内层函数没有起名字,称为匿名函数。
// 这种手法中匿名函数立即执行,好处是不污染全局变量。
// 称之为立即执行匿名表达式
(function(window,undefined){
})(window);
// 为什么传入window而不传入undefined呢?
// 传入window是为了速度
// jQuery为了加快内部查找局部变量的速度,而直接将window以参数形式传入。
// 这样的话,window就在jQuery内部的AO上,那查找速度就会快。
// 不传入undefined时候为了安全
// 因为在IE和FF的低版本中,undefined竟然可以重新赋值,如undefined=3。
// 声明undefined局部变量,名字是undefined。同时又不传参,值自然是undefined。
// 防止外界对undefined的污染。
function(){
function(){
function(){
function(){
// document将会沿着作用域链由内向外层层上找,直到最外层的全局对象window。
document.getElementById('sidebar');
}
}
}
}
函数声明与函数表达式有什么区别呢?
ECMAScript规范规定:函数声明必须带有标识符(identifier),也就是函数名称。而函数表达式则可省略。实际上,解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。解析器会率先读取函数声明,并使其执行任何代码之前可用(可访问)。至于函数表达式,则必须等到解析器执行到它所在代码行时,才会真正被解析执行。
console.log(sum(1,2));// 3
function sum(i, j){
return i + j;
}
代码执行前,JS解析器通过名为函数声明提升(function declaration hoisting)的过程,读取并将函数声明添加到执行环境中。对代码求值时,JS引擎也能把函数声明提升到顶部。
console.log(sum(1,2)); // Uncaught TypeError: sum is not a function
var sum = function(i,j){
return i + j;
}
将函数声明修改为等价的函数表达式,代码执行时出现错误。原因在于函数位于一个初始化语句中,而不是一个函数声明。换句话说,在执行到函数所在的语句之前,变量sum
中不会保存有对函数的引用。而且,由于错误的出现,代码也就不会向下执行了。
如何判断是函数声明还是函数表达式呢?
ECMAScript是通过上下文来区分的,如果函数作为赋值表达式的一部分的话,那它就是一个函数表达式。如果函数被包含在一个函数体内,或位于程序最顶端的话,那它就是一个函数声明。
function fn(func){
console.log(func);// 1
var func = 100;
console.log(func);// 100
// 将函数表达式赋值给变量
func = function(){
console.log(func);// f (){console.log(func);}
}
func();
console.log(func);// f (){console.log(func);}
}
fn(1);
作用域链
作用域链就是函数由内向外所产生的活动对象(AO)的链