JavaScript之对象创建与继承
对象创建
关于对象创建示例最基础的两种方式就是使用new操作符后跟Object构造函数和对象字面量表示法(即:{}),但对于大量同模式的重复对象,这两种创建方法就会显得代码冗余,因此产生了以下几种对象创建模式。
工厂模式
工厂模式是最常用的实例化对象模式,该模式相当于创建实例的new操作符,用函数来封装以特定接口创建对象的细节。示例如下:
function createStudent(name,age,gender){
var o = new Object();
o.name = name;
o.age = age;
o.gender= gender;
o.saySelf = function() {
alert("My name is:" + this.name + ". I’m " + this.age + " years old." );
};
return o;
}
var student1 = createStudent("Lili",10,"girl");
var student2 = createStudent("Sim",12,"boy");
student1.saySelf(); //"My name is:Lili. I’m 10 years old."
student2.saySelf(); //"My name is:Sim. I’m 12 years old."
alert(student1 instanceof Object); //true
alert(student1 instanceof createStudent); //false
alert(student1.saySelf == student2.saySelf); //false
从上述例子可以看出函数createStudent()能够根据接受参数来构建含有必要信息的新对象。该函数可以重复且无限次调用,而每次调用都会返回包含三属性一方法的对象。
工厂模式解决了创建多个相似对象的问题,但其缺点是无法识别对象(即无法判断对象类型)。
构造函数模式
关于工厂模式无法识别对象类型这个缺点,构造函数模式则解决了这个问题。ECMAScript中构造函数除了用来创建特定类型对象的原生构造函数(如Object、Array等)还可以用来创建自定义的构造函数并定义自定义对象类型的属性和方法。重写工厂模式示例如下:
function Student(name,age,gender){
this.name = name;
this.age = age;
this.gender = gender;
this.saySelf = function() {
alert("My name is:" + this.name + ". I’m " + this.age + " years old." );
};
}
var student1 = new Student("Lili",10,"girl");
var student2 = new Student("Sim",12,"boy");
student1.saySelf(); //"My name is:Lili. I’m 10 years old."
student2.saySelf(); //"My name is:Sim. I’m 12 years old."
alert(student1 instanceof Object); //true
alert(student1 instanceof Student); //true
alert(student2 instanceof Object); //true
alert(student2 instanceof Student); //true
alert(student1.saySelf == student2.saySelf); //false
比较工厂模式与构造函数模式
从上述例子比对工厂模式的示例,可以发现工厂模式与构造函数模式(或函数封装与构造函数)之间不同之处,具体区别如下表:
工厂模式 | 构造函数模式 | |
---|---|---|
显示创建对象(即new Object) | 否 | 是 |
属性、方法赋值对象 | 对象变量 | this对象 |
return 语句 | 有 | 没有 |
是否能检测自定义类型(instanceof操作符) | 否 | 是 |
命名方式(按惯例) | 驼峰命名法(第一个字母为小写) | 第一个字母为大写 |
构造函数模式解决了工厂模式无法检测自定义类型的问题,但它并没有优化全部。工厂模式和构造函数模式都存在的一个问题 —— 每个方法都会在每个实例上重新创建(即没有共享方法)。以这种方法创建函数会导致不同作用域链与标识符的解析,且完成同样任务的函数方法创建多个属实没有必要,存在浪费内存的可能性。在工厂模式和构造函数模式的示例中,可以看出两个实例的同名函数是不相等的。
由于有this对象在,可以不用在执行代码前就把函数绑定到特定对象上。因此为保证函数封装性外还能实现共享属性与方法,就用到了原型模式。
原型模式
讲原型模式之前首先要了解原型(prototype)属性。每个函数都有一个prototype属性(原型属性),该属性是一个指针,指向一个对象。而该对象的作用就是包含可以由特定类型的所有实例共享的属性和方法,即通过调用构造函数而创建的对象实例的原型对象。
由上述描述可知,使用原型对象的好处是可以让所有对象实例共享它的属性和方法,即对象实例信息不定义在构造函数里而添加在原型对象中。用原型模式改写构造函数模式示例如下:
function Student() {}
Student.prototype.name = "unknown";
Student.prototype.age = 0;
Student.prototype.gender = "unknown";
Student.prototype.saySelf = function(){
alert( this.name + " " + this.age + " years old" );
};
var student1 = new Student();
var student2 = new Student();
student1.saySelf(); //"unknown 0 years old"
student2.saySelf(); //"unknown 0 years old"
alert(student1.saySelf == student2.saySelf); //true
上述例子与构造函数模式示例中比较,可以看出两者的不同 —— 原型模式示例中,studen!和student2访问的是同一组属性和saySelf()函数。
在原型模式中,函数、原型对象和实例之间的关系很容易让人混淆。网上的帖子和书籍上都有详细的解释,而在面试询问时如何简洁扼要的回答是我想做到的。因此,我简述三者关系如下:
构造函数:在创建函数时就有prototype属性,该属性是一个指针,指向原型对象。
原型对象:所有原型对象会自动获得一个constructor(构造函数)属性,该属性也是一个指针,指向prototype属性所在的函数。
实例: 调用构造函数创建实例后,实例内会包含一个为指针的内部属性,该指针指向构造函数的原型对象。也就是说,构造函数与实例之间没有连接,连接存在于构造函数的原型对象与实例之间。(注:实例内指向构造函数的原型对象的指针在ES5中称为[[prototype]],而在部分浏览器中为_proto_)
注意点
除此之外,在原型模式中还需要注意以下几个要点:
- 可以通过对象实例访问保存在原型对象中的值,但不能通过对象实例重写原型对象的值。当对象实例中添加一个属性时,该属性会屏蔽原型对象中的同名属性。不过可以使用delete操作符完全删除对象实例属性。示例如下:
function Student() {}
Student.prototype.name = "unknown";
Student.prototype.age = 0;
Student.prototype.gender = "unknown";
Student.prototype.saySelf = function(){
alert( this.name + " " + this.age + " years old" );
};
var student1 = new Student();
var student2 = new Student();
student1.name = "Lili";
alert(student1.name); //"Lili"
alert(student2.name); //"unknown"
alert(student1.name == Object.getPrototypeOf(student1).name); //false 该行判断实例属性是否等于实例原型对象的同名属性
delete student1.name;
alert(student1.name); //"unknown"
- 属性枚举方法即通过循环访问函数里的每个属性,常用方法概述与示例如下:
for-in循环 | Object.keys()方法 | Object.getOwnPropertyNames()方法 | |
---|---|---|---|
返回内容 | 所有可访问、可枚举的属性,包括实例和原型中的属性 | 所有可枚举的实例属性,不包括原型中的属性 | 所有的实例属性(无论是否可枚举),不包括原型中的属性 |
返回类型 | 字符串 | 数组 | 数组 |
function Student() {}
Student.prototype.name = "unknown";
Student.prototype.gender = "unknown";
Student.prototype.age = 0;
Student.prototype.sayName = function() {
return this.name;
};
var student = new Student();
//实例未添加属性前
//for-in循环
for(var prop in student ){
alert(prop); //依次弹出"name"、"gender"、"age"、"sayName"
}
//Object.keys()方法
alert(Object.keys(Student.prototype)); //name,gender,age,sayName
alert(Object.keys(student)); //""
//Object.getOwnPropertyNames()方法
alert(Object.getOwnPropertyNames(Student.prototype)); //constructor,name,gender,age,sayName
alert(Object.getOwnPropertyNames(student)); //""
student.name = "Lili";
student.gender = "girl";
student.age = 10;
//实例添加属性后
//for-in循环
for(var prop in student ){
alert(prop); //依次弹出"name"、"gender"、"age"、"sayName"
}
alert(typeof(prop)); //"string"
//Object.keys()方法
alert(Object.keys(Student.prototype)); //name,gender,age,sayName
alert(Object.keys(student)); //name,gender,age
alert(Object.keys(student) instanceof Array); //true
//Object.getOwnPropertyNames()方法
alert(Object.getOwnPropertyNames(Student.prototype)); //constructor,name,gender,age,sayName
alert(Object.getOwnPropertyNames(student)); //name,gender,age
alert(Object.getOwnPropertyNames(student) instanceof Array); //true
- 重写原型对象时,需要注意实例中的指针仅指向原型,而不指向构造函数。因为重写原型对象会切断现有原型与任何之前已存在的对象实例之间的联系,而它们引用的仍是最初的原型。看示例代码更容易理解这一点:
//未重写原型对象时
function Student() {}
var student = new Student();
Student.prototype.sayYes = function() {
alert("yes");
};
student.sayYes(); //"yes"
//重写原型对象时
function Student() {}
var student = new Student();
Student.prototype = {
constructor: Student,
sayYes: function() {
alert("yes");
}
};
student.sayYes(); //error,student.sayYes is not a function
- 当原型对象包含引用类型值得属性时,由其共享特性导致的问题就显现出来了。当引用类型属性共享时,直接对引用类型属性操作则会直接改变原型对象中引用类型属性值。示例如下:
function Student(){}
Student.prototype = {
constructor: Student,
name: "Lili",
age:10,
gender: "girl",
friends: ["Amy","Tony"],
sayName:function() {
return this.name;
}
}
var student1 = new Student();
var student2 = new Student();
student1.friends.push("Sam");
alert(student1.friends); //Amy,Tony,Sam
alert(student2.friends); //Amy,Tony,Sam
实例和原型之间关系判断
由于在所有实现中都无法访问[[prototype]]属性,那如何确定实例和原型之间的关系也是一个问题。下方表格则是整合了确定实例和原型之间的关系的常用方法:
instanceof操作符 | isPrototypeOf() | Object.getPrototypeOf() | |
---|---|---|---|
判断方法 | 检测对象类型是否为原型 | [[prototype]]是否指向调用该方法的对象 | 该方法用于获取对象的原型 |
function Student() {}
Student.prototype = {
constructor: Student,
name: "Lili",
age: 10,
gender: "girl",
getName: function() {
return this.name;
}
};
function Teacher(){}
Teacher.prototype = {
constructor: Teacher,
name: "Lulu",
age: 36,
gender: "man",
getName: function() {
return this.name;
}
};
var student = new Student();
var teacher = new Teacher();
//instanceof操作符
alert(student instanceof Student); //true
alert(teacher instanceof Student); //false
//isPrototypeOf()
alert(Teacher.prototype.isPrototypeOf(student)); //false
alert(Teacher.prototype.isPrototypeOf(teacher)); //true
//Object.getPrototypeOf()
alert(Object.getPrototypeOf(student) == Student.prototype); //true
alert(Object.getPrototypeOf(teacher).name); //"Lulu"
属性归属判断
当我们需要判断对象中是否存在某个属性或属性是存在实例中还是存在原型中时,可以使用表格中的两种方式:
in操作符 | hasOwnProperty() | |
---|---|---|
作用 | 判断对象是否能访问到某属性,无论该属性存在实例还是原型中 | 检测属性存在于实例中还是原型中 |
function Student() {}
Student.prototype = {
constructor: Student,
name: "Unkown",
age: 0,
gender: "Unkown",
getName: function() {
alert(this.name);
}
}
var student1 = new Student();
var student2 = new Student();
student1.name = "Lili";
//in操作符
alert("name" in student1); //true
alert("name" in student2); //true
//hasOwnProperty()
alert(student1.hasOwnProperty("name")); //true
alert(student2.hasOwnProperty("name")); //false
student1.getName(); //"Lili"
student2.getName(); //"Unkown"
通常会结合in操作符与hasOwnproperty()来确定某个属性是否属于原型:
function hasPrototypeProperty(obj,prop){
return !obj.hasOwnProperty(prop) && (prop in obj);
}
//结合上面实例使用hasPrototypeProperty()
alert(hasPrototypeProperty(student1,"name")); //false
alert(hasPrototypeProperty(student2,"name")); //true
比较三种创建对象模式
总结以下三种创建模式概念以及特征:
工厂模式 | 构造函数模式 | 原型模式 | |
---|---|---|---|
概念 | 使用函数封装创建对象 | 使用构造函数创建对象 | 使用prototype属性创建对象 |
优点 | 解决多个相似对象创建问题 | 能够实现自定义对象识别 | 实现所有实例属性和方法共享 |
缺点 | 无法识别自定义对象,所有实例无法共享属性和方法 | 所有实例无法共享属性和方法 | 共享属性为引用类型时会出现问题 |
其他创建方法
组合模式 | 动态原型模式 | 寄生构造函数模式 | 稳妥构造函数模式 | |
---|---|---|---|---|
概述 | 组合使用构造函数模式与原型模式 | 所有信息(包括初始化原型-判断添加)都封装在构造函数中 | 封装创建对象代码,然后返回新创建的对象 | 使用没有公共属性且方法不引用this的稳妥对象创建对象 |
优点 | 每个实例都可拥有自己的实例且有共享方法 | 将构造函数与原型整合到一起,它们独立编写易混乱 | 重写调用构造函数时返回的值,用于为对象创建构造函数 | 安全环境(禁止使用this和new)中使用或防止数据被篡改 |
与工厂模式的关系 | —— | —— | 除使用new操作符并包装函数为构造函数外,其余一模一样 | 与工厂模式区别较大,一是方法不引用this,二是没有公共属性 |
关于工厂模式、寄生构造函数模式和稳妥构造函数模式的区别与联系可以查看这篇博客:《JS设计模式深入理解—工厂模式、寄生构造函数模式和稳妥构造函数模式的区别》
下面是关于四个设计模式的使用示例:
//组合使用构造函数模式和原型模式
function Student(name,gender,age){
this.name = name;
this.gender = gender;
this.age = age;
this.friends = ["Lili","Amy"];
}
Student.prototype.sayName = function(){
alert(this.name);
};
var student1 = new Student("Lili","girl",10);
var student2 = new Student("Sam","boy",12);
student1.friends.push("Tony");
alert(student1.friends); //Lili,Amy,Tony
alert(student2.friends); //Lili,Amy
alert(student1.friends == student2.friends); //false
alert(student1.sayName == student2.sayName); //true
alert(student1 instanceof Student); //true
//动态原型模式
function Student(name,gender,age) {
//属性
this.name = name;
this.gender = gender;
this.age = age;
this.friends = ["Lili","Amy"];
//方法
if(typeof this.sayName != "function" ){
Student.prototype.sayName = function() {
alert(this.name);
};
}
}
var student1 = new Student("Lili","girl",10);
var student2 = new Student("Sam","boy",12);
student1.sayName(); //"Lili"
student2.sayName(); //"Sam"
alert(student1.sayName == student2.sayName); //true
alert(student1 instanceof Student); //true
//寄生构造模式
function Student(name,gender,age) {
var o = new Object();
o.name = name;
o.gender = gender;
o.age = age;
o.friends = ["Lili","Amy"];
o.sayName = function() {
alert(this.name);
};
return o;
}
var student1 = new Student("Lili","girl",10);
student1.sayName(); //"Lili"
alert(student1 instanceof Student); //false
//稳妥构造函数模式
function Student(name,gender,age) {
//创建返回对象
var o = new Object();
//创建私有属性和函数
var name = name;
var gender = gender;
var age = age;
var friends = ["Lili","Amy"];
o.sayName = function() {
alert(name);
}
return o;
}
var student1 = Student("Lili","girl",10);
student1.sayName(); //"Lili"
alert(student1 instanceof Student); //false
对象继承
OO语言(面对对象语言)都支持接口继承和实现继承。接口继承只是继承方法签名【方法签名:由方法名称和一个参数列表方法的参数的顺序和类型)组成;接口继承:被继承者只有方法特征没有方法实现,继承者继承后需实现改方法】,实现继承则继承实际的方法。由于函数没有签名,因此在ECMAScript中无法实现接口继承。ECMAScript只实现接口继承,其实现继承主要是依靠原型链来实现的。
原型链
原型链的构建是通过将一个构造函数的实例赋值给另一个构造函数的原型实现的,由此子类型就能访问超类型的所有属性和方法。下面看原型链的实例能够更深刻的了解原型链构建的含义:
function School() {
this.schoolName = "China University";
}
School.prototype.getSchoolName = function() {
return this.schoolName;
}
function Class() {
this.className = "Peaceful Class";
}
Class.prototype = new School();
Class.prototype.getClassName = function() {
return this.className;
}
var class1 = new Class();
alert(class1.getClassName()); //"Peaceful Class"
alert(class1.getSchoolName()); //"China University"
注意点
-
所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针(可称为[[prototype]]或_proto_),指向Object.prototype。
-
原型链中确定原型与实例之间的关系:使用instanceof操作符或isPrototypeOf()方法测试实例与原型链中出现过的构造函数。
-
在原型链中,给原型添加方法的代码一定要放在替换原型的语句之后,并且在通过原型链实现继承时,不能使用对象字面量创建原型方法。例子如下:
function School() {
this.schoolName = "China University";
}
School.prototype,getSchoolName = function() {
return this.schoolName;
};
function Class() {
this.className = "Peaceful Class";
}
Class.prototype = new School();
Class.prototype = { //会覆盖上一行,使上一行无效
constructor: Class,
getClassName: function() {
return this.className;
},
}
var class1 = new Class();
alert(class1.getClassName()); //"Peaceful Class"
alert(class1.getSchoolName()); //Uncaught TypeError: class1.getSchoolName is not a function
- 原型链的问题是对象实例共享所有继承的属性和方法,因此有原型模式存在的包含引用类型,除此外还有创建子类型实例时,不能像超类型的构造函数中传递参数。
借用构造函数
为了解决原型链继承实例共享所有继承的属性和方法的这个问题,我们使用一种借用构造函数的技术,该技术是通过在子类型构造函数的内部调用超类型构造函数。由于函数是在特定环境中执行代码的对象,因此常用apply()和call()方法在新创建的对象上执行函数。
function CreateStudent(name,age) {
this.name = name;
this.age = age;
this.sayName = function() {
alert(this.name);
};
}
function Student(name,age,gender){
CreateStudent.call(this,name,age);
//CreateStudent.apply(this,[name,age]);
this.gender = gender;
this.sayGender = function() {
alert(this.gender);
};
}
var student1 = new Student("Lili",10,"gril");
var student2 = new Student("Sam",12,"boy");
student1.sayName(); //"Lili"
student1.sayGender(); //"gril"
student2.sayName(); //"Sam"
student2.sayGender(); //"boy"
alert(student1.sayName == student2.sayName); //false
观察上述例子,可以发现借用构造函数相比与原型链继承的优势在于可以在子类型构造函数中向超类型构造函数传递参数。但其也无法避免构造函数的问题——方法都在构造函数中定义,函数复用无法实现。
其他继承方法
组合继承 | 原型式继承 | 寄生式继承 | 寄生组合式继承 | |
---|---|---|---|---|
概述 | 别称“伪经典继承”,将原型链和借用构造函数组合使用,通过原型链实现对原型属性和方法的继承,通过构造函数来实现对实例属性的继承 | 借助原型基于已有对象创建新对象,不必因此创建自定义类型 | 创建一个仅用于封装继承过程的函数,该函数在内部增强该对象后再返回对象 | 通过借用构造函数来继承属性,通过原型链的混成形式来继承方法 |
优点 | 既实现函数复用又保证实例属性有独立性 | 不创建构造函数而只想让一个对象于另一个对象保持类似 | 主要考虑返回对象且任何能够返回新对象的函数都能使用该模式 | 避免调用两次超类型构造函数造成属性重叠,提高效率 |
缺点 | 超类型构造函数调用两次造成属性重叠 | 不使用构造函数无法检测自定义类型 | 不使用构造函数无法检测自定义类型 | 代码较为冗长 |
//组合继承
function CreateStudent(name){
this.name = name;
this.friends = ["Amy","Lulu"];
}
CreateStudent.prototype.sayName = function() {
alert(this.name);
}
function Student(name,age) {
CreateStudent.call(this,name);
//CreateStudent.apply(this,[name]);
this.age = age;
}
Student.prototype = new CreateStudent();
Student.prototype.constructor = Student;
Student.prototype.sayAge = function() {
alert(this.age);
}
var student1 = new Student("Lili",10);
student1.friends.push("Tony");
student1.sayName(); //"Lili"
student1.sayAge(); //10
alert(student1.friends); //Amy,Lulu,Tony
var student2 = new Student("Sam",12);
student2.sayName(); //"Sam"
student2.sayAge(); //12
alert(student2.friends); //Amy,Lulu
alert(student1.sayName == student2.sayName); //true
alert(student1.sayAge == student2.sayAge); //true
alert(student1 instanceof CreateStudent); //true
alert(student1 instanceof Student); //true
//原型式继承
function object(o){
function F(){};
F.prototype = o;
return new F();
}
//在ECMAScript5中,有Object.create()方法实现该功能
var student = {
name: "Lili",
age: 10,
friends: ["Amy","Lulu"],
sayName: function() {
alert(this.name);
},
};
var anotherStudent = object(student);
anotherStudent.name = "Sam";
anotherStudent.friends.push("Tony");
student.sayName(); //"Lili"
anotherStudent.sayName(); //"Sam"
alert(student.friends); //Amy,Lulu,Tony
alert(anotherStudent.friends); //Amy,Lulu,Tony
alert(anotherStudent.sayName == student.sayName); //true
alert(anotherStudent instanceof student); //error
//寄生式继承
function object(o){
function F(){};
F.prototype = o;
return new F();
}
//在ECMAScript5中,有Object.create()方法实现该功能
function createAnother(o){
var clone = object(o);
clone.sayBye = function() {
alert("Bye");
};
return clone;
}
var student = {
name: "Lili",
age: 10,
friends: ["Amy","Lulu"],
sayName: function() {
alert(this.name);
},
};
var anotherStudent = createAnother(student);
anotherStudent.name = "Sam";
student.sayName(); //"Lili"
anotherStudent.sayName(); //"Sam"
anotherStudent.sayBye(); //"Bye"
student.sayBae(); //error:student.sayBae is not a function
//寄生组合式继承
function object(o){
function F(){};
F.prototype = o;
return new F();
}
//在ECMAScript5中,有Object.create()方法实现该功能
function inheritPrototype(subType,superType){
var prototype = object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
function CreateStudent(name){
this.name = name;
this.friends = ["Amy","Lulu"];
}
CreateStudent.prototype.sayName = function() {
alert(this.name);
}
function Student(name,age) {
CreateStudent.call(this,name);
//CreateStudent.apply(this,[name]);
this.age = age;
}
inheritPrototype(Student,CreateStudent);
Student.prototype.sayAge = function() {
alert(this.age);
}
var student1 = new Student("Lili",10);
student1.friends.push("Tony");
student1.sayName(); //"Lili"
student1.sayAge(); //10
alert(student1.friends); //Amy,Lulu,Tony
var student2 = new Student("Sam",12);
student2.sayName(); //"Sam"
student2.sayAge(); //12
alert(student2.friends); //Amy,Lulu
alert(student1.sayName == student2.sayName); //true
alert(student1.sayAge == student2.sayAge); //true
alert(student1 instanceof CreateStudent); //true
alert(student1 instanceof Student); //true