1. 变量和作用域
JavaScript中变量可以分为全局变量
和局部变量
:
全局变量
就是可以在任意位置访问的变量;局部变量
则是在函数中用var定义的变量,只能在函数中访问这个变量,函数外部访问不了。
对应地,作用域也分为全局作用域
和局部作用域
:
全局变量所对应的作用域就是全局作用域(global scope),局部变量所对应的作用域为局部作用域(local scope)。局部作用域里面定义的东西,在这个作用域的外面是访问不到的。例如:
function a(){
var message = "hello world";
}
console.log(message);
这时浏览器就会报错,提示“message is not defined”,因为message是在a函数里定义的,它的作用域只是在a函数内,因此在全局作用域下访问不到。
这里要注意的是:
- 在函数中如果不使用var定义变量那么js引擎会自动添加成全局变量。
- 全局变量从创建的那一刻起就会一直保存在内存中,除非关闭页面,局部变量当函数运行完以后就会销毁这个变量,假如有多次调用这个函数它下一次调用的时候又会重新创建那个变量,既运行完就销毁,回到最初的状态,简单来说局部变量是一次性的,用完就扔,下次要再重新创建。
- 在函数内定义一个局部变量,函数在解析的时候都会将这个变量“提前声明”。比如下面的例子:
function a(){
var message = "hello world";
function b(){
console.log(message); //undefined
var message = "shanghai";
console.log(message); //shanghai
}
b();
}
a();
这也就相当于在b函数中先声明了一个变量message还没有赋值,因此第一次输出为undefined,然后给message赋值,最后成功打印输出。说明js函数内的变量值不是在编译的时候就确定的,而是等在运行时再去寻找的。
function a(){
var message = "hello world";
function b(){
var message;
console.log(message); //undefined
message = "shanghai";
console.log(message); //shanghai
}
b();
}
a();
2. 关于函数
- 函数可以多层嵌套,即一个函数内可以嵌套多个函数;
- 函数里面的子函数可以访问它上级定义的变量,注意不只是一级,如果上级没有会继续往上级找,直到找到为止,如果找到全局变量到找不到就会报错。例如:
function a(){
var message = "hello world";
function b(){
console.log(message);
}
b();
}
a(); //hello world
- 函数的另外一种调用形式,可以把它叫做自调用,自己调用自己,达到自执行的效果。例如:
var a = 0;
(function(){
console.log(++a);
})() //1
这种方式用()把内容包裹起来,如果没有(),js会把它当作函数声明来处理,而用括号包裹起来就是表达式,后面的()表示执行这个函数。
3. 关于作用域链
根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问。
3.1 执行环境
每个函数运行时都会产生一个执行环境,js为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。 全局执行环境是最外围的执行环境,全局执行环境被认为是window对象,因此所有的全局变量和函数都作为window对象的属性和方法创建的。
js的执行顺序是根据函数的调用来决定的,当一个函数被调用时,该函数环境的变量对象就被压入一个环境栈中。而在函数执行之后,栈将该函数的变量对象弹出,把控制权交给之前的执行环境变量对象。
当某个函数第一次被调用时,就会创建一个执行环境(execution context
)以及相应的作用域链,并把作用域链赋值给一个特殊的内部属性([scope]
)。然后使用this,arguments
(arguments在全局环境中不存在)和其他命名参数的值来初始化函数的活动对象(activation object
)。当前执行环境的变量对象始终在作用域链的第0位。
var scope = "global";
function fn1(){
return scope;
}
function fn2(){
return scope;
}
fn1(); //"global"
fn2(); //"global"
以上面的代码为例,当第一次调用fn1()时的作用域链如下图所示:(因为fn2()还没有被调用,所以没有fn2的执行环境)
可以看到fn1活动对象里并没有scope变量,于是沿着作用域链(scope chain)向后寻找,结果在全局变量对象里找到了scope,所以就返回全局变量对象里的scope值。
再看一段代码:
function outer(){
var scope = "outer";
function inner(){
return scope;
}
return inner;
}
var fn = outer();
fn(); //"outer"
outer()内部返回了一个inner函数,当调用outer时,inner函数的作用域链就已经被初始化了(复制父函数的作用域链,再在前端插入自己的活动对象),具体如下图:
一般来说,当某个环境中的所有代码执行完毕后,该环境被销毁(弹出环境栈),保存在其中的所有变量和函数也随之销毁(全局执行环境变量直到应用程序退出,如网页关闭才会被销毁), 但是像上面那种有内部函数的又有所不同,当外部函数执行结束,执行环境被销毁,但是其关联的活动对象并没有随之销毁,而是一直存在于内存中,因为该活动对象被其内部函数的作用域链所引用。
具体如下图:
outer执行结束,内部函数开始被调用;outer执行环境等待被回收,outer的作用域链对全局变量对象和outer的活动对象引用都断了。
像上面这种内部函数的作用域链仍然保持着对父函数活动对象的引用,就是闭包(closure)
。
注:该部分引用自CSDN博客,原文地址:https://blogcsdn.net/whd526/article/details/70990994[4]
4. 关于闭包
书中对闭包的定义:闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。在内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。
前面说到,局部变量当函数运行完以后就会销毁这个变量,假如有多次调用这个函数它下一次调用的时候又会重新创建那个变量。例如:
function fn(){
var num = 5;
num+=1;
console.log(num);
}
fn(); //6
fn(); //6
两次调用fn(),每次都输出6,说明每次执行都是重新创建变量num并初始化的。当函数执行完毕的时候num就会被销毁。
那么,有没有方法可以让num不被销毁,并一直递增下去呢?闭包这个时候就派上用场了。
function a(){
var num = 5;
function b(){
num+=1;
console.log(num);
}
return b;
}
var c = a();
c(); //6
c(); //7
这里在函数a()的内部定义了一个函数b(),并return了回去,同时在函数a的外部定义了一个变量c并引用了函数a。这时候发现第二次执行c()的时候输出为7,这说明在执行过一次的时候,变量num没有被销毁。JavaScript中有回收机制,函数在被执行完以后这个函数的作用域就会被销毁,如果一个函数被其他变量引用,这个函数的作用域将不会被销毁。
下图中给出了调用a()函数过程中产生的作用域链之间的关系。
当a()执行结束,内部函数开始被调用;a的执行环境等待被回收,a的作用域链对全局变量对象和a的活动对象引用都断了。但是内部函数b()保持着对a()函数活动对象的引用,而b()又保存在全局变量c中,因此这些活动对象都不会被销毁。
在Chrome浏览器中添加断点调试,一步一步往下执行,可以看到call stack(函数调用栈)
与scope(作用域链)
的变化,以及函数执行位置的变化:
将函数a()赋给了全局变量c,而此时还没有调用a(),可以看到此时的作用域链只有一个Global。
通过执行c()语句开始调用a(),接着a()内部开始执行b(),可以看到,当前的作用域链:b()的活动对象(this:window)、a()的活动对象num以及全局活动对象(Global)。由于b()中引用了a()中定义的变量num,a()因此成为闭包。
继续往下执行,此时num增加了1
第二次执行c()过程中发现num的值为6而不是5,这说明第一次num加1之后并没有被销毁,因为它一直在被b()引用。
最后总结可以确定闭包的形成需要两个条件:
- 在函数内部创建新的函数;
- 新的函数在执行时,访问了函数的变量对象;
5. 原型(prototype)和原型链
5.1 原型(prototype)
每创建一个函数,就会为该函数创建一个prototype
属性,这个属性指向函数的原型对象,而原型对象中会自动获得一个constructor(构造函数)
属性,这个属性包含一个指向prototype
属性所在函数的指针。当调用构造函数创建一个新实例后,该实例内部将包含一个指针[[prototype]](内部属性)
,指向构造函数的原型对象
。这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。简单讲就是:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
function Person(){ }
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
console.log(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
下面的图片中展示了上述代码中各个对象间的关系:
原型具有动态性:可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来。比如,给上面的例子Person
类型的原型添加一个方法sayHi()
:
Person.prototype.sayHi = function(){
console.log("hi!");
}
person1.sayHi(); //"hi!"
可以看到,person1
实例是在之前定义的,后面又给Person
类型的原型添加一个方法sayHi()
,这时候person1
同样能够访问到该方法。这就是JavaScript中的原型搜索机制
:当以读取模式访问一个实例属性时,首先会在实例中搜索该属性。如果没有找到该属性,则会继续搜索实例的原型。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。
注意
:虽然可以为原型动态添加属性和方法,但如果重写了整个原型,在之前原型基础上创建的实例对象再调用原型中的方法时就会产生错误,因为重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。
5.2 原型链
原型链就是让原型对象等于另一个类型的实例,该实例也会有一个原型对象,而该实例的原型对象则可以又是另外一个类型的实例,如此层层递进就会形成原型链。例如:
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//继承了 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
在上面的例子中定义了两个类型,分别是SuperType和SubType。两个类型都包含一个属性和一个方法;同时,SubType类型的原型又是SuperType类型的一个实例,这样SubType类型的原型就会拥有SuperType类型中所有的属性和方法。也就是说,SubType类型的原型中包含了property、getSuperValue()和getSubValue()一个属性两个方法。因此通过原型链可以实现继承。
下图中给出了上述代码例子中的实例以及构造函数和原型之间的关系: