`
DBear
  • 浏览: 231836 次
  • 性别: Icon_minigender_2
  • 来自: 上海
社区版块
存档分类

JavaScript精炼---类(class)、构造函数(constructor)、原型(prototype)

阅读更多

类 Class

类的概念应该是面向对象语言的一个特色,但是JavaScript并不像Java,C++等高级语言那样拥有正式的类,而是多数通过构造器以及原型方式来仿造实现。在讨论构造器和原型方法前,我可以看看一种叫做“工厂方式”的仿造方法。

 

function start() {

   alert("Bang!!");

}

 

function createCar(color, title) {

   var car = {};

   car.color = color;

   car.title = title;

   car.start = start;

   return car;

}

 

var car1 = createCar("red", "BMW");

var car2 = createCar("yellow", "VOIS");

 

这种方式显然可以实现class的功能,但是外形上怎么也无法说它是个class以及class实例的创建过程。因此,出现了“构造函数方式”,它的关键在于构造器(Constructor)概念的引入。

构造器 Constructor

我们先来看“构造函数方式”的具体做法:

 

function start(){

   alert("Bang!!!");

}

 

//constructor

function Car(color, title){

   this.color = color;

   this.title = title;

   this.start = start;

}

 

var car1 = new Car("red", "BMW");

var car2 = new Car("yellow", "VOIS");

 

这个看起来有点类的样子了吧(先不提那个难看的外置function)?我们发现,那个constructor其实就是一个简单的function,它与“工厂方式”中的createCar()区别就在于:1、方法名大写 2、没有了空对象的创建和返回 3、使用this做引用。那原来的那个空对象的创建以及返回的步骤去哪了呢?这两个步骤,现在都由创建实例时的“new”实现了。“new”这个操作符负责创建一个空对象,然后将那个叫做构造器的function添加到实例对象中并触发它,这样这个function实际上就是这个对象的一个method,function中的this指向的便是这个对象,最后将这个对象返回。根据如上分析,我们可以把这个过程简单分解为如下代码:

 

var obj = {};

obj.constructor = Car;

obj.constructor("red", "BMW");  //”this“ refers to obj

return obj;

 

“构造函数方式”方式虽然与高级面向对象语言中的类创建方式已经很接近(使用”new“创建),但是貌似那个游离在类之外的function start()其实却是个相当有碍观瞻的瑕疵。我们应该想一种办法让这个方法与类挂钩,让它成为类的一个属性,不是全局的。于是,这就产生了“构造函数+原型法”的类构造方法。

原型 prototype

在”构造函数+原型法“中,我们对于类的method期待得到的效果是:1、仅是类的method而不是全局的 2、只在类被定义时创建一个method实例,然后被所有类的实例共用。由这两个目标,我们很容易想到高级面向对象语言Java的private static变量的特点。JavaScript没有为我们提供这么简单的符号来实现这个复杂功能,但是却有一个属性可以帮我们仿造出这种效果:prototype。我们先来看几段prototype的使用代码。

  1. function Car(){
  2. }

  3. Car.prototype.material = "steel";

  4. var car1 = new Car();
  5. var car2 = new Car();

  6. document.write(car1.material);  //prints "steel" 
  7. document.write(car2.material);  //prints "steel" 

  8. //car1.prototype.material = "iron" //compile error:car1.prototype is undefined
  9. car1.material = "iron";

  10. document.write(car1.material);  //prints "iron"
  11. document.write(car2.material);  //prints "steel" 
  12. document.write(Car.prototype.material); //prints "steel"

  13. Car.prototype.material = "wood";
  14. var car3 = new Car();
  15. document.write(car1.material);  //prints "iron"
  16. document.write(car2.material ); //prints "wood"
  17. document.write(car3.material ); //prints "wood"
  18. document.write(Car.prototype.material); //prints "wood"

分析该段代码前,需要明确两个概念:对象的直属属性和继承属性。直接在构造函数中通过this.someproperty = xxx这种形式定义的someproperty属性叫做对象的直属属性,而通过如上第4行代码那样Car.prototype.material = "steel";这种形式定义的material属性叫做继承属性。由上面这段代码,我们可以总结出prototype属性的如下特点:

 

  1. prototype是function下的属性(其实任意object都拥有该属性,function是对象的一种)
  2. prototype属性的值是一个对象,因此可任意添加子属性(line 4)
  3. 类的实例可以直接通过"."来直接获取prototype下的任意子属性(line 9)
  4. 所有以此function作为构造函数创建的类实例共用prototype中的属性及值(ling 9,10)
  5. 类的实例没有prototype属性(line 12)
  6. 可以直接通过 "实例.属性 = xxx" 的方式修改继承属性,修改后的值将覆盖继承自prototype的属性,但此修改不影响prototype本身,也不影响其它类实例(line 15,16,17)
  7. 继承属性修改后,该属性就成为类实例的直属属性
  8. 可以直接修改prototype的属性值,此改变将作用于此类下的所有实例,但无法改变直属属性值(极晚绑定line 21-24)

      在起初总结出prototype的这些特点时,我最不能理解的是第三条,为什么类的实例可以像获取自身属性那样的获取function prototype中的属性。有些书上说是因为这些属性被附加到了对象实例中(sigh,我刚开始这样以为),但是,这种说法无法是第8条特性成立,后来看到一位网友的解释是:对象实例在读取某属性时,如果在本身的直属属性中没有查找到该属性,那么就会去查找function下的prototype的属性。这个观点能够很好的解释prototype的第8个特点,有查看了一些书,发现JavaScript—The Definitive Guide这本书里也是如此解释的:)

 

Tip:我们可以通过hasOwnProperty方法来判断某属性是直属于对象还是继承自它的prototype属性

  1. car1.hasOwnProperty("material"); // true: material is a direct property of r
  2. car2.hasOwnProperty("material"); // false: material is an inherited property of r
  3. "material " in car2;// true: "material " is a property of r

 

好好理解下prototype的这些特点,我们不难看出,在prototype中定义的属性与Java类中的static属性特点极为相近,适合定义那些所有类实例都可共用的一些属性的值,比如汽车的构造材料这一类。

了解了prototype的这些特点,我们可以言归正传,把它应用到类方法的仿造和实现上了。其实做法很简单,就是把value换成function,先看下面的代码:

  1. function Car(){
  2. }

  3. Car.prototype.start = function(){

       alert("Bang!!!");

    };


  4. var car1 = new Car();
  5. var car2 = new Car();
  6. car1.start();  //bang!!!
  7. car2.start();  //bang!!!

得益于prototype的强大特性,我们知道这个method确实是可以这么被绑定到类上的。但是,最有用的是,那个method只会在执行Car.prototype.start =这个定义的时候创建一个实例,之后通过new创建的对象实例里面使用的这个method都是它的引用,因此不会有内存浪费。

好了,method的问题终于解决了,那么整理一下,下面写出使用“构造函数+原型方式”构造一个类的完整代码:

 

  1. function Car(color, title){
  2.    this.color = color;
  3.    this.title = title;
  4. }

  5. Car.prototype.material = "steel";
  6. Car.prototype.start = function(){
  7.   alert("Bang!!!");
  8. };
  9. Car.prototype.repaint = function(color){
  10.     this.color = color;
  11. }

  12. var car1 = new Car("red", "BMW");
  13. var car2 = new Car("yellow", "VW");

  14. car1.repaint("pink");
  15. document.write(car1.color);  //prints pink
  16. document.write(car2.color);  //prints yellow

 

   怎么样,这个跟高级面向对象语言中的class的样子更~加类似了吧?Perfect ?NO!!!上述写法只是在“语义”上达到了对类属性和方法的封装,很多面向对象思想的完美主义者希望在“视觉”上也达到封装,因此就产生了“动态原型法”,请看下面的代码:

  1. function Car(color, title){
  2.    this.color = color;
  3.    this.title = title;
  4.    if (typeof Car._initialized == "undefined") {
  5.       Car.prototype.start = function(){
  6.           alert("Bang!!!");
  7.       };
  8.       Car.prototype.material = "steel";
  9.       Car._initialized = true;
  10.    }
  11. }

   我们看,其实Car.prototype的属性定义是可以被放进Car function的定义之中的,这样就达到了“视觉”封装。但是我们没有单纯的move,我们需要加一个条件,让这些赋值操作只执行一次,而不是每次创建对象实例的时候都执行,造成内存空间的浪费。添加_initialized 属性的目的就在于此。

 

注意:

 

      对于类的属性值,我们上面只考虑了primitive type和function的类型情况,如果属性值是对象,比如Array,那情况就又复杂了,思考下面的一段代码。

  1. function Car(){
  2. }
  3.    
  4. Car.prototype.drivers = ["Kitten", "Bear"];
  5.   
  6. var car1 = new Car();
  7. var car2 = new Car();
  8.    
  9. car1.drivers.push("Point");
  10.    
  11. document.write(car1.drivers);  //prints Kitten,Bear,Point
  12. document.write(car2.drivers);  //prints Kitten,Bear,Point
      可以看到,我们只是想对car1的drivers做改变,但是car2的居然也改了,这显然是我们不希望出现的结果。为什么会这样呢?因为Car.prototype.drivers只是引用而已。在使用new关键字创建类实例时,附加给类实例的属性的属性值也是跟它指向相同值的引用。如果它所指向的数据是primitive或function自然没有问题,因为没有什么操作可以直接修改这些值,我们只能修改引用本身,让它们指向新的值,但这种改变根本不会影响其它指向这个值的引用。但如果指向的是数组就不一样了,在任何一个指向它的引用上实施.push操作,都会影响这个Array的值,并且这种改变会对所有引用都生效。因此,像Array这种复杂数据类型是不应该放在prototype里面做属性值的,这样,上述代码就应该更改为:

  1. function Car(){
  2.    this.drivers =  ["Kitten", "Bear"];
  3. }
  4.   
  5. Car.prototype
  6. var car1 = new Car();
  7. var car2 = new Car();
  8.    
  9. car1.drivers.push("Point");
  10.    
  11. document.write(car1.drivers);  //prints Kitten,Bear,Point
  12. document.write(car2.drivers);  //prints Kitten,Bear
 
下面我们来创建一个完成的类:

  1. function Car(){
  2.    this.drivers =  ["Kitten", "Bear"];
  3.    if (typeof Car._initialized == "undefined") 
  4.       Car.prototype.getDiversNumber = function(){
  5.         alert(this.drivers.length)
  6.       };
  7.       Car._initialized = true;
  8.    }
  9. }

  10.   
  11. var car1 = new Car();
  12. var car2 = new Car();
  13.    
  14. car1.drivers.push("Point");
  15.    
  16. document.write(car1.drivers);  //prints Kitten,Bear,Point
  17. document.write(car2.drivers);  //prints Kitten,Bear
  18. car1.getDiversNumber(); //3
  19. car2.getDiversNumber(); //2

      总结出来一句话就是:用构造函数方式定义对象的所有非函数属性,用原型方式定义对象的函数属性。

      至此,我们应该说已经比较透彻的了解了JavaScript创建类的方法。目前,“构造函数+原型”方式使用的比较广泛,“动态原型法”也很流行。我个人比较喜欢后者。

 

 


 

 

 

 

 

 

 

 

 

分享到:
评论
1 楼 gavinsun2008 2010-12-21  
不错,几篇精炼都挺深入。

相关推荐

    JavaScript精炼之构造函数 Constructor及Constructor属性详解

    对象的constructor属性用于返回创建该对象的函数,也就是我们常说的构造函数,除了创建对象,构造函数(constructor) 还做了另一件有用的事情—自动为创建的新对象设置了原型对象(prototype object)

    js构造函数constructor和原型prototype原理与用法实例分析

    主要介绍了js构造函数constructor和原型prototype原理与用法,结合实例形式分析js构造函数constructor和原型prototype基本原理、功能、使用方法及操作注意事项,需要的朋友可以参考下

    js核心基础之构造函数constructor用法实例分析

    通过以上讨论,我们可以看到构造函数在JavaScript中扮演的重要角色,以及如何通过合理使用构造函数和原型模式来优化代码结构和性能。正确理解和运用这些概念,对于编写高效的JavaScript代码至关重要。

    在派生类的构造函数中调用基类的构造函数

    这就涉及到了在派生类的构造函数中调用基类构造函数的知识点。 首先,每个类都有一个构造函数,它在对象创建时自动执行,用于初始化类的数据成员。当派生类创建时,它的构造函数会先于派生类的任何其他操作调用基类...

    Javascript的构造函数和constructor属性

    真正的原因是:一个对象的constructor是它的构造函数的prototype.constructor,而每一个函数都有一个prototype,默认情况下,这个prototype有一个constructor属性,指向的是它自己。 我觉得Javascript的设计本意是让...

    java反射之Constructor

    下面将详细讲解 Constructor 类的概述、获取构造函数的方式、构造类的实例概述等。 Constructor 概述 ------------- Constructor 类提供了关于类的单个构造方法的信息,包括构造函数的名称、参数类型、访问权限等...

    Javascript 设计模式之构造函数模式.zip

    总结,JavaScript的构造函数模式是创建对象的主要方式之一,它结合原型和原型链提供了面向对象编程的基础。理解并掌握这一模式有助于编写可维护、可扩展的代码。在实际开发中,构造函数模式常常与其他设计模式(如...

    js定义类 对象 构造函数,类的继承

    在JavaScript中,类(Class)、对象(Object)和构造函数(Constructor)是面向对象编程的基础。这篇文章将深入探讨这三个概念,以及如何实现类的继承。 首先,让我们理解什么是JavaScript中的对象。在JavaScript中...

    没有可用的复制构造函数或复制构造函数声明

    在C++编程中,"没有可用的复制构造函数或复制构造函数声明"是一个常见的错误,通常出现在尝试复制一个对象,而该对象的类没有定义复制构造函数时。在这个特定的情境中,问题出在一个名为`CArray, int>`的自定义数组...

    setter-on-constructor-prototype.rar_The Next

    在JavaScript编程中,"setter-on-constructor-prototype.rar_The Next"这个主题涉及到对象属性的访问和修改,尤其是关于构造函数原型链上的setter方法。在描述中提到的"Get the name of the next property of Result...

    【技术分享】从浅入深 Javascript 原型链与原型链污染 .pdf

    首先,JavaScript 中的对象继承并不像传统面向对象语言那样基于类,而是通过原型链机制。每个对象都有一个内部属性`[[Prototype]]`,在开发环境中通常通过`__proto__`或`constructor.prototype`来访问。当试图访问...

    prototypal-oo-js-object-oriented-constructor-functions-lab-onlin

    oo-js-object-oriented-constructor-functions-lab-onlin”表明这是一个关于JavaScript中原型式面向对象编程(Prototype-based Object-Oriented Programming)的实验或练习,特别是涉及构造函数(Constructor ...

    构造函数的继承问题 笔记

    例如,在以下代码中,因为`Base`类中没有定义任何构造函数(默认构造函数被注释掉),所以`Derived`类中的构造函数尝试调用不存在的`Base`构造函数时会导致编译错误。 ```java class Base { //base() { // 被注释掉...

    JavaScript中构造函数与原型链之间的关系详解

    - `demo.constructor.prototype`实际上也是`Demo.prototype`,因为`constructor`属性的值是构造函数,而构造函数的`prototype`属性指向实例继承的原型。 引用图例中的关系可以这样理解: - `Demo.prototype`是一个...

    通讯录 c++ 构造函数 类

    在C++编程中,"通讯录 c++ 构造函数 类"这一主题涉及到对象导向编程中的核心概念,如类的设计、构造函数的使用以及数据成员和成员函数的管理。下面将详细解释这些知识点: 1. **类(Class)**: 类是C++中的一个...

    深入理解javascript构造函数和原型对象

    在JavaScript中,每个对象都有一个原型对象(prototype),原型对象本身也是一个对象,并且有一个特殊的属性constructor,这个属性指向创建当前对象的构造函数。通过原型对象,可以实现对象之间的属性和方法共享,...

    重写重载构造函数

    结构类型的构造函数与类的构造函数类似,但是structs不能包含显式默认构造函数,因为编译器将自动提供一个构造函数。此构造函数将结构中的每个字段初始化为默认值表中显示的默认值。 类和structs都可以定义具有参数...

Global site tag (gtag.js) - Google Analytics