万字:长文彻底弄懂ES5中的类和继承

前置知识

原型

定义

每个构造函数都有一个原型对象①,原型一个属性指回构造函数②,而实例有一个内部指针指向原型③

问题
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)

image-20210226085911471

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bweNSNho-1614320651950)(https://raw.githubusercontent.com/litangmm/cloudImage/main/img/image-20210226090358264.png)]

可以看到,原型上 constuctor就是自身, 还有我们定义的getName。还有一个属性__proto__,它指向的区域是似乎是Object的原型。

image-20210226092551099

这似乎很费解,为什么prototype里还有一个__proto__呢?

其实,我们可以这样想,prototype本质上也是一个对象,而对象是Object的实例,所以Object实例的__proto__自然指向Object.prototype了。


根据③,实例instance 有一个内部指针,指向构造函数的原型。这个指针称被Chrome、FireFox等浏览器暴露出来为__proto__。那么此时,instance上应该有构造函数执行时绑定在instance上的name,值为litangmm,和__proto__属性,指向Person.prototype

console.log(instance);
console.log(instance.__proto__ === Person.prototype)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GUKRyUuS-1614320651954)(https://raw.githubusercontent.com/litangmm/cloudImage/main/img/image-20210226091845921.png)]

而在JavaScript中,函数也是一种对象,那么是否意味着,Person也有__proto__属性呢?根据函数的定义方法,我们似乎有一种不推荐使用的方法来构造函数,即使用Fuctionnew一个函数。

image-20210226093255870

我们尝试一下:

image-20210226092834986

原型链

如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型。相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。

原型链的应用:

  1. JavaScript属性搜索
  2. 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()是如何执行的。

  1. 首先,会在instance的实例中,即instance的本身属性上查找是否有getSuperValue

    image-20210226095225031

    显然,并没有这个属性。

  2. 所以,搜素会沿着指针(__proto__)进入原型对象SubType.prototype,在原型对象上搜索。注意:代码进行了一次赋值SubType.prototype此时是一个SuperType实例,所以会有property属性,和__proto__属性(指向SuperType)。

    image-20210226095526420

    似乎还是没有找到。

  3. 所以,搜素会沿着指针(__proto__)进入原型对象,在原型对象上搜索。注意:此时的原型对象是一个SuperType实例的原型对象,所以指向的是Super.prototype

    image-20210226100351080

    终于,我们看到了getSuperValue()属性。于是搜索结束,返回该对象。

  4. 最后,是函数的执行。这里会执行一次搜索property,与搜索函数类似。

    可以思考一下,如果SubType构造函是这样

    function SubType(){
        // this.subproperty = false;
        this.property = false; 
    }
    

    代码会输出啥?

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操作符做了以下这些事:

  1. 在内存中创建一个新对象。
  2. 这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype 属性。
  3. 构造函数内部的this 被赋值为这个新对象(即this 指向新对象)。
  4. 执行构造函数内部的代码(给新对象添加属性)。
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

注意第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()规范了这一操作。

**Object.create()**方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。这个方法会在继承中再次遇到。我们可以先了解一下,它是怎么做的:

if (typeof Object.create !== "function") {
    Object.create = function (proto, propertiesObject) {
        if (typeof proto !== 'object' && typeof proto !== 'function') {
            throw new TypeError('Object prototype may only be an Object: ' + proto);
        } else if (proto === null) {
            throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
        }

        if (typeof propertiesObject !== 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");

        function F() {}
        F.prototype = proto;

        return new F();
    };
}

它是如何实现的呢?核心代码是最后3行,我们从原型的角度分析它返回了什么。

这代码返回一个objobj构造函数为 FF的原型是 传入对象。所以返回对象 obj.__proto__ === F.prototype === proto 。就是这么简单。

为什么要这样做呢?直接赋值不更简单吗?其实,这是因为__proto__属性不是ES标准中的,是部分浏览器的实现,其他环境,如node,通过__proto__是访问不到的。

image-20210226113303751

优化我们的代码:

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,来进行实例的类型判断。

当然,这种方式还是有缺点的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gNsYK1SS-1614320651962)(https://raw.githubusercontent.com/litangmm/cloudImage/main/img/image-20210226114906222.png)]

不难发现,我们创建的每一个实例,都有相同的函数,这些函数都挂载在实例的属性上,我们有没有方法可以干掉这些属性吗?

原型模式

思想:对于类共有的方法,我们可以挂载在构造函数的原型上,而不是写在构造函数内。

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();

这样创建的对象实例,就只有属性值,而函数都在原型上,使用时可以通过原型访问到。

image-20210226114805712

这样看起来就很舒服了。

继承

继承是面向对象的另外一个重要特性。比如说,我们使用原型模式创建了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());

这个我们在前置知识中介绍过了,所以不再赘述。这里指说一下它的缺点:

  1. 子类共享一个Person实例,通过不同子类实例访问父类属性会是同一个;
  2. 无法定制父元素的属性值。

盗用构造函数

对于原型链继承的缺点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实例。

红皮书上给出的是这个例子:

let person = {
	name: "Nicholas",
	friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"

寄生式继承

现在不管原型式继承共享变量和类型判断的问题,我们先来看看,原型式继承如何实现自定义方法和属性。

原型式继承返回一个对象,即要为对象要自定义方法和属性,这其实和类很相似,所以,我们可以试试使用工厂模式,来自定义这个对象。

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值