你的位置:首页 > Java教程

[Java教程]javascript之面向对象程序设计(对象和继承)


总结的文章略长,甚点。

知识点预热

  • 引用类型:引用类型的值(对象)是引用类型的一个实例。在ECMAScript中,引用类型是一种数据结构,用于将数据和功能组织在一起。在其他面向对象语言中被称为类,虽然ECMAScript从技术上讲也是一门面向对象语言,但它不具备传统面向对象语言所支持的类和接口等基本结构而是通过别的形式实现类模板和继承。引用类型描述的是一类对象所具有的属性和方法。
  • 对象:对象是某个特定引用的实例,新对象是使用 new 操作符后跟一个构造函数来创建的。实例对象其实就是一组特定数据和具体功能的集合。
  • 构造函数:本身就是一个函数,只不过该函数是出于创建新对象的目的而定义的。通常用来为对象定义默认的属性和方法。
  • 举例: var person=new Object(); 保存对象(新实例)的变量 person 里的内容为这个对象(新实例)在内存堆中的地址。我们知道c语言是可以直接用代码访问变量的物理内存,所以我就想javascript能不能也能直接访问内存地址,谷歌关于javascript访问内存地址的有效信息也寥寥,得知javascript这种高级语言是并不能直接通过代码访问物理内存,需要进行脚本扩展接口等等。参考(javascript能不能访问物理内存?)。待以后学习深度加强专门研究一下。

 

Object类型

在ECMAScript中(就像在Java中的 java.lang.Object 对象一样), Object 类型是所有它的实例的基础, Object 类型所具有的任何属性和方法也同样可被更具体的对象所用。

JavaScript主要是通过原型链实现了面向对象中的实现继承(区分接口继承和实现继承),所以每当构造一个实例对象时便继承了 Object.prototype 上的方法,但这种原型链式的继承并不是复制方法的副本,而是引用(指向)式的继承。

  •  constructor: 保存着用于创建当前对象的函数,这里构造函数就是Object(); person.constructor;// function Object() { [native code] }
  •  hasOwnProperty(propertyName): 用于检查给定的属性在当前对象的实例中(而不是在实例的原型中)是否存在,返回布尔值。属性名以字符串形式给定。
    person.hasOwnProperty('constructor');// falseObject.prototype.hasOwnProperty('constructor');// true

  •  isPrototypeOf(object): 用于检查调用该方法的对象是否是传入对象的原型。Object.prototype.isPrototypeOf(person);// true
  •  propertyIsEnumerbale(propertyName): 检查给定的属性是否能够使用 for in 来枚举,当然能枚举的前提是该属性的特性被设置为是可枚举的。
  •  toLocaleString(): 返回对象的字符串表示,该字符串与执行环境的地区对应。
  •  toString(): 返回对象的字符串表示。
    //Number,String,Boolean类型返回新的字符串,其实是在包装类的实例上调用toString或toLocaleStringvar number=10;number.toString();// "10"var str='xx';var strnew=str.toString();// "xx"str==strnew;// true 两个string基本类型的字符串比较内容而已所以为truestr+='add';// "xxadd" 修改str指向的内容,进一步确定toString()返回的是副本并不是str的引用strnew;// "xx"var bol=true;bol.toString();// "true"//引用类型构造函数及其实例对象调用toString/toLocaleString返回Object.toString();// "function Object() { [native code] }"var person=new Object();person.toString();// "[object Object]"Array.toString();// "function Array() { [native code] }"var a=new Array();a.toString();// "" 即调用数组每一项的toString()方法,然后拼接成字符串Functiom.toString();// "function Function() { [native code] }"new Function('console.log(1)').toString();//"function anonymous() {console.log(1)}"Boolean.toString();// "function Boolean() { [native code] }"new Boolean(true).toString();// "true"String.toString();// "function String() { [native code] }"new String('xx').toString();// "xx"Number.toString();// "function Number() { [native code] }"new Number(10).toString();// "10"

  •  valueOf(): 对于字符串,数值或布尔值这三个基本类型返回的是副本,对于包装类的实例对象返回该实例的基本类型的表示,对于除过包装类实例的引用类型则返回的是自身的引用。
    //基本类型Number,String,Booleanvar num=1;var numnew=num.valueOf();num+=2;// 3numnew;// 1var str='xx';str.valueOf(); //"xx"var bol=true;bol.valueOf();// true;//包装类的实例new Number().valueOf();// 0new String().valueOf();// ""new Boolean().valueOf();// false//其他引用类型返回自身的引用Object.valueOf()==Object;// truevar o=new Object();o.valueOf()==o;// true

创建Object实例:不论用哪种方式效果是一样的

       

  1. new 构造函数
    var o=new Object();o.name="xx";o.say=function(){ console.log('hi')}//如果不传参,可以省略圆括号,但不推荐var o=new Object;

  2. 对象字面量
  3. var o={ name:'xx', say:function(){  console.log('hi'); }}

    ECMAScript中表达式上下文的定义:该上下文期待的一个值(表达式)。在这个例子中,左边的花括号 ({) 表示对象字面量的开始 ,因为它出现在表达式上下文中。赋值操作符表示后面是一个值,所以左花括号在这里表示一个表达式的开始。
    ECMAScript中语句上下文的定义:同样的花括号,例如跟在if条件语句后面,则表示一个语句块的开始。
    在通过对象字面量定义的对象时,实际上不会调用 Object 构造函数。此话出自JavaScript高级程序设计第三版,不明白不调用 Object 构造函数那是通过什么幕后形式创建的对象,js引擎?
    属性名可以使用字符串定义,也可以像本代码一样简写,JavasScript会自动转化为字符串。

    for(var i in o){ console.log(typeof i);//string}

    对象字面量也是向函数传递大量可选的参数的首选方式。

    function displayInfo(args){   var output="";   for(var i in args){    if(args.hasOwnProperty(i)){      if(typeof args[i]=='function'){         output+=args[i]();       }      else{        output+=args[i]+' ';      }         }   }  console.log(output); };displayInfo({  name:'xx',  say:function(){   return 'hi';  }});

    方括号语法的主要优点是通过可以通过变量来访问属性,如果属性名中包含会导致语法错误的字符,或者属性名使用的是关键字或保留字,也可以使用方括号表示法访问。将要访问的属性以字符串形式放在方括号中。

     

ECMA-262把对象定义为

无序属性的集合,其属性可以包含基本值,对象,函数。相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值,可以把ECMAScript中的对象想象成散列表,无非就是一组名值对,其中的值可以是数据或函数。每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型也可以是自定义类型。

创建对象:虽然 Object 构造函数或对象字面量都可以用来创建单个对象,但这些方法明显有缺点:使用同一个接口创建很多对象,会产生大量重复的代码。所以产生如下七种模式。

1.工厂模式:抽象了创建具体对象的过程,考虑到在ECMAScript中无法创建类,所以就用函数封装代码实现特定功能。实质就是用函数封装以特定接口来创建对象的细节(细节是指构造对象实例的方式)。实际想想在软件工程领域中我们经常这样做,将一个功能封装起来只给外界提供接口。

function createPerson(name,age,job){ var o=new Object(); o.name=name; o.age=age; o.job=job; o.say=function(){  console.log(this.name);  };  return o;}var person1=createPerson('xx',20,'student');var person2=createPerson('mm',22,'student');person1==person2;// false

有点感觉了,像java/C++中类里面的构造函数有没有,可以返回同功能的不同的多个实例。

缺点:没有解决对象识别问题(即怎样知道一个对象的类型),虽然你可以用代码试探实例是那种类型(比如可以用 person1.__proto__ 的方法),但却没法直观地知道 person1 和 person2 到底是哪种类型的实例。

2.构造函数模式:ECMAScript中的原生构造函数可以用来创建特定类型的对象,此外也可以创建自定义构造函数,从而定义自定义对象类型的属性和方法。

  • function Person(name,age,job){ this.name=name; this.age=age; this.job=job; this.say=function(){   console.log(this.name); }; }var p1=new Person('xx',20,'student');var p2=new Person('mm',22,'student');p1==p2;// falsep1.constructor==Person;//true

     Person 中的代码与工厂模式中 createPerson 中的代码相比:
    1.并没有显式创建对象
    2.直接将属性和方法赋值给了 this 对象
    3.没有 return 语句
    小tip:构造函数命名规范,始终以一个大写字母开头,而非构造函数则以一个小写字母开头。这个做法借鉴自其他OO语言,主要为了区别于ECMAScript中其他函数。
    通过 new 操作符的方式调用构造函数实际上会经历以下四个步骤:JavaScript面向对象之我见 中提到函数的 prototype 属性的值被作为原型对象来可克隆出新对象(实际上是后面说的寄生式继承),可以把 new 运算符想象成一个方法,这个方法的功能
  • 创建一个新对象
  • 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
  • 执行构造函数中的代码(为这个新对象添加属性)
  • 返回新对象

 p1 和 p2 这两个对象都有一个 constructor (构造器)属性(javascript高程上这么说其实并不准确, constructor 其实并不是 p1 和 p2 自身有的属性而是通过原型链继承来的属性), constructor 指向 Person 。

这里所创建的 p1 和 p2 既是 Object 的实例,又是 Person 的实例,原因是原型链上继承关系,后面有说。 p1 instanceof Object;// true

p1 instanceof Person;// true 

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,明确的知道实例的类型这正是构造函数模式胜于工厂模式的地方。

在另一个对象的作用域中调用:在某个特殊对象的作用域中调用 Person 函数。

var o=new Object();Person.call(o,'xx',20,'student');o.say();// xx

缺点: say 这个方法要在每个实例上都创建一遍, p1.say==p2.say;// false p1和p2上的say方法虽然内容一样但却是完全不同的两个 Function 类型实例, this.say=function(){ console.log(this.name);};  相当于 this.say=new Function("console.log(this.name)"); 以这种方式创建函数,会导致不同的作用域链和标识符解析(变量,函数,属性,参数的名字),但创建 Function 新实例的机制仍然是相同的。

解决方案:鉴于 say 函数对于 p1 和 p2 完成的功能一样,那么可以不用在执行代码前就把该函数绑定到特定对象上面,通过把完成特定功能的函数单独拿出来,而让实例对象的 "say" 属性指内存堆里同一个函数来解决问题。这样 p1 和 p2 就共享了在全局作用域中定义的同一个函数。

function Person(name,age,job){ this.name=name; this.age=age; this.job=job; this.say=say;};function say(){ console.log(this.name); }var p1=new Person('xx',20,'student');var p2=new Person('mm',20,'student');

缺点:

  1. 全局作用域中调用的函数只能被某个对象调用才会体现出该函数的功能,这让全局作用域有点名不副实。
  2. 如果对象需要定义很多个方法,那么就需要定义很多全局函数,于是我们这个自定义的引用类型就没有封装性可言了。

3.原型模式:JavaScript中每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象。 prototype 是这个属性的名字,这个指针的内容就是该函数原型属性对象在堆内存中的地址。这个原型对象的作用就是包含可以由特定类型的所有实例共享的属性和方法。也可以这么理解, prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型好处就是可以让所有对象实例共享它所包含的属性和方法。即不必在构造函数中定义对象实例的信息,而是可以将这些信息添加到原型对象中。

function Person(){}Person.prototype.name='xx';Person.prototype.age=29;Person.prototype.job="student";Person.prototype.say=function(){  console.log(this.name);};var p1=new Person();p1.name;// 'xx'var p2=new Person();p1==p2;// falsep1.say==p2.say;// true

  • 理解原型对象:每当代码读取某个对象的某个属性时,都会执行一次搜索具有给定名字的属性。搜索先从本对象实例开始,如果在对象实例中找到具有给定名字的属性则返回该属性的值。如果没找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。这正是多个对象实例共享原型所保存的属性和方法的基本原理。使用delete操作符可以完全删除实例属性从而恢复原型链继承,前提是属性的 configurable 特性为 true 。
  • 原型与in操作符:两种方法使用 in 操作符(单独使用, for-in 循环)
    单独使用: in 操作符会在通过对象能够访问给定属性时返回 true ,无论该属性存在于实例还是原型中。 'job' in p1;// true
    for in :返回的是所有能够通过对象访问的,可枚举的属性,其中既包括存在实例中的属性,也包括原型中的属性。 Object.prototype 上的属性都是不可枚举的,因为它们的 enumerable 特性默认为 false 。 for(i in Object.prototype){ console.log(i) };// undefined 。但我们可以为实例定义同名的方法覆盖原型对象上方法的不可枚举性。因为通过字面量对象上添加属性和构造函数创建的对象上的属性们默认都是可枚举的,注意通过 Object.defineProperty 定义对象属性是不可枚举的。
    //字面量创建对象属性var p={ name:'xx', say:function(){}}Object.getOwnPropertyDescriptor(p,'name');// Object {value: "xx", writable: true, enumerable: true, configurable: true}//直接在对象上添加属性var o=new Object();o.name='xx';// "xx"Object.getOwnPropertyDescriptor(o,'name');// Object {value: "xx", writable: true, enumerable: true, configurable: true}//Object.definedProperty定义属性var p={};Object.defineProperty(p,'name',{ value:'xx'});// Object {name: "xx"} 一旦定义某一属性后就不能通过这种方式再次定义该属性,因为此时writable默认为false,除非当时显式声明为trueObject.getOwnPropertyDescriptor(p,name);// Object {value: "xx", writable: false, enumerable: false, configurable: false}

    var o={ toString:function(){  console.log('我是实例上的toString'); }};for(var i in o){ console.log(i);};// toString

    注:上面的覆写 toString 代码会在IE早期版本中出现bug,不会打印出 toString ,因为IE认为原型的 toString 方法被打上了值为 false 的 [[Enumerable]] 标记,因此会跳过该属性。
    解决方案:
    1.改变原型上方法的可枚举性

    Object.defineProperty(Object.prototype,"valueOf",{enumerable:true });Object.defineProperty(o,"valueOf",{enumerable:true});Object.getOwnPropertyDescriptor(Object.prototype,'valueOf');// Object {writable: true, enumerable: true, configurable: true}for(var i in o){ console.log(o[i]);}

    2.如果你想得到所有实例属性而不论它们是否都可枚举,使用 Object.getOwnPropertyNames() ,返回类型为 "[object Object]" 类数组:

  1. Object.getOwnPropertyNames(Person.prototype);// ["constructor", "name", "age", "job", "say"]Object.getOwnPropertyNames(Object.prototype);/* ["constructor", "toString", "toLocaleString", "valueOf", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "__defineGetter__", "__lookupGetter__", "__defineSetter__", "__lookupSetter__", "__proto__"] */

    ECMAScript5中 Object.keys() 方法可以取得对象上所有可枚举的实例属性,这样就不用 for-in 和 hasOwnProperty 筛选判断了:参数为对象,返回一个包含所有可枚举属性的字符串数组

    Object.keys(Person.prototype);// ["name", "age", "job", "say"]p1.sex='女';Object.keys(p1);// ["sex"]

  • 更简单的原型语法:为了更好的封装原型的功能,将原型上定义的属性和方法用对象字面量重写整个原型

  1. Person.prototype={  name:'xx',  age:22,  say:function(){console.log(this.name)}}

    但这样做法会使原型上的 constructor 属性不见了, Object.getOwnPropertyNames(Person.prototype);// ["name", "age", "say"] ,为什么呢?因为这种方式是在重定义原型属性的内容(即让 prototype 指向了别处对象),而之前的方式是在默认的原型属性上添加新属性而已。我们知道,每创建一个函数,就会同时创建它的 prototype 对象,这个对象也就会自动获得 constructor 属性,但现在我们让 prototype 指向了别处新对象,原来带 constructor 的那个对象在内存中就没人引用了,如果也没有实例对象的引用情况下就等待垃圾处理机制的回收。有意思的是虽说 Person.prototype 没有 construcor 属性了,但是再次访问 Person.prototype.constructor;// Object() { [native code] } 什么鬼?不是说没有这个属性不能访问到了吗?原来现在的 constructor 其实是在自己对象上搜索不到的属性,便顺着原型链继承自 Object.prototype.constructor 来的。此时已经不能用 constructor 来判断实例对象的类型了。

    Person.prototype.hasOwnProperty('constructor');// falseObject.prototype.hasOwnProperty('constructor');// true

    如果需要 constructor ,可以特意将 constructor 设回适当的值。

    Person.prototype={  name:'xx',  age:22,  say:function(){console.log(this.name);},  constructor:Person }

    但是这样会将它的 [[Enumerable]] 特性被设置为 true 。当然再加一步,通过 Object.defineProperty() 将特性重新赋为 false 。 

  • 原型的动态性:实例与原型间的松散连接关系可以让实例对象访问后来在原型上添加的方法。因为实例与原型之间的连接只不过是一个指针,而非一个副本。
    注意:如果修改了整个原型对象,那么就要慎重访问了,顺序尤为重要。我们知道调用构造函数时会为实例添加一个指向最初原型的 [[prototype]] 的指针,而现在把构造函数的原型修改为指向别处,那么这时候实例创建的顺序对访问结果有很大影响。出现错误的原因就是 p.__proto__ 和 Person.prototype 指向的不是一个对象。
  1. 原生对象的原型可以取得所有默认方法的引用,而且也可以定义新方法。但是不推荐在原型上添加方法可能会导致命名冲突。
  2. 原型对象的问题:
    它省略了为构造函数传递参数这一部分导致所有实例在默认情况下都取得相同属性值。
    原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。但对于包含引用类型值的属性问题就比较突出了。
    function Person(){}Person.prototype={ constructor:Person, name:'xx', hobby:['a','b']};var p1=new Person();var p2=new Person();p1.hobby.push('c');p1.hobby;// ['a','b','c']p2.hobby;// ['a','b','c']p1.hobby===p2.hobby

4.组合使用构造函数模式和原型模式:构造函数模式用于定义实例上的属性,原型模式定义方法和共享的属性。这样每个实例都会有自己的一份实例属性的副本,但同时又共享着方法的引用,节省了内存。

function Person(name,age){  this.name=name;  this.age=age;  this.hobby=['a','b'];}Person.prototype={ constructor:Person, say:function(){  console.log(this.name); }}var p1=new Person('xx',20);var p2=new Person('mm',20);p1.hobby.push('c');p2.hobby;// ['a','b'];

 5.动态原型模式(在构造函数中初始化原型):动态原型是把所有信息都封装在了构造函数中,这样构造函数和原型就不独立开了,符合了OO语言中功能在一块的习惯。通过在构造函数中初始化原型(仅在必要的情况下)又保持了同时使用构造函数和原型的优点。换句话说可以通过检查某个应该存在的方法是否有效来决定是否需要初始化原型。

function Person(name,age){  this.name=name;  this.age=age;  if(typeof this.say!='function'){ //或用instanceof判断   Person.prototype.say=function(){     console.log(this.name);   }   }}var f=new Person("xx",20);f.say();// 'xx'

这段代码只会初次调用构造函数时才会执行,此后原型已经完成初始化,需要再做什么改变了。 if 语句检查的可以是初始化之后应该存在的任何属性或方法。不必用一大堆 if 语句检查每个属性和每个方法,只要检查其中一个就好。

缺点:不能用对象字面量的形式重写原型,因为实例先于修改原型创建,执行 new 时,调用构造函数,为实例添加一个指向默认原型的 [[prototype]] ,然后再初始化,所以是在修改原型之前。

6.寄生构造函数模式:工厂模式和构造函数模式的结合。用一个函数封装创建创建对象的代码然后返回新创建的对象。除了使用 new 操作符并把使用的包装函数叫构造函数外,这个模式和工厂模式其实一样。这里涉及到JavaScript中构造函数的返回值

  • 当构造函数没有返回值时,则按照其他语言一样返回实例化对象
    当构造函数有返回值时,若返回值为非引用类型如(String,Number,Boolean,undefined,null)则与无返回值相同,返回实例对象。若返回值为引用类型,则实际返回值为这个引用类型。
    function Person(name,age){ var o=new Object(); o.name=name; o.age=age; o.say=function(){  console.log(this.name); } return o;}var p=new Person('xx',20);p.say();

    应用:假设想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用这个模式

    function SpecialArray(){  var values=new Array();  values.push.apply(values,arguments);//初始化数组  values.toPipedString=function(){    return values.join('|');  };  return values;}var hobbits=new SpecialArray('a','b','c');hobbits.toPipedString();// "a|b|c"

    关于寄生构造函数模式,说明一点,返回的对象与构造函数或者与构造函数的原型属性之间没有关系。即构造函数返回的对象与在构造函数外部创建的对象没什么区别,为此不能依赖instanceof操作符来确定对象类型。此种模式不推荐。

7.稳妥构造函数模式:
稳妥对象:没有公共属性,而且其方法也不引用 this 的对象。稳妥对象最适合在一些安全的环境中(这些环境会禁止使用 this 和 new )或者防止数据被其他应用程序改动时使用。
稳妥构造函数遵循与寄生构造函数类似的模式,有两点不同:一是创建对象的实例方法不引用 this ,二是不使用 new 操作符调用构造函数。

function Person(name,age){  var o=new Object();  //在这里定义私有变量和函数  o.say=function(){   console.log(name);  }  return o;}var p=Person('xx',20);p.say();// 'xx'

注意这种模式创建的对象中,除了使用say方法外没有别的方法可以访问其数据成员。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的方法访问传入到构造函数中的原始数据。使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此也不能用 instanceof 判断类型。 

 

 

继承

许多OO语言都支持两种继承方式,接口继承和实现继承。接口继承只继承方法名,实现继承继承实际的方法。由于函数没有签名,在ECMAMScript中无法实现接口继承。ECMAScript只支持实现继承,而且其实现继承主要是依靠原型链来实现的。

1.原型链:作为实现继承的主要方法。基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。假如让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。

function SuperType(){ this.property=true;}SuperType.prototype.getSuperValue=function(){ return this.property;}function SuberType(){ this.subproperty=false;}SuberType.prototype=new SuperType();SuberType.prototype.getSubValue=function(){ return this.subproperty;}var instance=new SuberType();instance.getSuperValue();// true 

调用 insatnce.getSuperValue() 会经历:搜索实例;搜索 SubType.prototype ;搜索 SuperType.prototype ,最后一步才会找到该方法。

  1. 别忘记默认的原型:所有引用类型默认都继承了Object。所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是所有自定义类型都会继承toString和valueOf等默认方法的根本原因。
  2. 确定原型和实例的关系:
    instanceof操作符:测试实例与原型链中出现过的构造函数,结果返回true。
    instance instanceof Object;// trueinstance instanceof SuperType;// trueinstance instanceof SuberType;// true

    isPrototypeOf()方法:只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPrototypeOf()也会返回true。
    Object.prototype.isPrototypeOf(instance);// trueSuperType.prototype.isPrototypeOf(instance);// trueSuberType.prototype.isPrototypeOf(instance);// true

  3. 谨慎地定义方法:谨慎使用对象字面量创建原型方法,这样做会重写原型链。
  4. 原型链的问题:最主要的问题是包含引用类型值的原型。在创建子类型的实例时,不能向超类型的构造函数传递参数(但是Java的 super() 函数就可以实现啊)。也就是说没有办法在不影响所有对象实例的情况下给超类型的构造函数传递参数。所以实践中很少会单独使用原型链。

2.借用构造函数:解决了原型中包含引用类型值所带来的问题。即在子类型的构造函数中调用超类型的构造函数(看来也是借鉴了Java中的 super() ),至于是怎么实现的?JavaScript中函数只不过是在特定环境中执行代码的对象,因此通过 apply 和 call 方法可以在将来新创建的对象上执行构造函数。

//父类function SuperType(){  this.colors=["a","b"];}//子类function SuberType(){  SuperType.call(this);}var instance1=new SuberType();instance1.colors.push('c');// 3 colors值为["a","b","c"];var instance2=new SuberType();instance2.colors;// ["a","b"];

通过使用 call 方法或 apply 方法,我们实际是在(未来将要)新创建的 SuberType 实例的环境下调用 SuperType 构造函数,毕竟每当 new 一个实例的时候是先将构造函数的作用域赋给新对象( this 指向确定)。这样一来,就会在新的 SuberType 对象上执行 SuperType 函数中定义的所有对象初始化代码。这样, SuberType 的每个实例就都会有自己的 colors 属性的副本了。
优点:可以在子类的构造函数中向超类构造函数传递参数(这点Java的 super() 传递参数也可做到)

function SuperType(name){ this.name=name;}function SuberType(age){ //继承了SuperType还传递了参数 SuperType.call(this,"xx"); //实例的其他属性 this.age=age;}var instance=new SuberType(20);instance.name;// "xx"instance.age;// 20

为了确保 SuperType 构造函数不会重写子类的属性,所以在子类中定义的属性写在调用的后面。
缺点:如果仅仅使用构造函数来完成继承,那么也无法避免构造函数模式中存在的问题,即方法都在构造函数中定义,就没有函数的复用了。在超类原型中定义的方法,对子类型而言也是不可见的,结果所有类型就只能使用构造函数模式。考虑到这些,借用构造函数的技术也很少单独使用。
3.组合继承:原型链和构造函数的技术结合起来,使用原型链实现实现对原型属性和方法的继承,借用构造函数实现对实例属性的继承。

//父类function SuperType(name){ this.name=name; this.colors=["a","b"];} SuperType.prototype.sayName=function(){ console.log(this.name);}//子类function SuberType(name,age){ SuperType.call(this,name); this.age=age;}SuberType.prototype=new SuperType();

SuberType.prototype.constructor=SuberType;SuberType.prototype.sayAge=function(){ console.log(this.age);}

var a1=new SuberType('xx',20);

a1.colors.push('c');a1.colors;// ["a","b","c"]a1.sayAge();// 20;a1.sayName();// xx

这种方式 instanceof 和 isPrototypeOf 同样可用。但注意到 SuberType.prototype 有个 name 和 colors 的无用属性。
缺点:会调用两次超类的构造函数,一次是在创建子类原型的时候,另一次是在子类构造函数内部的 call 或 apply 。子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。解决办法是寄生组合继承。
4.原型式继承:这种方法并没有使用严格意义上的构造函数,而是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

function object(o){ function F(){} F.prototype=o; return new F(); }

先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,返回了临时类型的一个新实例。

var person={ name:"xx", friends:["aa","bb","cc"]};var p1=object(person);p1.name;// "xx" 原型继承p1.name="xixi";p1.friends.push("dd");person.friends;// ["aa", "bb", "cc", "dd"] var p2=object(person);// 再次调用object(),虽然是重新执行了F.prototype但是o参数仍指向原来的personp2.name="xuxu";p2.friends.push("ee");person.friends;// ["aa", "bb", "cc", "dd", "ee"]

ECMA5新增的 Object.create() 规范化了原型式继承,两个参数,一个用作新对象原型的对象,(可选的)为一个新对象定义额外属性的对象。在传入一个参数情况下, Object.create() 和 object() 没什么区别。

var person={ name:"xx", friends:["aa","bb","cc"]};var p1=Object.create(person);p1.name="xixi";p1.friends.push("dd");var p2=Object.create(person);p2.name="xuxu";p2.friends.push("ee");person.friends;// ["aa", "bb", "cc", "dd", "ee"]

 Object.create() 方法的第二个参数与 Object.defineProperties() 方法的第二个参数格式相同,每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。

var person={ name:'xx', friends:["aa","bb","cc"]};var p1=Object.create(person,{ name:{   value:"xixi" }});p1.name;// "xixi"person.name;// "xx"

在没有必要创建构造函数,而只是想让一个对象与另一个对象保持类似情况下,原型式继承是完全可以的。不过缺点还是在的,比如包含引用类型值得属性始终都是共享的。
5.寄生式继承:是一种与原型式继承紧密相关的思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式增强对象,最后再像是它做了所有工作一样返回对象。

function createAnother(original){  var clone=object(original); //调用函数创建一个新对象  clone.say=function(){ //增强这个对象   console.log("hi");  }  return clone; }

var person={ name:'xx', friends:["aa","bb","cc"]}var p1=createAnother(person);p1.name;// "xx"p1.friends;// ["aa", "bb", "cc"]p1.say();// hi

在主要考虑对象而不是自定义类型的构造函数情况下,寄生式继承也是一种有用方法,而且 object() 函数也不是必须的,任何能够返回新对象的函数都适用此模式。
缺点:由于是增强对象给对象添加函数,所以不能函数复用。这点与构造函数模式相似。

6.寄生组合式继承:通过构造函数继承属性,原型链的混成继承方法。说白了就是不必为了指定子类型的原型而调用超类型的构造函数而造成子类型原型上出现一些用不到的属性,我们所要的无非就是超类型原型的一个副本实现原型链之间的继承关系而已。那么为何不考虑结合寄生式继承因为寄生式继承可以为一个对象指定原型啊。这样使用寄生式继承来继承超类型的原型,然后再将返回的结果指定给子类原型。

function inheritPrototype(suberType,superType){ var prototype=object(superType.prototype);// 创建对象 prototype.constructor=suberType;// 增强对象 suberType.prototype=prototype;// 指定对象}

function SuperType(name){ this.name=name; this.colors=["aa","bb","cc"];} SuperType.prototype.say=function(){ console.log(this.name); }function SuberType(age,name){ SuperType.call(this,name); this.age=age;}inheritPrototype(SuberType,SuperType);SuberType.prototype.say=function(){ console.log(this.age);}

只调用一次父类构造函数不仅提高了效率,这样做还能正常地使用 instanceof 和 isPrototypeOf 。寄生组合式继承是引用类型最理想的继承范式。YUI的 YAHOO.lang.extend 就用到了寄生组合继承(https://yui.github.io/yui2/docs/yui_2.3.0/docs/Lang.js.html)

 

参考 :《JavaScript高级程序设计》