JavaScript 中的面向对象、原型、原型链、继承
JavaScript 中说一切都是对象,是不完全的,在 JS 中 6 种数据类型(Undefined、Null、Number、Boolean、String、Object)中,前五种是基本数据类型,是原始值类型,这些值是在底层实现的,他们不是 object,所以没有原型,没有构造函数,所以并不是像创建对象那样通过构造函数创建的实例。
创建对象
1.使用构造函数创建
var obj = new Object();
2.字面量创建
var obj = {};
3.工厂模式
如果使用构造函数和字面量创建很多对象,每个对象本身又有很多相同的属性和方法的话,就会产生大量重复代码,每个对象添加属性都需要重新写一次。如两个对象都需要添加 name、age 属性及 showName 方法:
var p1 = new Object();
p1.name = '张三'
p1.age = '16',
p1.showName = function() {
return this.name
}
var p2 = new Object();
p2.name = '李四'
p2.age = '18',
p2.showName = function() {
return this.name
}
为了解决这个问题,人们采用了工厂模式,抽象了创建对象的过程,采用函数封装以特定接口(相同的属性和方法)创建对象的过程。
function createPerson(name, age) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.showName = function () {
return this.name;
};
return obj;
}
var p1 = createPerson('张三', 16);
var p2 = createPerson('李四', 18);
4.构造模式
虽然工厂模式解决了创建多个对象的多个相同属性问题,却无法判定对象的具体类型,因为都是 Object,无法识别是 Array、或是 Function 等类型,这个时候构造函数模式出现了。
JS 中提供了像 Object、Array、Function 等这样的原生的构造函数,同时也可以创建自定义的构造函数,构造函数是一个函数,用来创建并初始化新创建的对象。将工厂模式的例子用构造函数可以重写为:
function Person(name, age) {
this.name = name;
this.age = age;
this.showName = function() {
console.log(this.name);
}
}
var p1 = new Person('张三', '16');
var p2 = new Person('李四', '18');
用 Person 代替了工厂模式的 createPerson 函数,而且函数名首字母 P 大写,这是因为按照惯例,构造函数首字母应该大写,而作为非构造函数的函数首字母小写。另外可以注意到构造函数内部的特点:
- 没有显示创建对象
- 直接在 this 上添加属性和方法
- 没有 return
另外,还使用了 new 操作,要创建一个实例,必须使用 new 操作符,使用 new 操作符调用构造函数,在调用构造函数的时候经历了如下几个阶段:
- 创建一个对象
- 把创建的对象赋值给 this
- 执行函数中的代码,即把属性和方法添加到赋值之后的 this
- 返回新对象
用伪代码来说明上述 new Person() 的过程如下:
// 使用new操作符时,会激活函数本身的内部属性[[Construct]],负责分配内存
Person.[[Construct]](initialParameters):
// 使用原生构造函数创建实例
var Obj = new NativeObject() //NativeObject为原生构造函数,如Object、Array、Function等
// 给创建的实例添加[[Class]]内部属性,字符串对象的一种表示, 如[Object Array]
// Object.prototype.toString.call(obj)返回值指向的就是[[Class]]这个内部属性
Obj.[[Class]] = Object/Array/Function;
// 给创建的实例添加[[Prototype]]内部属性,指向构造函数的prototype
O.[[Prototype]] = Person.prototype;
// 调用构造函数内部属性[Call],将Person执行上下文中this设置为内部创建的对象Obj
Result = Person.[[Call]](initialParameters);
// this = Obj;
// result是如果构造函数内部如果存在返回值的话,调用[[call]]时作为返回值,一般为Object类型
// 调用Person.[[call]]时,执行Person中的代码,给this对象添加属性和方法
this.name = name;
this.age = age;
this.showName = function() {
console.log(this.name);
};
//如果Person.[[call]]的返回值Result为Object类型
return Result
// 否则
return Obj;
构造函数虽然解决了实例多个同名属性重复添加的问题,但是也存在每个实例的方法都需要重新创建一遍,因为每个方法都是 Function 的不同实例,看下面这段代码就明白了:
function Person(name, age) {
this.name = name;
this.age = age;
this.showName = new Function("console.log(this.name);");
}
var p1 = new Person('张三', '16');
var p2 = new Person('李四', '18');
console.log(p1.showName === p2.showName); //false
这个问题可以用以下办法来解决,把 showName 变成全局函数
function Person(name, age) {
this.name = name;
this.age = age;
this.showName = showName;
}
function showName() {
console.log(this.name)
}
但是这样如果对象需要添加很多方法就会产生很多全局函数,这些问题可以通过原型模式来解决
5.原型模式
什么是原型
当每一个函数创建时,都会给函数设置一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,这个对象包含所有实例共享的属性和方法,在默认情况下,都会为 prototype 对象添加一个 constructor 属性,指向该函数。
Person.prototype.constructor = Person;
原型模式就是不必在构造函数中定义实例的属性和方法,而是将属性和方法都添加到原型对象中。创建自定义构造函数,其原型对象只会默认取得 constructor 属性,其他的属性和方法都是从 Object 继承来的。当使用构造函数创建一个实例之后,会给实例添加内部属性 prototype,这个属性是一个指针,指向构造函数的 prototype(原型)对象,由于是内部属性,无法通过脚本获取,但是在一些 Chrome、Firefox、Safari 等浏览器中在每个对象身上支持一个 __proto__ 属性,指向的就是构造函数的原型对象。另外可以通过 isProtoTypeOf() 来判断创建的实例是否有指向某构造函数的指针,如果存在,返回 true 如果不存在 返回 false。
function Person() {
}
Person.prototype.name = '张三';
Person.prototype.friends = ['张三', '李四'];
Person.prototype.showName = function() {
console.log(this.name);
}
var p1 = new Person();
var p2 = new Person()
console.log(p1.__proto__ === Person.prototype) // true
console.log(Person.prototype.isPrototypeOf(p1)) // true
在 ECMA5 中增加了一个方法 Object.getPrototypeOf(params),返回值就是创建对象的原型对象
console.log(Object.getPrototypeOf(p1) === Person.prototype); // true
console.log(Object.getPrototypeOf(p1).name); //张三
原型模式虽然解决了方法共享的问题,但是对于实例共享来说是个比较大的问题,因为每个实例都需要有描述自己本身特性的专有属性,还是上面的代码:
console.log(p1.name) // '张三'
console.log(p2.name) // '张三'
另外对于属性是引用类型的值来说缺点就更明显了,如果执行下面这段代码:
p1.friends.push('王五');
console.log(p1.priends); //['张三', '李四', '王五']
console.log(p2.priends); //['张三', '李四', '王五']
为了解决原型模式的问题,人们采用了原型和构造组合模式,使用构造函数定义实例,使用原型模式共享方法。
6 组合使用构造函数模式和原型模式
直接上代码:
function Person(name, age) {
this.name = name;
this.age = age;
this.friends = ['张三', '李四']; // this.friends = new Array('张三', '李四')
}
Person.prototype.showName = function() {
console.log(this.name);
};
var p1 = new Person('John');
var p2 = new Person('Alice');
p1.friends.push('王五');
console.log(p1.friends); // ['张三', '李四', '王五'];
console.log(p2.friends); // ['张三', '李四'];
// 因为这时候每个实例创建的时候的friends属性的指针地址不同,所以操作p1的friends属性并不会对p2的friends属性有影响
console.log(p1.showName === p2.showName) // true 都指向了Person.prototype中的showName
这种构造函数模式和原型模式组合使用,基本上可以说是 JS 中面向对象开发的一种默认模式,介绍了以上这几种常用创建对象的方式,还有其他不常用的模式就不介绍了,接下来想说的是 JS 中比较重要的继承。
继承
什么是原型链
ECMA 中继承的主要方法就是通过原型链,主要是一个原型对象等于另一个类型的实例,由于实例内部含有一个指向构造函数的指针,这时候相当于重写了该原型对象,此时该原型对象就包含了一个指向另一个原型的指针,假如另一个原型又是另一个类型的实例,这样就形成了原型链的概念,原型链最底层为 Object.prototype.proto 为 null。
属性查找机制
JS 中实例属性的查找,是按照原型链进行查找,先找实例本身有没有这个属性,如果没有就去查找查找实例的原型对象,也就是 [[prototype]] 属性指向的原型对象,一直查到 Object.prototype,如果还是没有该属性,返回 undefined。所有函数的默认原型都是 Object 实例。
function Parent() {
this.surname = '张';
this.name = '张三';
this.like = ['apple', 'banana'];
}
var par = new Parent()
function Child() {
this.name = '张小三';
}
Parent.prototype.showSurname = function() {
return this.surname
}
// 继承实现
Child.prototype = new Parent();
var chi = new Child();
console.log(chi.showSurname()) // 张
以上代码证明,此时 Child 实例已经可以访问到 showSurname 方法,这就是通过原型链继承 Parent 原型方法,剖析一下其过程:
Child.prototype = new Parent();
相当于重写了 Child.prototype,指向了父实例 par,同时也包含了父实例的 [[prototype]] 属性,此时
console.log(Child.prototype.__proto__ === par.__proto__); // true
console.log(Child.prototype.__proto__ === Parent.prototype); // true
执行 chi.showSurname() 时,根据属性查找机制:
- 先从实例 chi 本身查找,有没有 showSurname,没有
- 继续查找 chi 的原型对象 Child.prototype 有没有 showSurname,没有
- 继续查找 Child.prototype 的原型指针 __proto__ 有没有 showSurname,此时 Child.prototype.__proto__ 的指针地址指向 Parent.prototype,找到了,所以
console.log(chi.showSurname()) // 张
所有函数默认继承 Object:
function Person() {}
console.log(Person.prototype.__proto__ === Object.prototype); // true
构造函数模式和原型模式组合继承
只通过原型来实现继承,还存在一定问题,所以 JS 中一般通过借用构造函数和原型组合的方式来实现继承,也称经典继承,还是继承那段代码,再贴过来把,方便阅读
function Parent() {
this.surname = '张';
this.name = '张三';
this.like = ['apple', 'banana'];
}
var par = new Parent()
function Child() {
this.name = '张小三';
}
Parent.prototype.showSurname = function() {
return this.surname
}
// 继承实现
Child.prototype = new Parent();
var chi1 = new Child();
var chi2 = new Child();
console.log(chi.showSurname()) // 张
// 主要看继承的属性
console.log(chi.like) // ['apple', 'banana']
这是因为Child.prototype指向父实例,当查找实例chi本身没有like属性,就去查找chi的原型对象Child.prototype,所以找到了
那么还存在什么问题呢?主要就是涉及到引用类型的属性时,引用类型数据的原始属性会被实例所共享,而实例本身的属性应该有实例自己的特性,还是以上代码
chi.like.push('orange');
console.log(chi1.like); // ['apple', 'banana', 'orange']
console.log(chi2.like); // ['apple', 'banana', 'orange']
所以构造函数和原型组合的经典继承出现了,也是本篇最重要的内容:
1.属性继承
在子构造函数内,使用 apply() 或 call() 方法调用父构造函数,并传递子构造函数的 this
2.方法继承
使用上文提到的原型链继承,继承父构造器的方法
上代码:
function Parent(name) {
this.name = name;
this.like = ['apple', 'banana'];
}
Parent.prototype.showName = function() {
console.log(this.name);
};
function Child(name, age) {
// 继承属性
Parent.call(this, name);
// 添加自己的属性
this.age = age;
}
Child.prototype = new Parent();
// 子构造函数添加自己的方法
Child.prototype.showAge = function() {
console.log(this.age);
};
var chi1 = new Child('张三', 16);
var chi2 = new Child('李四', 18);
chi1.showName(); //张三
chi1.showAge(); //16
chi1.like.push('orange');
console.log(chi1.like); // ['apple', 'banana', 'orange']
console.log(chi2.like); // ['apple', 'banana']
在子构造函数 Child 中是用 call() 调用 Parent(),在 new Child() 创建实例的时候,执行 Parent 中的代码,而此时的 this 已经被 call() 指向 Child 中的 this,所以新建的子实例,就拥有了父实例的全部属性,这就是继承属性的原理。
对 chi1 和 chi2 的 like 属性,是每个实例自己的属性,二者间不存在引用依赖关系,所以操作 chi.like 并不会对 chi.like 造成影响。方法继承,就是上文讲的到的原型链机制继承,另外可以给子构造函数添加自己的属性和方法。
这就是经典继承,避免了但是使用构造函数或者单独使用原型链的缺陷,成为 JS 中最常用的继承方式。
个人扩展补充
hasOwnProperty()
用法 obj.hasOwnProperty(prop)
使用 hasOwnProperty() 方法可以判断访问的属性是原型属性还是实例属性,如果是实例属性返回 true 否则返回 false
function Person() {
}
Person.prototype.name = '张三'
var p1 = new Person();
var p2 = new Person();
p1.name = '张三';
console.log(p1.hasOwnProperty('name')) //true
console.log(p2.hasOwnProperty('name')) //false
重写原型对象
在实际开发中,如果原型对象有很多方法,往往我们可以使用字面量的形式,重写原型,但是需要手工指定 constructor 属性
function Person(name, age) {
this.name = name;
this.age = age;
}
var p1 = new Person('张三', 16);
Person.prototype.showName = function() {
return this.name;
}
Person.prototype.showAge = function() {
return this.age;
}
如果构造函数的 prototype 方法很多,可以采用字面量方式定义
Person.prototype = {
constructor: Person,
showName: function() {
return this.name;
},
showAge: function() {
return this.age;
}
}
注意这里面手动加了一个 constructor 属性指向 Person 构造函数,这是因为使用字面量重写原型对象,这个原型对象变成了一个Object的实例,原型对象本身已经不存在最初函数创建时初始化的constructor 属性,这是原型对象的 [[prototype]] 指针指向了 Object.prototype
显式 prototype 和隐式 [[Prototype]] 属性
function Person() {
}
Person.prototype.a = 10;
var p = new Person();
console.log(p.a) //10
Person.prototype = {
constructor: Person,
a: 20,
b: 30
}
console.log(p.a) // 10
console.log(p.b) // undefined
var p2 = new Person();
console.log(p2.a) // 20
console.log(p2.b) // 30
因此,有的文章说 动态修改原型将影响所有的对象都会拥有新的原型 是错误的,新原型仅仅在原型修改以后的新创建对象上生效。
这里的主要规则是:对象的原型是对象的创建的时候创建的,并且在此之后不能修改为新的对象,如果依然引用到同一个对象,可以通过构造函数的显式prototype引用,对象创建以后,只能对原型的属性进行添加或修改。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 浅谈 JavaScript 中的闭包
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论