参考文章书籍:
JavaScript高级程序设计-第四章
深入了解JavaScript,从作用域链开始(1)
JS作用域面试题总结
索引:
一、涉及概念、知识点
1.执行环境
- 执行环境定义了变量或者函数有权访问的其他数据,执行环境有与之相关联的变量对象。
执行环境会在环境内所有代码执行完毕后,销毁该环境。(全局执行环境会等到应用程序退出或者浏览器窗口关闭才会销毁)
- 全局执行环境
全局执行环境也即window对象,因此所有的全局变量、函数都是window对象的属性和方法。 - 局部执行环境(函数执行环境)
当执行流进入一个函数时,执行环境变为这一特定函数的局部执行环境。待函数执行完后,栈将环境弹出,转而进入下一个执行环境。
- 全局执行环境
正由于不同执行环境间的切换,因此产生了变量和函数的作用域
2.作用域
- 作用域代表变量与函数的可访问范围,可以说作用域控制着变量与函数的可见性和生命周期。
- 在JavaScript中,变量的作用域有全局作用域和局部作用域两种,局部作用域又称为函数作用域。
全局作用域
拥有全局作用域的对象:
1.程序最外层定义的函数或者变量
var a = "tsrot";
function hello(){
alert(a);
}
function sayHello(){
hello();
}
alert(a); //能访问到tsrot
hello(); //能访问到tsrot
sayHello(); //能访问到hello函数,然后也能访问到tsrot
2.所有末定义直接赋值的变量(不推荐)
function hello(){
a = "tsrot";
var b = "hello tsrot";
}
alert(a); //能访问到tsrot
alert(b); //error 不能访问
3.所有window对象的属性和方法
如window.name、window.location、window.top等等。
局部作用域(函数作用域)
局部作用域在函数内创建,在函数内可访问,函数外不可访问。
function hello(){
var a = "tsrot";
alert(a);
}
hello(); //函数内可访问到tsrot
alert(a); //error not defined
3.作用域链
▷作用域链的用途:
保证该执行环境下有权访问的所有变量、函数的有序访问!
▷作用域链搜索方法:
目标标识符的解析是从执行环境的最前端开始,沿着作用域链一级一级向后回溯,直到找到标识符为止。
var color = "blue";
function changeColor(){
if(color=="blue"){
color = "red";
}
else{
color = "blue";
}
}
changeColor();
console.log(color); //red
以上代码解释了标识符color
的搜索过程。在调用函数changeColor()
后,执行到if
判定时需要用到变量color
。于是在当前执行环境下先于函数内部作用域搜索变量color
;未找到后向外层检索,此时访问到了全局变量color="blue"
,if
判定符合条件,执行颜色修改为red
。
▷作用域链访问不可逆。
整个搜索访问的过程中,可以通过作用域链从内部环境访问外部环境,但不可逆转,是线性有向的过程。
var color = "blue";
function changeColor(){
var anotherColor = "red";
function swapColor(){
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
//这里可以访问color、anotherColor和tempColor
}
//这里可以访问color和anotherColor
}
//这里只能访问color
changeColor();
以上代码中,全局环境
、changgeColor()
和swapColor()
为三个不同的执行环境,可以由内层向外层搜索访问,但不可向内层访问。
★JavaScript没有块级作用域
JavaScript与其它语言不同的一点是,没有块级作用域。
块级作用域:由花括号封闭的代码块都有自己的作封闭执行环境(作用域)。JavaScript中只有函数具有类似块级作用域的函数作用域。
其它if、for、while等
具有花括号的语句则没有块级作用域,也即语句执行结束后,内部变量、函数仍可以在外层执行环境中访问到!
for (var i;i<10;i++) { ------for示例
dosomething(i);
}
alert(i); //10
//依旧可以访问i
if(true){ ------if示例
var color = "blue";
}
alert(color); //"blue"
//同样可以访问到color
function add(a,b){ ------function示例
var sum = a+b;
return sum;
}
var result = add(10,20);
alert(sum); //error:sum is not defined
二、作用域习题测试
1.纯作用域、作用域链类
1.
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
console.log(x + y + z);
};
bar()
};
foo();
代码的输出结果为”60″。**函数bar可以直接访问”z”,然后又通过作用域链访问上层
的”x”和”y”。**
2.
var y = 'global';
function test(x){
if(x){
var y ='local';
}
return y;
}
console.log(test(true)); //local
在当前作用域(test()
函数)内,可以找到目标标识符y
,因此不需要向上访问全局变量y=“global”
。
3.
var y = 'global';
function test(x){
(function(){
if(x){
var y = 'local';
}
})();
return y;
}
console.log(test(true)); //global
在当前作用域(test()
函数)内,找不到标识符y
,因此按照向上搜索的规则,沿着作用域链访问全局变量y=“global”
。
★不能向内层访问自执行函数中的y
4.★★★★函数嵌套类
var a=10;
function aaa(){
alert(a);
};
function bbb(){
var a=20;
aaa();
}
bbb(); //10
因为bbb()
访问不了内层作用域的变量a
,因此向上访问全局变量a = 10
.
我们一步步来分析这个过程:
先看个简单的:若果aaa本身就有,肯定拿自己的。
var a = 10;
function aaa() { //step-4
var a=30;
alert(a); //step-5->执行alert,此时最先找到aaa作用域下的a=30
};
function bbb() { //step-2
var a = 20;
aaa(); //step-3
}
//定义了函数没啥用,调用才是真格的所以这里是step-1
bbb(); //30 //step-1
- 假如aaa()内未定义a呢,会取谁?
var a = 10;
function aaa() { //step-4
alert(a); //step-5->执行alert,此时最先找到aaa的父作用域中的a=10
};
function bbb() { //step-2
var a = 20;
aaa(); //step-3
}
//定义了函数没啥用,调用才是真格的所以这里是step-1
bbb(); //10 //step-1
- 要是全局的var a=10都没有呢?
function aaa() { //step-4
alert(a); //step-5->执行alert,此时沿着作用域一层一层找,没有a,所以报错。
};
function bbb() { //step-2
var a = 20;
aaa(); //step-3
}
//定义了函数没啥用,调用才是真格的所以这里是step-1
bbb(); //10 //step-1
★★注意:
函数作用域的嵌套关系是定义时决定的,而不是调用时决定的,也就是说,JavaScript 的作用域是静态作用域,又叫词法作用域,这是因为作用域的嵌套关系可以在语法分析时确定,而不必等到运行时确定。
具体可参看这篇文章
放在我们上面的实例中理解就是:aaa和bbb函数作用域是兄弟作用域,互相不能访问内部变量,这在定义的时候就确定了。虽然调用的时候,aaa是在bbb函数的内部调用,但是作用域链却不会被改变!
★5. a=b=10类特殊情况
function aaa(){
var a=b=10;
}
aaa();
alert(a);//结果为,无法访问到
alert(b);//结果为10;
var a=b=10;
可以解析成 b=10;var a=b;
也就是b为全局变量,a为局部变量,所以外部访问a访问不到,访问b结果为10;
2.作用域+变量(函数)提升类(笔试面试最爱!)
1.
var y = 'global';
function test(x){
console.log(y); //undefined
if(x){
var y = 'local';
}
return y;
}
console.log(test(true)); //local
这里涉及到JavaScript中的变量提升,JavaScript中会自动把变量声明的语句提升到当前作用域的最前方 。
以上代码可以这样来理解
var y = 'global';
function test(x){
var y; //声明提前了
console.log(y);
if(x){
y = 'local'; //赋值仍留着原地
}
return y;
}
console.log(test(true)); //local
当test函数中打印y时,变量y只是被声明了,并没有赋值,所以先打印出了undefined;
当程序继续向下执行,则将local返回出来。
2.
var a = 1;
function b(){
a = 10;
console.log(a); //10
return;
var a = 100;
}
b();
console.log(a); // 1
变量a
先是全局声明,在调用b()
函数时,在内部又声明了局部的a变量(因为var a=100
变量提升),改变其值为10
.在执行完毕后,退出函数环境,局部的a变量销毁,因此访问到的a为全局的1
。
4.
var a = 100;
function testResult(){
var b = 2 * a;
var a = 200;
var c = a / 2;
alert(b);
alert(c);
}
testResult() //NaN 100
同样是基于变量声明提前,原理参考第1题过程。局部变量a在函数中声明提前到第一行,值为undefined
。因此b值为NaN
,在a赋值后,c值为100。
5.
var getName = function(){
console.log(2);
}
function getName (){
console.log(1);
}
getName();
这个例子同时涉及到了变量声明提升和函数声明提升。
上例等同于:
var getName; //变量声明提升
function getName(){ //函数声明提升到顶部
console.log(1);
}
getName = function(){ //变量赋值依然保留在原来的位置
console.log(2);
}
getName(); // 最终输出:2
由于变量和函数声明均提升到顶部,因此getName又被后面函数表达式的赋值操作给覆盖了,所以输出2
而如果将上代码稍加改动,变这样的话:
getName(); //最终输出:1
var getName = function(){
console.log(2);
}
function getName (){
console.log(1);
}
等同于:
var getName; //变量声明提升
function getName(){ //函数声明提升到顶部
console.log(1);
}
getName(); // 最终输出:1
getName = function(){ //变量赋值依然保留在原来的位置
console.log(2);
}
看到这里,有些人可能会觉得,当存在两个相同的声明,是不是由于覆盖的现象?造成了上面的结果?
那么接下来我们研究一下,到底两者共存遵循什么规律。
比如,有下述代码:
var a = funtion () {
console.log(10)
}
var a;
console.log(a); // f a() {console.log(10)}
console.log(a()); // 10 和 undefined
a = 3;
console.log(a) //3
a = 6;
console.log(a()); //a() is not a function;
从结果我们可以看到:首先变量a提升到顶部,然后给它赋值一个函数,此时打印a,a为函数。后面进行了赋值3操作后,a变为3,此时再执行a(),浏览器会提示我们出错了。也就是说此时a又被覆盖为一个变量,而非函数。
a的变化过程如下:
var a; //变量提升a=undefined
a = function (){
console.log(10); //a赋值一个函数
}
a = 3; //a赋值一个变量
a = 6;
总结:
1.同时出现变量提升和函数提升时,函数会首先被提升,然后才是变量。
2.函数提升要比变量提升的优先级要高一些,且不会被变量声明覆盖,但是会被变量赋值之后覆盖。
详细内容可以参看:书籍《你不知道的JavaScript》
三、延长、扩展作用域
1.延长作用域链
实现原理:在作用域链的前端增加一个变量对象,当执行流进入下列语句时,作用域链就得到加长:
-try-catch
语句的catch
块
-with
语句
1.对于with
语句来说,会将指定的对象添加到作用域链中。
2.对于catch
语句来说,会创建一个新的变量对象。
例:
function alterUrl() {
var qs = "?debug=true";
with(location) {
var url = href + qs;
}
return url;
}
with语句接收location
对象,等同于with语句的作用域链扩展添加了location对象作用域部分。
因此,在with语句中可以调用所有的location对象的属性和方法。此时的href默认将获取浏览器的href,无需赋值。
2.扩展作用域
扩展作用域的方法常用有两种:
- call()
方法
- apply()
方法
PS:
call
和apply
方法是函数本身具有的非继承的方法,不仅可以传递参数,还可以扩充函数运行的作用域。
★apply()
方法能劫持另外一个对象的方法,继承另外一个对象的属性.
Function.apply(obj,args)`方法接收两个参数:
obj:这个对象将代替Function类里this对象
args:这个是数组,可以是Array,也可以是arguments对象。总之它将作为参数传给 Function(args–>arguments)
★ call:
和apply的意思一样,只不过是将参数数组改为了参数列表.
Function.call(obj,[param1[,param2[,…[,paramN]]]])
obj:这个对象将代替Function类里this对象.
params:这个是一个参数列表.
示例:
1.call()
:
(1)不传递参数,只改变this
指向,以扩充作用域。
window.color = 'red';
document.color = 'yellow';
var s1 = {color: 'blue' };
function changeColor(){
console.log(this.color);
}
changeColor.call(); //red (默认传递参数为window对象参数)
changeColor.call(window); //red
changeColor.call(document); //yellow
changeColor.call(this); //red
changeColor.call(s1); //blue
★(2)call的参数列表必须一一对应,否则将会出现赋值交叉。
function food(name,price){
this.name = name;
this.price = price;
}
function fruits(name,price,weight){
food.call(this,name,price,weight);
this.weight = weight;
}
var result = new fruits("pingguo",5,2)
alert(result.name+"--"+result.price+"--"+result.weight); //pingguo--5--2
以上代码参数列表与food的变量顺序是保持一致。如果将参数列表顺序打乱(或者food函数参数顺序打乱),就会出现赋值交叉的情况,如:
function food(name,price){
this.name = name;
this.price = price;
}
function fruits(name,price,weight){
food.call(this,price,name,weight);
this.weight = weight;
}
var result = new fruits("pingguo",5,2)
alert(result.name+"--"+result.price+"--"+result.weight); //5--pingguo--2
2.apply()
:
(1)不传递参数,只改变this
指向,扩展作用域
window.number = 'one';
document.number = 'two';
var s1 = {number: 'three' };
function changeColor(){
console.log(this.number);
}
changeColor.apply(); //one (默认传参为window对象下参数)
changeColor.apply(window); //one
changeColor.apply(document); //two
changeColor.apply(this); //one
changeColor.apply(s1); //three
(2)函数间作用域扩展,实现方法调用。
function Person(name,age){ //定义一个人类
this.name = name;
this.age = age;
}
function Student(name,age,grade){ //定义一个学生类
Person.apply(this,arguments); //扩展Student的作用域到Person。
//或者Person.call(this,name,age); 也可实现作用域扩展!
this.grade = grade;
}
var student1 = new Student("xiaowang",21,90);
alert(student1.name+student1.age+student1.grade); //xiaowang2190
详细的扩展作用域的方法解析,可以参看这篇文章:call,apply,bind改变this指向