Javascript面向对象全面剖析 —创建对象
先介绍目前在ECMAScript中使用最广泛,认同度最高的默认模式。
1.组合使用构造函数及原型
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.friends = ["Shelby","Court"]; } Person.prototype = { constructor : Person, sayName : function(){ alert(this.name); } } var person1 = new Person('Nocholas',29,'Software Engineer'); alert(person1.friends); //"Shelby,Count,Van" person1.sayName(); //"Nocholas"
其中实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor 和方法 sayName() 则是在原型中定义的。
constructor属性始终指向创建当前对象的构造函数,不用刻意牢记。constructor属性
1.在构造函数中用this.创建属性 而不是在原型上。
这么做的本质是因为:对于一个属性而言,在类的对象中的值要求是独立的,对象独自保存自己属性的值,修改的同时不能影响到其他实例化对象。在构造函数中使用this创建的属性,在每个实例上重新创建一遍,保证了此特性。
原型创建的内容是这个类所有对象所共享的,如果使用原型对象来创建对象属性,那么任何一个类的对象修改了自己的某个属性(即原型属性),其他对象的相同属性也会被修改。
所以一般情况我们在函数内部创建属性。
2.用原型来保存方法 而不在构造函数中(之后我们会讲到什么情况下我们才需要在构造函数中定义方法)
方法对所有类对象来说都应该是一样的,没有必要每个对象都保存一个方法,只要由类的原型保存一份,每个对象需要使用方法的时候就调用原型对象中保存的方法。节省了资源。
传统的方式在构造函数定义方法,每个类的被实例化的时候,都会重复创建这个方法,这样会耗
费很多资源。
下面我们来系统地认识构造函数模式和原型模式
2.构造函数模式
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName : function(){ alert(this.name); } } var person1 = new Person('Nocholas',29,'Software Engineer'); var person2 = new Person('Greg',27,'Doctor'); person1.sayName(); //"Nocholas" person1.sayName(); //"Greg"
按照惯例构造函数始终都应该以一个大写字母开头
特点:这种方法没有显式地创建对象;直接将属性和方法赋给了 this 对象;没有返回值。
要创建Person的新实例时,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下4个步骤:
(1) 创建一个新对象
(2) 将构造函数的作用域赋给对象(因此 this 就指向了这个新对象)
(3) 执行构造函数中的代码(为这个新对象添加属性)
(4) 返回新对象
我们来检测一下对象类型
alert( person1 instanceof Object); //true alert( person1 instanceof Person); //true alert( person2 instanceof Object); //true alert( person2 instanceof Person); //true
这个例子中,person1 和 person2 都是Person的实例,同时所有对象均继承自Object。
构造函数模式的缺点:
使用构造函数的主要问题就是每个方法都要在每个实例上重新创建一遍。在前面的例子中,person1 和 person2 都有一个名为 sayName 的方法,但那两个方法不是同一个 Function 的实例。 ECMAScript中的函数是对象,因此每定义一个函数就是实例化了一个对象,从逻辑角度讲,此时的构造函数也可以这样定义:
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName = new Function(){ alert(this.name); } }
从这个角度看构造函数更容易明白每个Person 实例都包含一个不同的 Function 实例的本质,如前所述这两个函数是不相等的,
alert(person1.sayName() == person2.sayName()) //false
我们没有理由对实现同一功能的方法多次创建,特别是在方法数量较多的情况,即便是可以通过下面的方法来避免多次创建:
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName = sayName; } function sayName(){ alert(this.name); }
我们创建全局函数 sayName,将构造函数内部的属性设置成等于全局的 sayName 函数;由于sayName 包含的是一个指向函数的指针,person1 和 person2 对象就共享了全局作用域中的函数,这样确实解决了两个函数共做一件事的问题,可是这样在全局作用域中的函数 sayName 只是为了Person 实例化的对象调用,让全局作用域有点名不副实。
而更让人无法接受的是要定义很多方法的时候,就要定义很多函数,于是我们自定义的类就变得丝毫没有封装性可言了。还好这些问题可以通过原型模式来解决。
3.原型模式
简单理解:我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个对象,它的用途就是可以让所有实例共享它包含的的属性和方法。 换句话说,不必在构造函数中定义对象信息,而是可以将这些信息直接添加到原型对象中,如下所示:
function Person(){} Person.prototype.name = "Nicholas"; proson.prototype.age = 29; Person.prototype.sayName = function(){ alert(this.name); } var person1 = new Person(); person1.sayName(); //"Nicholas" var person2 = new Person(); person2.sayName(); //"Nicholas" alert(person1.sayName == person2.sayName); //true
我们将sayName() 方法和所有属性直接添加到了Person 的 prototype 属性中,构造函数变成了空函数,即便如此也仍可以通过调用构造函数来创建一个新对象,而且新对象还会具有相同的实行和方法,新对象中的属性和方法是由所有实例共享的。
下面我们来理解原型模式的工作原理,有点抽象,不过却是js面向对象编程的最核心部分,理解他很重要,多看几遍就是:
理解原型(prototype)
每一个JavaScript对象(null除外)都和另一个对象相关联,“另一个”对象就是我们熟知的原型,每个对象都从原型继承属性。
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,我们可以为 prototype 添加属性和方法。默认情况下,prototype 属性都会自动获得一个 constructor 属性,constructor属性始终指向创建当前对象的构造函数,默认情况下指向函数自己,我们不用深究constructor。
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.friends = ["Shelby","Court"]; } Person.prototype.sayName =function(){ alert(this.name); } console.log(Person.prototype.constructor === Person); //true
另外每个对象都会在其内部初始化一个属性,就是__proto__,__proto__指向当前对象父对象的pertotype 当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去__proto__里找这个属性,这个__proto__又会有自己的__proto__,于是就这样一直找下去,也就是我们平时所说的原型链的概念。
function Person( name, age, job ){ this.namee = name; this.age = age; this.job = job; } Person.prototype.sayName = function(){ alert(this.name); } console.log(Person.__proto__ === Function.prototype); //true from Function; console.log("******************"); console.log(Function.prototype.__proto__ === Object.prototype); console.log(Object.prototype.__proto__ === null); //true
原型链的起点是Object.prototype Object.prototype中包含着toString()、valueOf() 等内置方法,这也是各种数据类型的同名方法,其实都是继承于此。
看下面,注意区分 prototype 和 __proto__ ,通俗的来理解:
一个普通的函数 function Person(){} 同时拥有 prototype 和 __proto__。 Person.prototype 包含着Person拥有的一切以后要传给儿子的属性和方法 ,一开始只包含一个constructor属性 可以自由增加 Person.prototype.familyName = "陈";Person.prototype.skill = “泡妞”;
Person.__proto__ 则指向 Person的老爸的原型 Function.prototype,显然默认也只包含一个constructor属性 如果曾经发生过 Function.prototype.car = "劳斯莱斯",老爸有辆劳斯莱斯的车,那么 console.log(Person.car) //劳斯莱斯Person也继承了。
实例化的对象 person1 = new Person(); 是没有prototype的 console.log(person.prototype); //undefined。其他的对象类型也一样。
var arr = new Array(); var fun = new Function(); var obj= new Object(); console.log(arr.prototype) //undefined console.log(fun .prototype) //undefined console.log(obj.prototype) //undefined
更简单的原型语法
前面的例子中没添加一个属性和方法就要敲一遍 Person.prototype。为减少不需要的输入,也从视觉上更好地封装原型的功能,常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,如下
function Person(){} Person.prototype = {
constructor : Person, name : "Nicholas", age : 29, job : "Software Engineer", sayName : function(){ alert(this.name); } }
原型对象的问题:
首先,它省略了为构造函数传递初始化参数这一环节,结果所有实力在默认情况下都取得相同的属性值,这会在某种程度上带来一些不便,但这还不是原型的最大问题,原型的最大问题是由其共享的本性所导致的。
原型中的所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说得过去(通过在实例上添加一个同名属性,可以隐藏原型中的对应属性),然而对于包含引用类型值得属性来说,问题就比较突出了。
如果对于值类型,引用类型不太清楚的同学,请参阅 Javascript传值方式
function Person(){} Psrson.prototype = { constructor : Person, name : "Nicholas", age : "29", friends : ["Shelby","Court"], sayName : function(){ alert(this.name); } } var person1 = new Person(); var Person2 = new persin(); person1.friends.push("Van"); // 向friends属性添加一个元素 alert(person1.friends); // ["Shelby","Court","Van"] alert(person2.friends); // ["Shelby","Court","Van"] alert(person1.friends === person2.friends); // true
由于Person的friends属性是一个数组,是引用类型(对象),我们修改了person1.friends 引用的数组,向数组中添加了一个字符串。由于friends数组存在于Person.prototype 而非 person1 中,所以我们的修改会影响到到所有的实例,假如我们的初衷就是这样在所有实例中共享一个数组,那么这个结果倒也可以接受,可是实例一般都是要有属于自己的全部属性的,而这个问题正是我们很少看到有人单独使用原型模式的原因所在。
我们最常见的方式,就是在开篇中介绍的组合使用构造函数模式与原型模式,构造函数用于定义实例属性,原型模式用于定义方法和共享属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外这种混成模式还支持向构造函数传递参数,可谓是集两种模式之长。
下面贴出Javascript中最常用的继承模式:
/*=========== 父类 ============*/ function SuperType(name){ this.name = name; this.colors = ["red","blue","green"]; } SuperType.prototype.sayName = function(){ alert(this.name); } /*=========== 子类 ============*/ function SubType(name, age){ this.age = age; SuperType.call(this,name); } SubType.prototype = new SuperType(); SubType.prototype.sayAge = function(){ alert(this.age); }
注:本文知识点源自《javascript高级程序设计》,想要对javascript面向对象了解更多的园友,可以自行查阅。
如果感觉本文对您有所助益,劳驾您推荐下,在此谢过。