RSS Feed

Posts Tagged ‘JS’

  1. JS的面向对象

    April 27, 2012 by xudifsd

    JS在很长一段时间内都被认为是没什么多大用处的语言,而且JS的程序员也不被重视,常常有类似于这样的程序员鄙视图,而且JS程序员总是处在被很多其他程序员鄙视的地位。但是原因主要是由于JS只能在浏览器中使用,出了浏览器JS什么也干不了。以前学《JavaScript语言精粹》看它介绍了很多复杂的面向对象的东西,我当时就觉得奇怪,为什么一个网页上的程序要这么复杂的技术,所以当时很多东西都直接跳过。但是现在有了Node,它将JS从浏览器中解放了出来,并且用在了高性能的服务器端,这让JS有了很大的用途。所以重新复习了下《JavaScript语言精粹》,记一下笔记。

    JS的面向对象

    JS支持面向对象,只是它的支持方法有些非主流,它使用“原型链”来实现面向对象。只是原型链对于传统编程人员来说很难理解,但是一旦理解其实会发现这种轻量级的继承表现力很强。

    JS有很好的对象的字面量表示,也正是因为JS的这种强大表示方法才有了JSON这种通用的数据交换格式。所以在JS中可以直接写出在其他语言需要“实例化”才能产生的对象。但是实际编程时,往往需要“未实例化”的对象的模板,也就是“类”,这样才能创造出许多相似但不同的“对象”。显然用JS对象的字面量来创造对象不能实现这一点,为了支持这一点,JS支持一种“伪类”,也就是原型链。

    用下面的代码就可以创造出一个叫“Cat”的伪类,和一个叫“cat”的实例,且cat有个实例变量name。

    var Cat = function(name) {
    	this.name = name;
    };
    
    var cat = new Cat("foo");

    这个语法和Java很像,但是这个例子并没有用到原型链,所以也就没有实现继承的。

    下面的代码用原型链实现的继承:

    var Animal = function(sound) {
    	this.sound = sound;
    };
    
    var Cat = function(name) {
    	this.name = name;
    };
    
    Cat.prototype = new Animal("meow");	//实现继承
    Cat.prototype.says = function() {
    	return this.sound + ', ' + this.name;
    };

    这里Cat伪类继承了Animal,通过原型链Cat子类可以访问在父类中定义的变量sound(通过this.sound访问)。但是这一段程序估计那些用正经类的人根本看不懂。下面进行一些解释:

    JS中的函数有四种调用方法

    一般静态语言的程序调用函数非常简单,将参数传入,取得返回值就行。支持闭包的程序就更复杂一些了,由于函数可以动态定义,所以还需要考虑函数定义时的环境。JS也支持闭包,但是JS的函数调用更加复杂:不同的函数调用方法会影响this关键字指向什么东西。

    JS的函数总共有四种调用方式:

    1. 方法调用模式
    2. 函数调用模式
    3. 构造器调用模式
    4. Apply调用模式

    这里只说构造器的调用模式。其他的可以看《JavaScript语言精粹》的4.3节。

    面向对象的特点之一就是允许对象有自己的私有空间,对象外部必须通过对象才能引用对象的私有空间内容,而对象的方法也需要有个关键字来引用自己的私有空间,在JS中这个关键字就是this。前面说的四种调用方法就会影响this的绑定。

    并且JS的函数的this是超级迟的绑定,声明了函数之后this是不一定绑定在哪,这样就允许JS中对函数的this进行高度地复用。在函数的构造器调用模式下this是绑定到一个新创建的对象,并且这个对象有一个隐藏连接到函数的prototype对象(注意,函数也是对象,它也有自己的属性)。构造器调用模式最后会返回这个新创建的对象。

    图解

    一图胜千言:

    var father = {
    	name: 'father'
    };

    这里用JS对象的字面量直接声明一个对象,这时内存中的样子像这样:

    var Child = function(name) {
    	this.name = name;
    };

    这里声明一个函数,由于JS支持闭包,所以这时在内存中函数还会有闭包属性,这个闭包包括了定义Child函数的环境的所有信息(在调用函数时会用到),但与其他语言不同的时JS的函数对象还有prototype属性,由于JS无法知道哪些函数是作为构造器来调用的,所以每个函数都会有这个属性,它指向一个有constructor属性的对象,并且该constructor指向函数本身:

    Child.prototype = father;

    这里修改了Child的prototype属性,令它指向前面创建的father对象,这时如果修改了father.name属性,那么也会在Child.prototype.name中体现:

    Child.prototype.get_name = function() {
    	return this.name;
    };

    给Child.prototype添加一个get_name属性,这个属性指向一个新创建的函数,该函数也有自己prototype和闭包属性,并且由于Child.prototype和father指向同一个对象,father也就有了get_name属性:

    var mychild = new Child('foo');

    这里是整个原型链的关键,注意必须用new来调用Child函数,这样JS才知道Child是以构造器的方式来调用的,否则如果没有new那么JS就会以普通函数来调用,这样this就会错误地绑定到全局变量上去,造成全局变量中name被修改或者定义,而且mychild会变成undefined,这样会造成很难发现的bug。另一个值得注意的地方是mychild从此有了一个从Child.prototype继承下来的隐藏的prototype属性,这个属性无法通过mychild直接访问到,只是在访问未在mychild中定义而在father中定义的属性会用到(在这个例子中如果访问mychild.get_name也会得到想要的函数,并且由于get_name的this没有绑定到任何地方,所以如果用father.get_name调用,this会绑定father,如果用mychild.get_name调用则会绑定到mychild上)。但是如果访问mychild.name时会直接得到’foo’,因为JS读任何对象的属性都遵守这样的一个规则:如果对象本身没有该属性,就到对象的隐藏的原型链中去找,如果再没有再找对象原型的原型,直到搜到原型链底部或者找到。但是写或者删除对象的属性就不会涉及到原型链。

    delete mychild.name;

    在里用delete语句将mychild的name属性删除,所以mychild没有了name属性,但是这种删除会让原型链中的内容暴露出来,这时如果访问mychild.name会得到’father’,避免这种情况的方法就是通过mychild.hasOwnProperty(‘name’)的返回值来确定mychild本身是否有该属性。如果执行了删除后再执行delete mychild.name并不会删除father.name,因为删除或者写属性不会涉及到原型链:

    短处

    这种伪类的继承方法本身很好,但是由于必须用new来调用函数,容易造成一些难以发现的bug,所以《JavaScript语言精粹》的作者并不推荐这种方法,他主张用函数的闭包来实现继承,闭包可以到这里看,虽然是用scheme语言做例子,但是原理都一样,在这里就不废话了。