JavaScript作用域学习笔记

时间:2021-10-06 14:06:41

额……写博客什么的好像很难的样子……脑子一下子空了~~~

算了不矫情了,就当做捋一捋~~没准写着写着就High了。

js作用域可以简单理解两句话。由上到下,由内到外

(一)预解析

我们来看一段代码

alert(a);
var a = 2;

 运行结果是undefined。在使用var声明变量但未对其加以初始化时,变量声明会提升至顶部,这个变量的值就是undefined。即a = undefined。

function a() {
	console.log(1);
}
alert(a);

运行结果:JavaScript作用域学习笔记

在预解析阶段JS解析器会找关键字var,function,变量等。

这时存入的是一段代码块,即 a = function a() {
    console.log(1);
}

这个过程叫做函数声明提升。解析器会率先读取函数声明并添加到执行环境中。

补充一个函数表达式

 

1 console.log(a());
2 var a = function () {
3     return 3;
4 };

运行结果: a is not a function

函数表达式和函数声明在解析器向执行环境中加载数据时并不一样处理。函数表达式必须等到解析器执行到它所在的代码行才会真的被解释执行。并不会提升。

在执行到函数所在的语句之前,变量a中不会保存有对函数的引用。

在全局作用域中遵循一个简单规则由上而下,在看一段代码,猜一猜结果吧

alert(a);					
function a (){ alert(4); }
var a = 1;
alert(a);							
function a (){ alert(3); }
alert(a);	

 结论:JS 的预解析遇到重名的只留一个, 变量和函数重名了,就只留下函数

(二)执行环境及作用域的概念

执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之相关的变量对象。环境中定义的所有变量和函数都保存在变量对象中。对象变量只在函数执行过程中存在。但是全局变量对象始终存在。

某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。

每个函数都有自己的执行环境。

JavaScript作用域学习笔记

fn1执行完毕后函数的执行环境被销毁,在函数内声明的变量a也随之销毁。

JavaScript作用域学习笔记

原谅我一直停留在小学时期的画图水平吧~~

当试行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

到这里插一句话,像我上图的例子里这样执行的函数都是同步滴~~同步任务在执行栈中~~推荐阅读阮一峰大神的JavaScript 运行机制详解:再谈Event Loop

www.ruanyifeng.com/blog/2014/10/event-loop.html

 (三)作用域链

  1. 创建函数时。在创建fn1时,会创建一个预先包含全局变量对象的作用域链,这个作用域链保存在内部[[scope]]属性中
  2. 执行函数时。首先会创建一个执行环境,再通过复制函数的[[scope]]属性中的对象构建执行环境的作用域链。然后创建函数的变量对象,并推入执行环境作用域链的前端。
  3. 作用域链可以保证对执行环境有权访问的所有变量和函数的有序访问。作用域的前端始终都是当前执行的代码所在环境的变量对象。作用域链中的下一个变量对象来自包含(外部)环境。作用域链的最后一个对象始终是全局执行环境的变量对象。即作用域链遵循由内到外的简单规则

现在把我那张小学生水平的图稍作改动

 

JavaScript作用域学习笔记

fn1的作用域链,开始是fn1的变量对象,然后是来自包含环境的变量对象,在这里是全局变量对象。

fn2执行时,执行a(),遇到arg1会顺着作用域链寻找arg1,会先从函数a变量对象中寻找arg1,没有就去作用域链上的下一个变量对象fn2的变量对象上寻找,没有就继续作用域链上下一个变量对象全局变量对象中寻找,这里找到了arg1 = true.

总结一下,内部环境可以通过作用域链访问所有外部环境,但外部环境不能访问内部环境中的任何变量或函数。

(四)没有块级作用域

在JavaScript中没有块级作用域。

嘿嘿==,关于这个问题只要记住这句话就够了~~额……当然不会就说这么一句话,不来点干货我都不好意思==

JavaScript作用域学习笔记

 

在其他类c语言中,由花括号{}封闭的代码块都有自己的作用域,但是js没有块级作用域!!!

如果存在块级作用域应该报错,因为a是一个局部变量,在全局是找不到a的。

但是运行结果a = 9,所以不存在块级作用域。

(五)this对象

在函数内部有个特殊的对象this,是在运行时基于函数的执行环境绑定的,只能在函数内部使用。初学者在this的使用时容易出现问题(必须捂脸,这个坑我掉过),真是傻傻分不清楚呀。

其实只要理解一句话就ok了~~this引用的是函数据以执行的环境对象,是调用函数的那个对象

看几个例子理解一下吧

  • 纯粹的函数调用
1 var x = 1;
2 function test(){
3    this.x = 0;
4 }
5 test();
6 alert(x);

运行结果:弹出0.

函数test是在全局执行环境执行的,所以此时this对象引用的是window。

  • 作为对象方法调用

 

var o = {};
o.x = 1;
function a() {
    alert(this.x);
}
var x =2;
o.m = a;
o.m();

运行结果:1.

函数a是对象o的方法,a据以执行的环境对象时o,所以此时this对象引用的是o。

  • 作为构造函数使用
function test(){
   this.x = 1;
}
var o = new test();
var a = 2; alert(o.x);

在上述例子中,o是构造函数test的实例,一个新对象,此时this对象引用的是o。

  • apply调用
1 var x = 2;
2 function test(){
3   alert(this.x);
4 }
5 var o={};
6 o.x = 1;
7 o.m = test;
8 o.m.apply();

apply()的参数为空时,调用的是全局对象,所以此时this对象引用的是window。


 

 → →说好的会后进更新,补充this的一点问题。

function Person(name,sex,age) {
    this.name = name;
    this.sex = sex;
    this.age = age;
}
var person = Person("kiki","women","20");
console.log(person.name);

 我们知道,当使用new调用时,构造函数内用到的this指针对象会指向新创建的对象实例,但是,问题在如果没有使用new操作符来调用该构造函数的情况上,由于该this对象是在运行时绑定的,所以直接调用Person(),this会映射到全局对象window上。导致错误对象属性的意外添加。

JavaScript作用域学习笔记

解决这种this晚绑定问题的方法是创建一个作用域安全的构造函数

那么所谓的创建作用域安全的构造函数是什么呢?首先确认this对象是正确类型的实例,如果不是,就会创建新的实例并返回。

function Person(name,sex,age) {
	if(this instanceof Person) {
		this.name = name;
        this.sex = sex;
        this.age = age;
	}
	else {
		return new Person(name,sex,age);
	}
    
}

 JavaScript作用域学习笔记

 

 


 

(六)call()和apply()

每个函数都包含两个非继承而来的方法call()和apply(),这两个方法的用途是在指定作用域中调用函数,在这里提到他们是因为这两个方法能够扩充函数赖以运行的作用域。

apply()方法接受两个参数:一个是在其中运行的函数的作用域,另一个是参数数组;call()方法的参数:一个和apply一样是在其中运行的函数的作用域,其余参数都传递给函数。

 1 var a = 0;
 2 var o = {
 3     a: 1
 4 };
 5 function cout() {
 6     alert(this.a);
 7 }
 8 cout();   //0
 9 cout.call(this);  //0
10 cout.call(window);  //0
11 cout.call(o);    //1

 (七)eval()

差点漏掉这个了~~想起来就补充~~

eval()它会将传入的参数当做实际的ECMAScript语句来解析,然后把执行结果插入到原位置。被执行的代码具有与该执行环境相同的作用域链

在eval中创建的任何变量或函数都不会被提升,因为在解析代码的时候,它们被包含在一个字符串中。。

唔~~类似还有一个立即函数,下面说^ - ^

恩……在《javascript模式》里提到避免使用eval(),“eval()是一个魔鬼”(好像很厉害的样子,怕怕~~)使用eval()包含一些安全隐患,处理来自一个Ajax请求的Json响应时有可能执行被篡改过的代码。

(八)立即函数

本质上只是一个函数表达式,该函数在创建后立即执行。

1 function out(count) {
2     (function () {
3         for (var i = 0; i < count; i++) {
4             console.log(i);
5         }
6     })();
7     alert(i);
8 }
9 out(5);

运行结果:

JavaScript作用域学习笔记

立即函数可以模仿块级作用域,在匿名函数中定义的任何变量都会在执行结束时被销毁。因此,变量i只能在循环中使用,使用后立即被销毁。

在《javascript高级程序设计》和《javascript模式》中都提到这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数

未完待续……