理解JavaScript面向对象(三):作用域链及原型

来源:互联网 发布:java构造器和构造函数 编辑:程序博客网 时间:2024/06/04 20:11

前两篇我们花费了大量的篇幅讲解了JavaScript中面向对象的基本概念,理解js中的对象,以及JavaScript中为我们提供的原生对象Object,这些都是理解JavaScript面向对象的基础。为了能够理解今天我们要一起研究的原型,在介绍原型之前,我们先来了解几个概念:

1)执行环境和作用域链:

执行环境(简称环境)定义了变量,或者函数有权访问的其他数据,并决定了他们各自的行为。每个执行环境都对应的有一个与之关联的对象(称为变量对象)。虽然我们在编程的时候不直接使用它,但是解析器处理数据时会用到它(之后会举例)。执行环境的最外围是全局执行环境,在浏览器中,全局执行环境为window对象。因此,所有全局变量和函数都是作为window对象的属性和方法而创建的。

在ECMAScript中,每个函数都有自己的执行环境,当程序进入一个函数执行时,就把这个函数的执行环境压入到执行环境栈中。当函数执行完成,再把执行环境从栈中弹出,将执行控制权返回给之前的执行环境。ECMAScript用这种机制控制着程序的执行。为了保证对执行环境中的变量和函数有序的访问,ECMAScript当代码在执行环境中执行时,会创建变量对象(与执行环境关联)的作用域链,链的最前端始终是当前执行代码所在的执行环境相关联的变量对象。下一个对象是来自包含环境(包含这个环境的执行环境),再下一个对象是包含环境的包含环境,以此类推直到全局环境变量对象(始终是作用域链的最后一个对象)。系统判断能否访问某个属性(包括函数,因为函数本质也是对象)就是根据这个作用域链逐级查找的。我们看一个例子,加深一下理解:

理解JavaScript面向对象(三):作用域链及原型 注意:如果执行环境是函数,就把它的活动对象(函数的实例)作为变量对象,活动对象在开始时只包含一个对象,就是arguments对象。在上面的例子中,changeColor()的作用域链包含两个对象:它自己的变量对象和全局变量对象。函数中之所以能访问color,是因为从它的作用域链中能够找到它。我们看一个复杂一点的例子:

理解JavaScript面向对象(三):作用域链及原型上面的例子中,一共有三个执行环境,全局环境,changeColor局部环境,swapColor执行环境。此时的作用域链如图:

理解JavaScript面向对象(三):作用域链及原型 当执行swapColor()时,虽然swapColor的执行环境中有tmpColor,但是它的父环境(作用域链的下一个环境对象)中包含innerColor,父环境的父环境中包含color,所以,它可以访问所有变量。但是,全局环境就只能访问Color了。

所以,作用域链中的环境变量对象之间的联系是线性的,有序的。内部环境可以通过作用域链访问外部环境,而外部环境不能访问内部环境的变量和函数。每个环境都可以向上搜索作用域链,以查询变量或函数名,但是任何环境都不能向下搜索作用域链而进入到另一个执行环境。

2)再谈函数:

之前我们谈过,函数的本质也是对象,函数名仅仅是指向这个对象的指针而已。既然函数也是对象,它自然也就和其他对象一样,同样具有属性和方法。今天我们就一起看一看,函数的属性和方法:

this属性:是一个特殊的对象,它是一个指针,指向的是函数的执行对象。

arguments:是一个类数组对象,因为它不是Array的实例,但是可以通过[ ]的语法访问。这个对象用于保存传递给函数的参数(实际参数)。

length:表示函数希望接收的命名参数的个数(形参个数)。

prototype:对于引用类型而言,prototype是真正保存他们的所有实例的方法的对象(原型对象)。任何函数都具有的属性,由这个函数所创建的对象,默认会连接到这个属性上。

理解JavaScript面向对象(三):作用域链及原型 上面这个例子中,构造方法没有任何成员,而在它的原型对象中添加了name属性。在访问一个对象的属性(包括函数)会在当前对象中去查找,如果没有就会到与之关联的构造方法中去查找,再没有就到与构造方法相关联的原型对象中去查找。

理解了这些,我们就一起揭开原型的神秘面纱,大家都知道,JavaScript是基于原型而实现面向对象的,那么原型有什么好处呢?我们看一个传统的构造函数创建对象的例子:

理解JavaScript面向对象(三):作用域链及原型此例中,实例化了两个Person对象p1和p2,各自都有一个sayHello()的函数。此时的内存结构如图:

理解JavaScript面向对象(三):作用域链及原型

输出的结果是:

理解JavaScript面向对象(三):作用域链及原型 虽然是两个不同对象,但是函数的逻辑功能是相同的。由于我们内存资源是很宝贵的,考虑到数据的共享性,属性名称是各个对象独立的,而函数是实现相同的逻辑的,可以多个对象来共享。因为我们都知道,对象会到与之联系的prototype中去寻找数据,所以,可以考虑将共享的数据放到prototype中,这样,可以保证,无论创造多少对象,这个方法只有一个副本。这可以大大的节省内存资源,而且创建的对象越多,优势就越明显。引入原型后,同样创建两个对象,内存结构如图:

理解JavaScript面向对象(三):作用域链及原型可以看到,每个对象都有一个__proto__的属性,通过调试,我们可以发现对象的__proto__和它对应的构造函数(创建它的构造函数,否则没有关系)的prototype属性其实是同一个对象,这也就是我们之前在将prototype属性时提到的:由这个函数所创建的对象,默认会连接到这个属性上。我们用一个例子去理解一下:

理解JavaScript面向对象(三):作用域链及原型 输出结果也的确是true。那么,我们该如何去理解这两个属性呢?因为__proto__是非标准属性,是相对于对象的实例而言的。我们所说的“原型对象”也没有一个统一的规范。我们可以根据具体的访问情况来理解,例如上面的定义中:Person.prototype 是函数Person的原型属性,是对象p1的原型对象。由于__proto__是非标准的属性,而在大部分描述时我们用prototype。再说原型属性时是针对构造函数Person而言的,在说原型对象时是针对对象实例p1而言的。进行如此简单的划分就不至于混乱了。正是因为原型的引入,实现了数据共享的方法,也就奠定了JavaScript中面向对象的基础。
0 0
原创粉丝点击