美文网首页JS基础
JS对象原型prototype与继承,ES6的class与ext

JS对象原型prototype与继承,ES6的class与ext

作者: 下一站深圳 | 来源:发表于2018-02-16 16:49 被阅读2402次

    与java、c++相同,JavaScript 也是一门面向对象的程序语言。对象类型,是具有一系列相同的特征的事物的高度抽象,比如说人,每一个人有名字,会说话,会吃饭等,人就是一种对象类型。 如何来定义这种对象类型,描述其属性特征呢?

    传统方式:通过function关键字来定义一个对象类型

    function People(name) {
         this.name = name
    }
    People.prototype.toSay= function () {
        alert("我的名字是:" + this.name)
    }
    People.prototype.toEat= function () {
        alert("我吃饭")
    }
    var p = new People("小明")
    p.toSay(); // 我的名字是小明
    

    上面的代码里,我们定义People这种类型,它的属性特征有name、toSay、toEat 。然后我们以People为模板new出来一个p的实例对象。刚接触js时,可能会疑惑,function声明的不是函数么,怎么又变成定义对象类型?prototype是什么?

    其实在js中,函数本身也是一个对象。这种对象有点特殊,它的作用是定义了对象类型,可以说是数据结构模板。 而prototype是它的一个属性,称为对象原型,其本质也是一个对象,包含constructor和其他属性成员。constructor默认指向自身构造函数

    所以声明People的时候,程序自动People对象添加了prototype属性,并且让prototype.constructor指向了People,即函数本身。所以上面的例子等同于下面的写法:

    function People(name) {
       this.name = name
    }
    var proto = {
       constructor : People,
       toSay: function (name) {
            alert("我的名字是:" + name)   
       },
       toEat: function() {
            alert("我吃饭")
       }
    }
    People.prototype = proto   // 指定People的Prototype属性
    

    prototype的作用:当我们new一个实例对象p时,程序根据对象类型People的原型prototype,将原型所定义的属性(constructor除外)复制给新的实例对象p,并执行了一次prototype.constructor 所指向的构造函数,对实例对象p进行初始化

    实例对象p有两种属性:实例属性原型属性
    实例属性: 构造方法里定义的
    原型属性: 在原型prototype里定义
    hasOwnProperty方法可以帮我们区分

    p.hasOwnProperty("name"); // true
    p.hasOwnProperty("toSay"); // false,因为这个属性是原型上定义的
    

    问题1:为什么我们不直接都在构造函数里面定义呢?

    function  People(name) {
          this.name = name
          this.toSay = function() {
                alert("我的名字是:" + this.name)
          }
         this.toEat  = function() {
                alert("我吃饭")
        }
    }
    

    答: 这个主要考虑内存管理,因为函数是内存中的一个对象,也就是说,toSay或toEat都是对象占有一定内存。写在构造函数里面,每new一个实例对象,都会执行一次构造函数,都会重新创建一个函数对象,赋给新的实例对象的属性上。结果就是每一个实例对象的toSay或toEat属性都对应各自的函数对象,而这些函数功能都是一样的,我们创建了一大堆重复的函数对象。使用prototype不会,因为大家共享一个prototype对象。

    问题2: 为什么name不是直接定义在原型prototype上呢?
    答:每个人名字不同,如果定义在prototype上,大家名字就一样了,其中一个改变了name值,都会影响到其他实例对象。

    注意:实例对象是没有prototype属性,所以你不可以用实例对象为模板new一个新的实例对象来,只能用函数对象为模板来创建。

    var p1 = new People(''小明"); // 正确,函数对象的prototype的constructor指定构造方法
    var p2 = new p1("小王") ; // error ,实例对象没有prototype,找不到构造方法
    

    各大浏览器厂商给实例对象实现了一个 __proto__ 属性,指向对象原型,我们称为实例对象的隐式原型,即:

    var  p1 = new People("小明")
    p1.__proto__ ===  People.prototype    // true
    

    但我们要避免使用这个属性, 这个属性作用我猜测是浏览器提供给我们方便调试的时候用的。


    问题1:People.prototype是一个对象,这个对象是什么?
    答:Object, js所有对象默认继承js内置对象Object。
    问题2: js中,怎么实现对象的继承?
    答:js的继承是通过对象原型prototype来实现的。

    // 父类型
    function Animal(name) {
        this.name= name
        this.hasFoot = true
        this.color = ["orange", ''black"]
    }
    Animal.prototype = {
        constructor: Animal,
        voice: function(word) {
             console.info(word)
        } 
    }
    
    // 子类型 Cat
    function Cat() {}
    Cat.prototype = new Animal("cat");  //  Cat.prototype.constructor是Animal
    Cat.prototype.constructor = Cat; // 我们将构造函数指定回来,因为我们可以在构造扩展其它属性
    

    上面的代码,我们就实现了Cat的对象类型是继承了Animal对象类型,所以我们可以看到:

    var cat1 = new Cat()
    cat1.hasFoot    // true
    cat1.color      // ["orange","black"]
    cat1.toString   // function toString() { [native code] }
    

    hasFoot、与footNum都是从父类型annimal继承过来的,而toString为什么有呢,其实是这样,Cat继承了Animal,而Animal默认继承了Object,所以当我们找cat1的toString属性是,发现自身实例属性没有,发现原型上也没有定义,那程序就会寻所继承的父对象的实例属性,父对象的原型属性,这样一步步找下去,这就是JS的原型链。所以就是:

    cat1.__proto__   === Cat.prototype   // true
    cat1.__proto__.__proto__  === Animal.prototype  //  true ,因为Cat.prototype是一个Animal的实例对象
    

    上面的程序设计存在一个问题,有的猫只有一种颜色,有猫身上的颜色有三种,橘、白、黑。显然从Animal继承过来的颜色只有不能满足这种情况

    var  cat2 =new Cat()
    cat2.color.push("white");
    cat1.color  // ["orange", "black", "white"]
    //原因是因为,color是来自Cat.prototype,cat1和cat2共享一个prototype,你改变了cat2,cat1的color原型属性就会受到影响
    

    面对这种情况,我们的Cat对象类型应该这么写:

    function Cat() {
         Animal.call(this)  // 这样就可以将原型的实例属性变成自身的实例属性
    }
    Cat.prototype = new Animal()
    Cat.prototype.constructor = Cat
    var cat1 = new Cat()
    var cat2 = new Cat()
    cat1.color === cat2.color   // false
    

    虽然的方式解决了问题,但是还是有个缺点,调用了两次Animal构造函数。第一次是指定Cat.prototype,第二次是Cat自身构造函数中主动调用。我们更想要的是,指定了prototype,new实例时,构造函数就不要再调用Animal()了。此时我们需要一个工具来完成

    // 工具extend
    function extend(super, suber) {
            var proto = Object.create(super.prototype)
            proto.constructor = suber
            suber.prototype = proto
    }
    
    function Cat() {
          Animal.call(this); 
    }
    extend(Animal, Cat)
    

    上面的这种方式,将指定Cat.prototype从通过new Animal()换成直接Object.creat(Animal.prototype),这样就避免了Animal() 构造函数的执行。
    实际上,这方式是最高效的方式。

    对比其他面向对象开发语言(如: java),js通过function定义对象类型,容易让人不理解。ES6新规范推出class和extends关键字来实现面向对象编程。
    ES6方式:用class关键字定义对象类型,用extends关键字实现继承

    const private2 = Symbol('I am symbol value')
    class A {
       a1 = '1'  // ES7 实例属性,需要new实例来访问, ES6规定class没有静态属性,只有静态方法所以只能在constructor中定义属性
       static a2 = '2'  // ES7的静态属性,直接 A.a2 访问,不需要new实例
       getA1() {
             return  this.a1      // this指向new实例
       }
       static  getA2() {
            return   ‘2’    // 静态方法
       }
       constructor(name) {
             //一定要有构造方法,如果没有默认生成空构造方法
            this.a3 = '3'   // 这里定义实例属性
            this.name = name
        }
    
       // 私有方法写法
       publicMethod() {
             private1()    // 私有方法1,可以写在class体外
             private2()   // 利用Symbol值来定义
       }
       [private2]() {
           // 这里是私有方法
       }
    }
    const private1 = function() { // 这里也是私有方法,但别export出去}
    // 最后export class
    export default A
    

    class关键字会让我们更清晰设计一个对象类型,实际上,这只是语法糖:

    1. A 的实质还是一个function
    2. 对属性的定义是实例属性,而对方法的定义是定义在原型上
    // 通过extends继承
    class B extends A{
        constructor() {
          // 一定要在构造函数的第一句调用super
          super()    // 这是调用父类的构造方法
          this.b1 = '11'
          this.b2 = super.a1    // super直接调用时指向父类构造方法,范围属性时,指向父类实例,或调用父类静态方法
        }
    }
    

    我们可以知道,实际上A、B都是两个对象类型,B继承A。ES6的class作为语法糖也提供了prototype和proto两个属性;

    let instanceA = new A()
    let instanceB = new B()
    A.prototype   // Object
    instanceA.__proto__    //即A.prototype 还是Object
    B.prototype   //  A的实例对象,并且constructor指定为ClassB
    instanceB.__proto__   //B.prototype
    instanceB.__proto__.__proto__    // 即A.prototype ,即Object
    
    // es6提供对class  的__proto__的访问
    A.__proto__    // A本质是函数,函数也是对象,A是Object的实例,实例对象__proto__是对象类型的原型,这里是 [native code]
    B.__proto__    // B继承A,B.prototype是A的实例,B是A的实例,所以B.__proto__  === A.prototype
    

    对于class本来就是让我们能够避开传统function的不容易理解的语义,我们实际中尽量不要去使用proto,很多时候把自己给绕晕了。另外,这里补充一句:
    class内部定义的变量是不能存在变量提升的,也就是说你用了var也是不存在变量提升。因为他是一个语法糖,我们new一个实例时才会走进构造函数栈,执行完后,当前栈被销毁,而里面返回的值赋给了实例的属性,而里面的变量标记会被清除掉,因此不存在变量提升。

    相关文章

      网友评论

        本文标题:JS对象原型prototype与继承,ES6的class与ext

        本文链接:https://www.haomeiwen.com/subject/qadptftx.html