最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 长文 彻底弄懂ES5中的类与继承

    正文概述 掘金(李唐敏名)   2021-02-26   505

    前置知识

    原型

    定义

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

    问题

    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)
    

    长文 彻底弄懂ES5中的类与继承

    长文 彻底弄懂ES5中的类与继承

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

    长文 彻底弄懂ES5中的类与继承


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

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

    长文 彻底弄懂ES5中的类与继承

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

    长文 彻底弄懂ES5中的类与继承

    我们尝试一下:

    长文 彻底弄懂ES5中的类与继承

    原型链

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

    原型链的应用:

    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

      显然,并没有这个属性。

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

      似乎还是没有找到。

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

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

    4. 最后,是函数的执行。这里会执行一次搜索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操作符做了以下这些事:

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

    优化我们的代码:

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

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

    长文 彻底弄懂ES5中的类与继承

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

    原型模式

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

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

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

    长文 彻底弄懂ES5中的类与继承

    这样看起来就很舒服了。

    继承

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

    寄生式继承

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

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

    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


    起源地下载网 » 长文 彻底弄懂ES5中的类与继承

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元