JavaScript预解析作用域闭包/this关键字的指向理解

浏览器预解析

      浏览器是一个综合的软件,可以打开txt文本、照片、甚至符合格式的视频拖拽进去也能播放,也就是说浏览器支持各种格式的文件解析,具体的原理我们不用知道,需要知道的是,浏览器有一个专门负责解析js语句的东西,我们姑且叫做“js解析器”。

      在js解析器遇到script标记后,就启动解析器解析我们的js代码,这个解析过程中包括很多步骤,比如语法检查、垃圾回收…等等很多,但是其中有2点和我们今天说的作用域有关系,分别是”预加载“和”逐步解析“。

1、js预加载

      当浏览器来执行我的js代码时,并不是一上来就开始一行行的执行我们的代码,而是在执行前有一个“预加载”的操作,预加载做了什么事情呢?

      1> 在当前域中,找到已var 开头的变量,function定义的函数,和参数,并将其提取出来,放到“仓库”中。

      2>将var定义的变量保存起来后,给其赋值为:undefined。需要主意的时候,预加载的时候,不会执行变量后面的表达式,只是单纯的提取出变量名,然后给其值undefined

      3>将function定义的函数名字提取出来,给其赋值为当前函数体,因为js解析器也不知道函数里面有什么东西,所以讲整个函数直接赋值给当前函数名称。

      4>讲参数也提取到“当前域”中,和var 类似,“当前域”这个下面会讲到。

      上面是js预加载做的事情,简单的说,在当前域中(先可以理解为script中的代码)找到var funciton 和参数这三样东西,然后提取出来放到仓库中,第一步完成。

2、逐步解析

当js预加载完成后,开始执行第二步,逐步解析,通俗的说也就是一行一行的读取代码。我们已第一个代码片段为列子说明。

当预解析后,找到了var定义的变量,只有a是var定义,就把a放在仓库中,定义为undefined。第一步完成后,执行第二步,逐行执行代码。一上来第一行就是:console.log(a),

当读取到变量a的时候,浏览器就到仓库中去找a,因为经过了预解析,在执行代码前已经把a存起来了,并且赋值为undefined了,所以找打了a并且输出了他的值:undefined。

继续往下,读到了表达式 var a = “hello world!”; 同理,找到仓库中的a,并且赋值,记住表达式可以更改仓库中的值。然后继续往下走,读取b的时候,因为仓库中并没有存到变量b,因为他不是var定义的,所以没有,读取一个没有的变量,当然就报错了。

~~~~

1.JS预解析 #

当浏览器加载html页面的时候,会先提供一个供全局JS代码执行的环境,称为全局作用域(global/window)
预解析仅发生在当前作用域中(刚开始时仅对window下的进行预解析,只有函数执行时才对函数的进行预解析)
函数名存储的是一个内存地址,代表了函数本身,函数名加括号代表函数执行;函数执行结果为return返回的内容,若没有return则返回值为undefined
对象名存储的也是一个内存地址,当修改对象属性时先找到对应的空间,然后修改属性
在当前的作用域中,JS代码执行之前浏览器会默认把所有的var和function的进行提前的声明或定义

1.1 变量的预解析 #

变量在预解析时仅提前声明

console.log(num);  //undefined
var num = 12;
console.log(num);  //12
if(false){
  var a = 13;
}
console.log(a);  //undefined

预解析时仅var num; 告诉浏览器在全局作用域中有一个num变量了,此时还未定义
不管if条件成立不成立,里面有var声明都进行预解析

if(!("num" in window)){
  var num = 12;
}
//条件成立才会赋值
console.log(num);  //undefined

1.2 函数的预解析 #

  1. 函数在预解析时提前声明和定义

    showMsg();  //这是第二个函数
    function showMsg(){
    alert("这是第一个函数");
    }
    showMsg();  //这是第二个函数
    function showMsg(){
    alert("这是第二个函数");
    }
    

    函数执行时先创建自己的私有作用域,然后如果有形参,先给形参赋值,再进行私有作用域的预解析,再代码从上至下执行
    私有变量:在私有作用域中声明的变量和函数的形参

  2. var fn = function(){};在JS预解析时,在匿名函数表达式中只预解析等号(=)左边的,右边的作为值不参与预解析.即var fn;

    fn();  //报错 TypeError: fn is not a function //该项不为函数,不能执行
    var fn = function(){
    console.log("ok");
    }
    
  3. 自执行函数定义的function在全局作用域下是不进行预解析的,当代码执行到这个位置时,定义和执行一起完成
  4. 函数体中return下面的代码虽然不再执行了,但是也会预解析;return后面跟着的都是返回值,所以不进行预解析
    function fn(){
    console.log(num);
    return function(){};  //该function作为返回值不进行预解析
    var num = 12;  //该num作为声明变量进行预解析
    }
    fn();
    

1.3 全局变量 #

带var的变量可以进行预解析,在赋值之前执行不会报错
不带var的变量是不能进行预解析的,在赋值之前执行会报错

console.log(num);  //报错:num is not defined
num = 12;
num = 12;
console.log(num); // 12  window.num

全局作用域中增加了一个全局变量num,相当于给window增加了一个num属性,值为12
注:JS中在不做任何特殊情况处理下,上面的代码报错,下面的代码都不再执行
在JS中,变量名和函数名重复了,是冲突情况;在预解析时如果名字已经声明过了不需要重新声明,但要重新赋值

var fn = 12;
function fn(){...}
//这两个fn是一个名字,最终只能保留一个值,最终fn为变量
fn();
function fn(){console.log(1)}
fn();
var fn = 12;
fn();
function fn(){console.log(2)}
fn();

2.作用域链 #

2.1 私有作用域 #

在私有作用域中遇到的变量,若为私有变量则与外界变量没有任何关系,若不是私有变量,则在当前作用域上一级进行查找,直到找到window为止
私有作用域中出现的变量不是私有的,则往上级作用域中查找,若还没有则继续向上查找,直到window为止;若window下也没有则报错
私有作用域下保护了私有变量不受外界干扰,外界变量不能修改私有变量,同样私有变量也不能修改外界变量

console.log(total);  // undefined
var total = 0;
function fn(num1,num2){
  console.log(total);  //undefined
  var total = num1+num2;
  console.log(total);  //300
}
fn(100,200);
console.log(total);  //0

2.2 查找上级作用域 #

  1. 如何查找当前作用域的上级作用域:看当前函数是在哪个作用域下定义的,那么它的上级作用域就是谁
  2. 和函数在哪里执行没关系,和函数在哪里定义有关系
    var num = 10;
    function fn(){
    var num = 100;
    return funciton(){
     console.log(num);
    }
    }
    var f = fn();
    f();
    ~function(){
    var num = 1000;
    f();
    }();
    

    f()的值和执行环境无关,和定义环境有关

3.内存释放和作用域销毁 #

3.1 堆内存 #

存放引用数据类型值,比如对象的属性名和属性值,函数的字符串
对象数据类型或函数数据类型在定义时都会开辟一个堆内存,并且有一个引用地址;如果有变量引用这个地址,那就认为这段内存被占用,不能被销毁了
null为空对象指针
将变量值赋为null即可释放堆内存,浏览器在空闲的时候会把它销毁

var obj1 = {"name":"张三"};
var obj2 = obj1;
obj1 = null;
obj2 = null;

3.2 栈内存(作用域) #

私有作用域,只有函数执行才会产生私有作用域(for,if,switch的大括号不是私有作用域)
全局作用域属于不销毁作用域,只有当页面关闭的时候才会销毁
一般情况下,函数执行完成后,私有作用域会主动进行释放和销毁
特殊情况下,当前私有作用域的部分内存被作用域以外的东西占用了,那么当前作用域就不能被销毁了

  1. 函数执行返回了一个引用数据类型的值,并且在函数的外面被一个其他东西接收了,不销毁
    function fn(){
     var num = 100;
     return function(){
       console.log(num);
     }
    }
    var f = fn();  //返回值被f接收,fn不能被销毁
    
  2. 自执行函数形成的私有作用域在这种情况下也不销毁
var oDiv = document.getElementById('div1');
~function(){
oDiv.onclick = function(){

}
}();

oDiv为一个引用数据类型,引用click函数的一段内存;自执行函数形成的私有作用域,包含oDiv的点击事件,也引用同一段内存(在私有作用域中给DOM元素的事件绑定方法,这种情况下私有作用域不销毁)
注:通过DOM方法获取的元素、元素集合都是对象数据类型的值

var oDiv = document.getElementById('div1');
  1. 不立即销毁:fn返回的函数没有被其他东西占用,但还需要执行一次,所以暂时不能销毁,当返回的值执行完后浏览器在空闲时间把它销毁
  function fn(){
    var num = 12;
    return function(){

    }
  }
  fn()(); //表示执行函数fn,再执行返回值的函数

4.this关键字 #

this代表当前行为执行的主体;
context指当前执行的环境和区域;
this和context没有必然联系

4.1 window下的this #

函数执行首先看函数名前有没有点,有的话前边是谁this就是谁,没有的话就是window

function fn(){
    console.log(this);
}
var obj = {fn:fn};
fn();  // 方法名前没有点
obj.fn();  //方法名前有点
function sum(){
    fn();
}
sum();  //与执行环境无关
var aa = {
    sum:function(){
      fn();
    }
}
aa.sum();  //this --> window与执行环境无关

4.2 自执行函数this #

自执行函数的this永远是window

4.3 指定元素this #

给元素的某一事件绑定方法,当事件触发的时候,执行对应方法时当前this指该元素

document.getElementById('div1').onlick = fn;  //this ==> #该方法
document.getElementById('div1').onlick = function(){
    //该方法的this指#div1
    fn(); //该方法的this依然是window
}

~

Javascript是一种很灵活的语言, 而This关键字又是灵活中的灵活, 但是因为它的灵活, 也注定了它的难用.

以前我用this的时候, 都会觉得不踏实, 老是担心它不知道怎么地就会指到另外的什么地方.

其实, 这都是因为, 我们对它的不了解.

刚好最近再给百度学院做《Javascript高级-作用域/原型链》的ppt, 而swit1983网友也刚好提这个问题, 索性就把这部分内容独立总结出来, 与大家分享.

首先, 我先抛出一个定论:”在Javascript中,This关键字永远都指向函数(方法)的所有者”.

函数

首先,让我们看看”函数”:

function introduce() {
alert("Hello, I am Laruence\r\n");
}

对于,这个函数, this关键字指向谁呢?

如我之前的文章所述(Javascript作用域), 定义在全局的函数, 函数的所有者就是当前页面, 也就是window对象.

这也就是为什么, 我把函数用引号引起来. 因为定义在全局的函数, 其实也就是window对象的一个方法.

所以,我们即可用通过函数名直接调用, 也可用通过window.方法名来调用, 这个时候, 方法中的this关键字指向它的所有者:window对象.

如果, 我们查看window的introduce属性, 就会得到:

var name = "I am Laruence";
function introduce() {
alert(this.name);
}
alert(window.introduce);
/**
* output:
* function introduce() {
* alert(this.name);
* }
*/

看了上面的代码, 也许你就会想到既然, 全局函数是window对象的方法, 全局变量是window对象的属性(Javasript作用域中已有述及), 那么,在全局函数中就可用通过this关键字来访问全局变量吧?

答案是肯定的, 如果调用introduce函数, 你就会认识我是Laruence.

事件处理函数

也许, 对于this关键字的迷惑, 绝大部分原因是来自把函数(方法)用在事件处理的时候.

<input id="name" type="text"  name="name" value="Laruence" />

比如, 我们现在需要在点击”name”输入框的时候, 显示出name输入框的value. 那么, 可用写下如下代码:

function showValue() {
alert(this.value);
}
document.getElementById('name').onclick = showValue;

上面的代码, 运行正常, 但是why? 不是说函数的this指针永远指向函数所有者么? 不是说全局变量的所有者是window对象么?

呵呵, 如果你能想到这个问题, 那说明你在认真的看我的文章, 否则,,我建议你从头看起, 否则看完了,你还是迷糊~

恩, 对, 对于上面的代码, showValue是定义在了全局对象, 那么看来问题只能发生在onclick事件绑定的时候了.

我们知道, 在Js中一切都是对象, 函数和方法也都是对象的属性, 只不过函数有可执行的内部属性. 所以, 对于上面的代码, 在对onclick绑定处理器的时候, 其实是对id为name的输入框Dom对象的onclick属性赋值.

也就是说, 我们把函数showValue Copy 给了name输入框对象的onclick属性. 如果我们查看此时的onclick:

function showValue() {
alert(this.value);
}
document.getElementById('name').onclick = showValue;
alert(document.getElementById('name').onclick);
/**
* output
* function showValue() {
* alert(this.value);
* }
*/

所以, 当事件触发的时候, 就会去调用name输入框的onclick方法, 这个时候,this关键字自然就指向的是name输入框了.

但是, 迷惑的事情就来了, 比如下面这种写法:

function showValue() {
alert(this.value);
}
<input id="name" type="text" name="name" value="Laruence" onclick="showValue()"/>

就无法正常运行, 这又是为什么呢?

恩, 因为这个时候, 并不是赋值, 而是引用.

如果我们注意俩种onclick的写法, 你会发现, 对于之前的方法, 我们使用的是:

dom.onclick = showvalue; //没有调用符

而对于刚才的方法:

onclick = "showvalue()" //有调用符

这个也能侧面的反映出俩者的区别:对于前者,是赋值, 而对于后者是引用. 如果我们这个时候查看输入框的onclick属性,我们得到:

alert(dom.onclick);
/**
* output:
* function onclick() {
* showValue();
* }
*/

看到区别了么? 也就懂得为什么了吧?

讲到这里, 有一个很有趣的例子 ,大家可以在IE下试试:

<img src="xxx" onerror="alert(1);} function hi() { alert(2); " />
改变this的指向

那, 既然我们已经知道为什么了, 那怎么才能让this指向我们想要指的地方呢?

对于上面的事件处理函数来说, 我们可以有如下几种写法:

dom.onclick = showValue();
dom.onclick = function() { alert(this.value) ;}
<input onclick="alert(this.value);" /> //想想刚才我们的引用,是如何把这句嵌入的.
dom.addEventListener(dom, showValue, false); //ff only

而对于不是事件处理函数的场合, 我们可以使用apply, 或者call, 来改变this关键字的指向.

比如:

var laruence = {
name : 'laruence',
age : 26,
position : 'Senior PHP Engineer',
company : 'Baidu.inc'
};

function introduce() {
alert(this.name);
}

introduce.call(laruence);

~~~~

未经允许不得转载:WEB前端开发 » JavaScript预解析作用域闭包/this关键字的指向理解

赞 (0)