您的位置:首页 > Web前端 > JavaScript

JS作用域详解 4000

2017-10-22 17:46 344 查看
参考文章书籍:

JavaScript高级程序设计-第四章

深入了解JavaScript,从作用域链开始(1)

JS作用域面试题总结

索引:

一、涉及概念、知识点
1.执行环境

2.作用域
全局作用域

局部作用域(函数作用域)

3.作用域链
▷作用域链的用途:

▷作用域链搜索方法:

▷作用域链访问不可逆。

★JavaScript没有块级作用域

二、作用域习题测试
1.纯作用域、作用域链类
★★注意:

2.作用域+变量(函数)提升类(笔试面试最爱!)

总结:
1.同时出现变量提升和函数提升时,函数会首先被提升,然后才是变量。

2.函数提升要比变量提升的优先级要高一些,且不会被变量声明覆盖,但是会被变量赋值之后覆盖。

三、延长、扩展作用域
1.延长作用域链

2.扩展作用域
PS:

一、涉及概念、知识点

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指向
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息