前置知识
原型
定义
每个构造函数都有一个原型对象①,原型有一个属性指回构造函数②,而实例有一个内部指针指向原型③。
问题
function Person(){
this.name = "litangmm";
}
Person.prototype.getName = function(){
return this.name;
}
let instance = new Person();
console.log(instance.getName()); // litangmm
那么这段代码的Person
构造函数、实例instance
它们的原型是什么呢?
解析
根据①,Person
构造函数有一个原型对象,其中有一个属性指回构造函数本身。而第二段代码在Person的原型上添加了一个 getName
方法。
console.log(Person.prototype);
console.log(Person.prototype.constuctor === Person)
可以看到,原型上 constuctor
就是自身, 还有我们定义的getName
。还有一个属性__proto__
,它指向的区域是似乎是Object
的原型。
根据③,实例instance
有一个内部指针,指向构造函数的原型。这个指针称被Chrome、FireFox等浏览器暴露出来为__proto__
。那么此时,instance
上应该有构造函数执行时绑定在instance
上的name
,值为litangmm
,和__proto__
属性,指向Person.prototype
。
console.log(instance);
console.log(instance.__proto__ === Person.prototype)
而在JavaScript中,函数也是一种对象,那么是否意味着,Person
也有__proto__
属性呢?根据函数的定义方法,我们似乎有一种不推荐使用的方法来构造函数,即使用Fuction
来new
一个函数。
我们尝试一下:
原型链
如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型。相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。
原型链的应用:
- JavaScript属性搜索
- Object的类型检测
属性搜索
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
}
function SubType(){
this.subproperty = false;
}
SubType.prototype = new SuperType();
// SubType原型是SuperType的实例。而SuperType实例有一个内部指针指向SuperType的原型。
// SubType.prototype => SuperType instance
// SuperType instance.__proto__ => SuperType.prototype
SubType.prototype.getSubValue = function(){
return this.subproperty;
}
let instance = new SubType();
// instance是SubType的实例,所以instance.__proto__ = SubType.prototype
console.log(instance.getSuperValue());
这样做有什么用呢?这就涉及到通过对象访问属性的搜索方式了。
以上述代码为例,我们看最后一行,instance.getSuperValue()
是如何执行的。
-
首先,会在
instance
的实例中,即instance
的本身属性上查找是否有getSuperValue
。显然,并没有这个属性。
-
所以,搜素会沿着指针(
__proto__
)进入原型对象SubType.prototype
,在原型对象上搜索。注意:代码进行了一次赋值SubType.prototype
此时是一个SuperType
实例,所以会有property
属性,和__proto__
属性(指向SuperType
)。似乎还是没有找到。
-
所以,搜素会沿着指针(
__proto__
)进入原型对象,在原型对象上搜索。注意:此时的原型对象是一个SuperType
实例的原型对象,所以指向的是Super.prototype
。终于,我们看到了
getSuperValue()
属性。于是搜索结束,返回该对象。 -
最后,是函数的执行。这里会执行一次搜索
property
,与搜索函数类似。
Object的类型检测
Object不同于JavaScript的其他类型,使用 typeof 时,它总返回 Object。
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
var simpleStr = "This is a simple string";
var myString = new String();
var newStr = new String("String created with constructor");
var myDate = new Date();
var myObj = {};
var myNonObj = Object.create(null);
simpleStr instanceof String; // 返回 false, 非对象实例,因此返回 false
myString instanceof String; // 返回 true
newStr instanceof String; // 返回 true
myString instanceof Object; // 返回 true
myObj instanceof Object; // 返回 true, 尽管原型没有定义
({}) instanceof Object; // 返回 true, 同上
myNonObj instanceof Object; // 返回 false, 一种创建非 Object 实例的对象的方法
myString instanceof Date; //返回 false
myDate instanceof Date; // 返回 true
myDate instanceof Object; // 返回 true
myDate instanceof String; // 返回 false
**isPrototypeOf()**方法用于测试一个对象是否存在于另一个对象的原型链上。
function Foo() {}
function Bar() {}
function Baz() {}
Bar.prototype = Object.create(Foo.prototype);
Baz.prototype = Object.create(Bar.prototype);
var baz = new Baz();
console.log(Baz.prototype.isPrototypeOf(baz)); // true
console.log(Bar.prototype.isPrototypeOf(baz)); // true
console.log(Foo.prototype.isPrototypeOf(baz)); // true
console.log(Object.prototype.isPrototypeOf(baz)); // true
类
首先,我们要知道类是什么?
类:每个类包含数据说明和一组操作数据或传递消息的函数。类的实例称为对象。
可以理解为,类是一组对象的抽象,这组对象有相似的数据结构和函数。类能够节省许多不必要的重复代码
例如,人就是一个类,人都有姓名,性别,他们都能出自己的姓名和性别。如果,不使用类,可以写下如下代码:
let person1 = {
name: "litangmm",
sex: "man",
sayName: function(){
console.log(this.name);
},
saySex: function(){
console.log(this.sex);
}
}
let person2 = {
name: "littleM",
sex: "woman",
sayName: function(){
console.log(this.name);
},
saySex: function(){
console.log(this.sex);
}
}
person1.sayName();
person2.saySex();
如果使用类呢?
function Person(name,sex){
this.name = naem;
this.sex = sex;
}
Person.prototype.sayName = function(){
console.log(this.name);
}
Person.prototype.saySex = function(){
console.log(this.sex);
}
let person1 = new Person("litangmm","man");
let person2 = new Person("littleM","woman");
person1.sayName();
person2.saySex();
孰优孰劣,一眼就可以看出来。
工厂模式
function ObjectFactory(name,sex){
let obj = {
name:name,
sex:sex,
sayName: function(){
console.log(this.name);
},
saySex: function(){
console.log(this.sex);
}
};
return obj;
}
let person1 = ObjectFactory("litangmm","man");
let person2 = ObjectFactory("littleM","woman");
person1.sayName();
person2.saySex();
工厂模式的原理其实很简单,就是将我们直接操作Object的步骤抽象成了一个函数。
这存在一个问题,就是,没办法判断创建对象的类型,因为通过这种方式创建的所有对象就是加强过的Object
。当然如果你不需要判断类型,完全可以使用这种方法。
构造函数模式
function Person(name,sex){
this.name = name;
this.sex = sex;
this.sayName = function(){
console.log(this.name);
}
this.saySex = function(){
console.log(this.sex);
}
}
let person1 = new Person("litangmm","man");
let person2 = new Person("littleM","woman");
person1.sayName();
person2.saySex();
这里,使用了JavaScript提供给我们的new操作符,new操作符做了以下这些事:
- 在内存中创建一个新对象。
- 这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype 属性。
- 构造函数内部的this 被赋值为这个新对象(即this 指向新对象)。
- 执行构造函数内部的代码(给新对象添加属性)。
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
注意第2步,这是和工厂模式的重要区别。而前置知识中,原型的定义,就是在这一步实现的:
我们可以自己实现一个newInstance
函数来模拟new操作符:
function newInstacne(construct,...args){
let obj = {};
obj.__proto__ = construct.prototype;
let newConstruct = construct.bind(obj); // 可以使用 bind call apply,
let res = newConstruct(args);
return typeof res === 'object'?res:obj;
}
对于第一,二行,ES5为我们提供了Object.create()
规范了这一操作。
优化我们的代码:
function newInstacne(construct,...args){
let obj = Object.create(construct.prototype);
let newConstruct = construct.bind(obj); // 可以使用 bind call apply,
let res = newConstruct(args);
return typeof res === 'object'?res:obj;
}
构造函数模式通过指定所创建实例的原型使得我们可以通过instanceof
,来进行实例的类型判断。
当然,这种方式还是有缺点的。
不难发现,我们创建的每一个实例,都有相同的函数,这些函数都挂载在实例的属性上,我们有没有方法可以干掉这些属性吗?
原型模式
思想:对于类共有的方法,我们可以挂载在构造函数的原型上,而不是写在构造函数内。
function Person(name,sex){
this.name = name;
this.sex = sex;
}
Person.prototype.sayName = function(){
console.log(this.name);
}
Person.prototype.saySex = function(){
console.log(this.sex);
}
let person1 = new Person("litangmm","man");
let person2 = new Person("littleM","woman");
person1.sayName();
person2.saySex();
这样创建的对象实例,就只有属性值,而函数都在原型上,使用时可以通过原型访问到。
这样看起来就很舒服了。
继承
继承是面向对象的另外一个重要特性。比如说,我们使用原型模式创建了Person
,现在又有需求了,有一个Student
类,它不仅有name
sex
还有 一个 number
值,表示学号,sayNumber
函数,用来输出number
;还有一个 techer
类,它不仅有name
sex
还有 一个 course
值,表示所教的课程,sayCourse
函数,用来输出course
。那么,我们是不是得重新创建两个类,然后把Person
拷贝两份,然后再分别加上它们的特殊的值和函数吗?那太糟糕了!而继承就是用来解决这一问题,即继承父类的所有方法和属性,并扩展。
原型链
function Person(){
this.name = "litangmm";
this.sex = "man";
}
Person.prototype.sayName = function(){
console.log(this.name);
}
Person.prototype.saySex = function(){
console.log(this.sex);
}
function Student(number){
this.number = number;
}
Student.prototype = new Person();
Student.prototype.sayNumber = function(){
console.log(this.number);
}
let student = new Student(1);
console.log(student.sayName());
console.log(student.saySex());
console.log(student.sayNumber());
这个我们在前置知识中介绍过了,所以不再赘述。这里指说一下它的缺点:
- 子类共享一个Person实例,通过不同子类实例访问父类属性会是同一个;
- 无法定制父元素的属性值。
盗用构造函数
对于原型链继承的缺点1,我们可以利用对象变量搜索规则:对象会先搜索实例上的属性。
将不希望共享的变量绑定在Student
实例中。
那么怎么操作呢?我们可以在Student
,构造函数中加点操作。
function Person(){
this.name = "litangmm";
this.sex = "man";
}
Person.prototype.sayName = function(){
console.log(this.name);
}
Person.prototype.saySex = function(){
console.log(this.sex);
}
function Student(number){
Person.apply(this); // 绑定this
this.number = number;
}
Student.prototype.sayNumber = function(){
console.log(this.number);
}
let student = new Student(1);
console.log(student.sayName()); // 报错
console.log(student.saySex());
console.log(student.sayNumber());
我们盗用了Person构造函数,new Student时,会将name和sex赋给创建的实例中。注意,此时代码会报错,因为,student实例的原型时Student.prototype,而Student.prototype并没有重新赋值,所以也就找不到sayName和saySex方法。
这里调用了构造函数,所以,我们在调用时传递参数来进行赋值。
function Person(name,sex){
this.name = name;
this.sex = sex;
}
Person.prototype.sayName = function(){
console.log(this.name);
}
Person.prototype.saySex = function(){
console.log(this.sex);
}
function Student(number,name,sex){
Person.call(this,name,sex); // 绑定this
this.number = number;
}
缺点:无法使用父类原型上的方法。
组合继承
组合继承结合了原型链和盗用构造函数。
function Person(name,sex){
this.name = name;
this.sex = sex;
}
Person.prototype.sayName = function(){
console.log(this.name);
}
Person.prototype.saySex = function(){
console.log(this.sex);
}
function Student(number,name,sex){
Person.call(this,name,sex); // 绑定this
this.number = number;
}
Student.prototype = new Person();
Student.prototype.sayNumber = function(){
console.log(this.number);
}
let student1 = new Student(1,"litangmm","man");
console.log(student1.sayName());
console.log(student1.saySex());
console.log(student1.sayNumber());
let student2 = new Student(1,"littleM","woman");
console.log(student2.sayName());
console.log(student2.saySex());
console.log(student2.sayNumber());
看起来很完美!
接下来,你可以仿照Student,组合方式来写一个Teacher类。
原型式继承
虽然组合模式看起来很好,但是还是存在问题的。就那上面的代码来说,我们发现 Person() 构造函数被执行了两次。
function Student(number,name,sex){
Person.call(this,name,sex); // 绑定this // 第一次 ...
...
Student.prototype = new Person(); // 第二次
第一次,我们是必须执行的,因为不执行会造成变量共享。
那么第二次,我们是否可以不执行呢?
回想一下,我们执行第二次原型是为了把Student.prototype指向Person上的原型,从而用上定义在Person原型上的方法。是否可以使用其他方式来实现呢?
在类的构造函数模式里,我们了解了new的执行过程。
为了让生成的对象的__proto__
指向构造函数的原型。我们定义了一个函数,专门来进行这一步操作,ES5也帮我们进行封装:
Object.prototype.create(proto,propertiesObject){
......
function F() {}
F.prototype = proto;
return new F();
}
我们可以只执行这步操作,而不执行构造函数。这便是原型式继承。
function Person(){ }
Person.prototype.name = "litangmm"
Person.prototype.sex = "man"
Person.prototype.sayName = function(){
console.log(this.name);
}
Person.prototype.saySex = function(){
console.log(this.sex);
}
let student = Object.create(Person.prototype);
console.log(student.sayName());
console.log(student.saySex());
console.log(student.sayNumber());
在这种情况下,原型式继承的student
只能访问到原型上的属性和方法,因为并没有创建Person
实例。
寄生式继承
现在不管原型式继承共享变量和类型判断的问题,我们先来看看,原型式继承如何实现自定义方法和属性。
原型式继承返回一个对象,即要为对象要自定义方法和属性,这其实和类很相似,所以,我们可以试试使用工厂模式,来自定义这个对象。
function createNewObj(orgin){
let clone = Object.create(orgin);
clone.number = 1;
clone.sayNumber = function(){
console.log(number);
}
return clone;
}
寄生组合式继承
接下来,我们使用寄生组合式继承来优化组合模型,把第二次调用Person构造函数干掉。
// 目的,不使用new 构造函数,把 SubType.prototype -> Super.prototype
function solution(superType,subType){
let proto = Object.create(superType.prototype);
// proto: {__proto__: superType.prototype}
proto.constructor = subType; // 这一步是因为 subType的prototype 应该包含这个值,所以加上
// proto: {constructor:subType, __proto__ = superType.prototype}
subType.prototype = proto;
// subType: {prototype:{constructor:subType, __proto__:superType.prototype}}
}
如此,我们就实现了不调用构造函数,来改变 SubType 的原型的指向。
最终继承:
function Person(name,sex){
this.name = name;
this.sex = sex;
}
Person.prototype.sayName = function(){
console.log(this.name);
}
Person.prototype.saySex = function(){
console.log(this.sex);
}
function Student(number,name,sex){
Person.call(this,name,sex); // 绑定this
this.number = number;
}
solution(Student,Person); // 替换.....................
Student.prototype.sayNumber = function(){
console.log(this.number);
}
let student1 = new Student(1,"litangmm","man");
console.log(student1.sayName());
console.log(student1.saySex());
console.log(student1.sayNumber());
let student2 = new Student(1,"littleM","woman");
console.log(student2.sayName());
console.log(student2.saySex());
console.log(student2.sayNumber());
其实,寄生组合式继承就是少执行了 new 操作的 后面几步。
参考:
JavaScript高级程序设计(第4版)
MDN Web Docs
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!