[关闭]
@mircode 2016-08-15T16:42:03.000000Z 字数 6008 阅读 578

JavaScript重难点概念解析

作用域 闭包 匿名函数 面对对象


一、作用域

先引用一下《JavaScript权威指南》中的一句话,函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。从这句话中,咱们可以分析出几点,一个是函数可以分为定义阶段**和执行阶段**。另一个就是函数,运行在它定义的作用域中,而不是它的执行作用域中。这会也许你还不明白它在说什么,可以先再心里反复嘟囔几次。是定义的作用域而不是执行的作用域。

  1. // 示例
  2. var name="weiguoxing";
  3. function echo(){
  4. console.info(name);
  5. var name="hello";
  6. console.info(name);
  7. console.info(age);
  8. }
  9. echo();
  10. // 执行结果
  11. undefined
  12. hello
  13. age is not defined [error]

是不是和很多人预测的不一样呢,要解释结果,我们就有必要了解一下JavaScript的函数模型。

1、函数定义阶段和函数执行阶段

JavaScript的语法风格和C/C++类似,但是作用域的实现却和C/C++不同,并非采用“堆栈”方式,而是使用的链表实现的。大体是这么个情况。

函数定义阶段:在定义阶段,会将函数内部的[[scope]]属性,指向函数执行时候的作用域链[[scope chain]]。

函数执行阶段:在初始化阶段,会创建一个活动对象(Activetion Object),并将this、arguments、命名参数和该函数的所有局部变量赋值,存储在改活动对象中。总之一句话,用于这个活动对象,存储函数内部定义的变量。赋值完成后,然后,会将这个活动对象,添加到作用域链[[scope chain]]的顶端(第一个位置)。然后,函数开始执行。执行过程中,JavaScript引擎通过搜索执行上下文的作用域链来解析诸如变量和函数名这样的标识符。解析标识符的过程从作用域的顶端开始,按照自上而下的顺序进行。

我们用例子解释一下

  1. // 定义函数
  2. function add(num1,num2){
  3. return num1+num2;
  4. }

在定义该函数阶段,add函数拥有一个仅包含全局变量对象的[[scope]]属性。

1.png

在执行add函数阶段,JavaScript引擎会创建一个新的执行上下文和一个包含this、arguments、num1、num2的活动对象,并把活动对象添加到作用域链中。在add()函数内部运行时,JavaScript引擎需要解析函数里的num1和num2标识符。var total = add(5, 10);

2.png

解析过程是从作用域链中的第一个对象开始,这个对象就是包含该函数局部变量的活动对象。如果在该对象中没有找到标识符,就会继续在作用域链中下一个对象里查找标识符。一旦找到标识符,查找就结束。

现在我们看一下最开始的例子

  1. // 示例
  2. var name="weiguoxing";
  3. function echo(){
  4. console.info(name);
  5. var name="hello";
  6. console.info(name);
  7. console.info(age);
  8. }
  9. echo();

在JavaScript中, 是有预编译的过程的,JavaScript在执行每一段JavaScript代码之前, 都会首先处理var关键字和function定义式(函数定义式和函数表达式)。这个过程也叫作变量提升

如上文所说, 在调用函数执行之前,会首先创建一个活动对象,然后搜寻这个函数中的局部变量定义和函数定义,将变量名和函数名都做为这个活动对象的同名属性, 对于局部变量定义,变量的值会在真正执行的时候才计算, 此时只是简单的赋为undefined。也就是针对上述代码,编译器预编译之后,效果是和下面代码是类似的。

  1. // 在实际执行过程中,会进行变量声明提升等操作
  2. var name="weiguoxing";
  3. function echo(){
  4. var name; // 提升变量声明
  5. console.info(name);
  6. name="hello";
  7. console.info(name);
  8. console.info(age);
  9. }

所以,执行结果,就是undefined,eve,控制台报错。

2、变量提升

  1. console.info(typeof echo); // function
  2. console.info(typeof print); // undefined
  3. function echo(str){
  4. console.info(str);
  5. }
  6. var print=function(str){ // 函数表达式
  7. console.info(str);
  8. }
  9. console.info(typeof print); // function

echo是通过函数定义方式产生的,所以JavaScript编译器会提升echo,率先执行。所以console.info(typeof echo);不是undefined而是function。但是对于函数表达式,虽然也会提升walle,但是赋值语句不会提升,所以walle一开始是undefined。上述代码和下面代码是一样的。

  1. function echo(str){ // 函数定义提升
  2. console.info(str);
  3. }
  4. var print // 变量定义提升
  5. console.info(typeof echo); // function
  6. console.info(typeof print); // undefined
  7. print=function(str){
  8. console.info(str);
  9. }
  10. console.info(typeof print); // function
  1. <script>
  2. console.info(typeof echo);
  3. </script>
  4. <script>
  5. function echo(str){
  6. console.info(str);
  7. }
  8. </script>

但是要注意,如下情况
这种,情况alert(typeof echo);输出也是undefined。因为,变量提升是以段为单位的。也就是作为提升单元。

3、改变函数 作用域链

函数每次执行时对应的运行期上下文都是独一无二的,所以多次调用同一个函数就会导致创建多个运行期上下文,当函数执行完毕,执行上下文会被销毁。每一个运行期上下文都和一个作用域链关联。一般情况下,在运行期上下文运行的过程中,其作用域链只会被 with 语句和 catch 语句影响。
with语句是对象的快捷应用方式,用来避免书写重复代码。例如:

  1. function initUI(){
  2. with(document){
  3. var bd=body,
  4. links=getElementsByTagName("a"),
  5. i=0,
  6. len=links.length;
  7. while(i<len){
  8. update(links[i++]);
  9. }
  10. getElementById("btnInit").onclick=function(){
  11. doSomething();
  12. });
  13. }
  14. }

这里使用width语句来避免多次书写document,看上去更高效,实际上产生了性能问题。当代码运行到with语句时,运行期上下文的作用域链临时被改变了。一个新的可变对象被创建,它包含了参数指定的对象的所有属性。这个对象将被推入作用域链的头部,这意味着函数的所有局部变量现在处于第二个作用域链对象中,因此访问代价更高了。如下图所示:
3.png

因此在程序中应避免使用with语句,在这个例子中,只要简单的把document存储在一个局部变量中就可以提升性能。

另外一个会改变作用域链的是try-catch语句中的catch语句。当try代码块中发生错误时,执行过程会跳转到catch语句,然后把异常对象推入一个可变对象并置于作用域的头部。在catch代码块内部,函数的所有局部变量将会被放在第二个作用域链对象中。示例代码:

  1. try{
  2. dosometing();
  3. }catch(ex){
  4. console.info(ex.message); //作用域链发生改变
  5. }

请注意,一旦catch语句执行完毕,作用域链机会返回到之前的状态。try-catch语句在代码调试和异常处理中非常有用,因此不建议完全避免。你可以通过优化代码来减少catch语句对性能的影响。一个很好的模式是将错误委托给一个函数处理,例如:

  1. try{
  2. dosometing();
  3. }catch(ex){
  4. handleError(ex); // 委托给处理处理
  5. }

优化后的代码,handleError方法是catch子句中唯一执行的代码。该函数接收异常对象作为参数,这样你可以更加灵活和统一的处理错误。由于只执行一条语句,且没有局部变量的访问,作用域链的临时改变就不会影响代码性能了。

4、总结

经过上面的描述,我们总结了如下几条:
1. JavaScript函数的作用域链分为定义时作用域链和运行时作用域链;
2. 函数被定义的时候,它有一个属性[[scope]]标明它的定义作用域链,定义时作用域链[[scope]]遵守这样的规则:一个函数的定义时作用域链[[scope]]总是它所在的外部函数的执行时作用域链;
3. 全局函数的定义作用域链只包含window的属性;
4. 一个函数的执行时作用域链总是在定义时作用域链的头部压入当前活动对象(它包含this,arguments,参数,局部变量);
5. 函数执行时,变量寻址总是从作用域链的顶端朝下寻找;所以全局变量的寻址速度最慢;
6. 内部函数被执行的时候,他仍然能够访问它完整的作用域链。这就是闭包能够在运行时能够访问已经结束的外部函数定义的变量的原因;
7. 函数执行遇到with语句时,会临时在作用域链顶部压入with指定的对象的所有属性作为作用域链最顶端;
8. 函数执行遇到catch的时候,会临时在作用域链顶部压入catch指定的错误对象作为作用域链的最顶端;

JavaScript没有块级作用域(EcmaScript 5版本中,可以支持块作用域了)。最小的是函数级别的作用域。如下代码输出的都是local

  1. var name="weiguoxing";
  2. if(true){
  3. var name="local";
  4. console.info(name);
  5. }
  6. console.info(name);

二、闭包

1、概念

闭包:是指有权访问另外一个函数作用域中的变量的函数。创建闭包的常见方式就是在一个函数内部创建另外一个函数。

2、原理

如上一节里面说的,每个函数的执行,都会创建一个与该函数相关的函数执行环境,或者说是函数执行上下文。这个执行上下文中有一个属性 scope chain(作用域链指针),这个指针指向一个作用域链结构,作用域链中的指针又都指向各个作用域对应的活动对象。正常情况,一个函数在调用开始执行时创建这个函数执行上下文及相应的作用域链,在函数执行结束后释放函数执行上下文及相应作用域链所占的空间。如
4.png

但是闭包的情况就有点特殊了,由于闭包函数可以访问外层函数中的变量,所以外层函数在执行结束后,其作用域活动对象并不会被释放(注意,外层函数执行结束后执行环境和对应的作用域链就会被销毁),而是被闭包函数的作用域链所引用,直到闭包函数被销毁后,外层函数的作用域活动对象才会被销毁。这也正是闭包要占用内存的原因。所以使用闭包有好处,也有坏处,滥用闭包会造成内存的大量消耗。

3、示例

  1. function a(){
  2. var i=0;
  3. function b(){
  4. console.info(i++);
  5. }
  6. return b;
  7. }
  8. var c=a();
  9. c();

5.png
因为JavaScript支持闭包,所以b中可以访问到父函数a中的i变量。当a调用完毕后返回b函数,b函数对a的作用域链并没有释放。所以对于b来说,a执行完后,仍然可以访问到a中的i变量。
6.png

4、应用

1、匿名自执行函数

  1. (function(){}();
  2. +function(){}();
  3. -function(){}();
  4. !function(){}();
  5. ~function(){}();

2、保护私有变量

  1. function Persion(name){
  2. var name=name;
  3. this.getName=function(){
  4. return name;
  5. }
  6. }

3、缓存结果

我们开发中会碰到很多情况,设想我们有一个处理过程很耗时的函数对象,每次调用都会花费很长时间,那么我们就需要将计算出来的值存储起来,当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,然后更新缓存并返回值,如果找到了,直接返回查找到的值即可。闭包正是可以做到这一点,因为它不会释放外部的引用,从而函数内部的值可以得以保留。

  1. function appendToHead(element) {
  2. var hardpoint,
  3. heads = document.getElementsByTagName('head');
  4. hardpoint = heads.length && heads[0] || document.body;
  5. appendToHead = function(element) {
  6. hardpoint.appendChild(element);
  7. };
  8. return appendToHead(element);
  9. }

三、面向对象

  1. /*****************************
  2. * 问题一:prototype
  3. *****************************/
  4. function Person(){
  5. this.name="weiguoxing";
  6. }
  7. Person.prototype.getName=function(){
  8. return this.name;
  9. }
  10. var p1=new Person();
  11. var p2=new Person();
  12. var res=p1.getName==p2.getName;
  13. console.info(res); //true,说明p1和p2,使用的同一份函数拷贝
  14. function Person(){
  15. this.name="weiguoxing";
  16. this.getName=function(){
  17. return this.name;
  18. }
  19. }
  20. var p1=new Person();
  21. var p2=new Person();
  22. var res=p1.getName==p2.getName;
  23. console.info(res); //false,说明p1和p2,各自持有自己的函数拷贝,并不是共享的
  24. /*****************************
  25. * 问题二:静态方法定义
  26. *****************************/
  27. Person.getName=function(){
  28. console.info("weiguoxing");
  29. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注