• JS重难点之一:原型链和继承


    1 原型解决的问题

    在总结原型之前,先简单回顾一下较为简单常用的创建对象的方式。

    1.1 对象字面量和new Object()

    对象字面量创建方式:

    var person1 = {
        name: "Li Xiaoming",
        age: 18,
        id: 1,
        sayName: function(){
            console.log(this.name)
        }
    }
     
     person1.sayName()     //"Li Xiaoming"

    new Object()方式:

    var person2 = new Object();
    person2.name = "Ma DongMei";
    person2.age = 19;
    person2.id = 2;
    person2.sayName = function(){
        console.log(this.name)
    }
    person2.sayName()     //
    "Ma DongMei"

    上面这两种创建对象的方式有如下缺点:

    1.当需要创建大量类似的person时,会产生很多冗余代码

    2.对象一直都是Object类,没有解决对象识别的问题

    1.2 工厂模式

    工厂模式就是封装一个创建对象的工厂(函数),将创建对象的细节放在这个函数中处理,每次需要对象的时候,调用该函数,它会生产(返回)一个对象实例。

    function createPerson(name, age, id){
        var o = new Object();
        o.name = name;
        o.age = age;
        o.id = id;
        o.sayName = function(){
            console.log(this.name);
        }
        return o;
    }
    
    var person3 = createPerson("Xia Lou", 20, 3);
    person3.sayName()   //"Xia Lou"

    缺点:

    虽然解决了代码冗余问题,但是任然没有解决对象识别的问题,工厂模式创建的对象任然都是Object类

    1.3 构造函数模式

    使用构造函数模式创建对象依赖new运算符(构造函数的本质任然是函数,其实也可以直接调用,待会再说这个问题)。

    1.3.1 构造函数和工厂模式的区别

    使用构造函数创建实例的过程如下:

    function Person(name, age, id){
        this.name = name;
        this.age = age;
        this.id = id;
        this.sayName = function(){
            console.log(this.name)
        }
    }
    
    var person4 = new Person("Da Zhuang", 21, 4);
    person4.sayName();    //"Da Zhuang"
    
    

    可以看到,它与工厂模式有如下几个区别

    1.没有显示的创建对象

    2.直接将属性和方法赋给了this

    3.没有return语句

    4.函数名称首字母是大写

    为什么这样的构造函数能返回一个实例对象呢?那是因为new运算符会进行如下操作

    1.在内存中创建一个新对象

    2.这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性

    2.将构造函数的作用域赋给新对象(因此this就指向了这个新对象)

    3.执行构造函数中的代码(为新对象添加属性和方法)

    4.如果构造函数返回非空对象,则返回该对象;否则,返回刚才创建的新对象(this指向的对象)

    所以最终我们会得到Person的实例。

    1.3.2 构造函数当成函数使用

    若将构造函数当成函数使用:

    // 在全局作用域下使用,this指向window对象,所以属性都添加到window对象上了
    Person("window", 100, 6)
    window.sayName();    //"window"
    
    //使用call将构造函数作用域绑定到对象o上,所以this指向了o对象
    var o = new Object();
    Person.call(o, "o", 351, 7)
    o.sayName()      //"o"

    1.3.3 构造函数创建的实例的类型检测

    说到类型检测,其实还是要从原型上来说这个问题,后文再说明。上面通过Person类创建的实例,都可以使用instanceof来检测类型:

    console.log(person4 instanceof Object);   //true
    console.log(person4 instanceof Person);   //true

    1.3.4 构造函数的问题

    每个方法都要在新创建的实例上重新创建一遍,即使是功能一模一样的方法。例如上面几个例子中的sayName()。

    即构造函数在每次创建一个对象时,下面的语句都会创建一个新的sayName函数,每个实例上的sayName都是独立的函数(不同的内存地址),只是指向这些不同内存地址的变量的名字相同而已

        this.sayName = function(){
            console.log(this.name)
        }

    为了解决这个问题,可以将sayName函数提取出来放在全局中:

    function Person(name, age, id){
        this.name = name;
        this.age = age;
        this.id = id;
        this.sayName = sayName
    }
    
    var sayName = function(){
        console.log(this.name)
    }

    但是这样会造成全局污染,Person这个引用类型的封装性被破坏

    接下来的原型模式就能解决这个问题。

    2 原型模式

    2.1 原型模式创建对象的方式

    原型模式创建对象方式如下:

    function Person(){}
    
    Person.prototype.name = "Ma Dongmei";
    Person.prototype.age = 43;
    Person.prototype.sex = "female"
    Person.prototype.sayName = function(){
      console.log(this.name)
    }
    
    var person1 = new Person();
    person1.sayName();                  //"Ma Dongmei"

    // function Animal(name, age, sex){
    //   Animal.prototype.name = name;
    //   Animal.prototype.age = age;
    //   Animal.prototype.sex = sex;
    //   Animal.prototype.sayName = function(){
    //     console.log(this.name);
    //   }
    // }

    // var dog = new Animal("dog", 1, "male");
    // console.log(dog)
    // var cat = new Animal("cat", 2, "female")
    // console.log(dog)
     

    缺点:

    1.省略了为构造函数传递初始化参数这一环节,结果所有的实例在默认情况下都取得相同属性值,(注意:不能采用上面代码中被注释掉的构造函数,因为每次使用构造函数创建对象的时候,原型对象都会被修改,所以以前创建的实例会与新实例的属性一致。在上面的例子中,第一次打印的时候,dog还是dog,第二次dog就变成cat了)

    2.由于属性都是实例共享的,所以对于引用类型属性来说,在某个实例上修改它,有可能会反应到其他实例对象上。(包含基本值的属性没关系,因为修改这个属性,相当于在实例对象上添加了一个同名属性,它会覆盖掉共享的属性)

    function Person(){}
    
    Person.prototype.name = "Ma Dongmei";
    Person.prototype.age = 43;
    Person.prototype.sex = "female";
    Person.prototype.arr = [1,2,3,4]
    Person.prototype.sayName = function(){
      console.log(this.name)
    }
    
    var person1 = new Person();
    var person2 = new Person();
    person1.arr.push(5)                //[1,2,3,4,5]

    2.2 isPrototypeOf和Object.getPrototypeOf

    当调用构造函数创建新的实例对象时,该实例对象内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版管这个指针叫[[Prototype]]。在部分实现中,这个属性完全不可见,但是在FireFox、Chrome、Safari等实现中,都将这个属性设置为__proto__。

    isPrototypeOf:

    继承自Object,所有对象都可以使用。可以判断某个对象是否是另外一个对象的原型。

    console.log(Person.prototype.isPrototypeOf(person1));    //true

    Object.getPrototypeOf:

    ES5新增的方法,获取一个对象的原型。

    console.log(Object.getPrototypeOf(person1) === Person.prototype);    //true
    console.log(Object.getPrototypeOf(person2).name)     //Ma Dongmei

    2.3 属性查找机制

    JavaScript属性查找过程:

    1.在实例对象上查找是否有某个属性,有,则使用该属性,没有:

    2.在该实例对象的原型上查找是否有这个属性,有,则使用该属性,没有:

    3.在该实例对象的原型的原型上查找是否有这个属性,有,则使用该属性,没有:

    4.任然顺着原型链继续查找,知道最后__proto__指向null。

    5.若指向null后任然没有找到,则返回undefined。

    因此:

    虽然实例对象可以访问原型上的属性,但是不能通过实例对象重写原型中的值,因为在实例上添加了一个属性,该属性和原型中的属性同名,那么该属性将会屏蔽掉原型中的那个属性。

    可以使用delete操作符完全删除实例属性(注意:该属性描述符中的configurable属性应该为true,否则删除无效,在严格模式下还会报错)

    2.4 属性检测方法:hasOwnProperty、in运算符

    hasOwnProperty():

    继承自Object,所有的对象都可使用。检测实例对象上是否存在某个属性。(无论属性修改符enumerable是false还是true,都可以正常检测)

    var o = {
      a:1,
      b:2,
      c:3
    }
    
    Object.defineProperty(o,"d",{
      value:4
    })
    
    console.log(Object.getOwnPropertyDescriptor(o,"d"))   //除了value,其他的全是false
    console.log(o.hasOwnProperty("d"))        //true

    in:

    只要对象能通过原型链找到该属性(无论属性是否可枚举),就返回true。

    《JS高三》里封装了一个函数。

    function hasPrototypeProperty(obj, name){
      return !obj.hasOwnProperty(name) && (name in obj)
    }

    该函数仅能判断属性是否在实例对象的原型链上,而不能断定它在实例对象的原型上。

    2.5 属性的遍历方法

    for...in

    遍历对象自身和继承而来的可枚举属性的属性名。

    var o = {
      a:1,
      b:2,
      c:3
    }
    
    Object.defineProperty(o,"d",{
      value:4
    })
    
    for(let key in o){
      console.log(key)                 
    }
    //a  b  c 

    Object.keys(obj):

    返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)

    Object.getOwnPropertyNames(obj):

    返回一个数组,包含对象自身的所有属性,包括不可枚举的属性,不含Symbol属性)

    Object.getOwnPropertySymbols(obj):

    返回一个数组,包含对象自身所有的Symbol属性。

    Reflect.ownKeys(obj):

    a返回一个数组,包含对象自身的所有属性,不管属性名是Symbol还是字符串,也不管是否可枚举。

    3 终极方法:组合构造函数模式和原型模式

    为了解决上述原型模式和构造函数模式的种种缺点,可以将他们组合使用。

    构造函数内用于定义实例属性,原型中定义方法和共享的属性。

    function Person(name, age, sex, ...friends){
      this.name = name;
      this.age = age;
      this.sex = sex;
      this.friends = friends;
    }
    
    Person.prototype.sayHello = function(){
      console.log(this.name)
    }
      Person.prototype.enemy = ["佩恩","角度","迪达拉"]
    var person1 = new Person("那撸多",18,"male","萨斯给","撒库拉","hi那他")
    console.log(person1);
    person1.sayHello();

    优点:

    1.每一个实例都有一份实例属性副本,同时又共享方法的引用

    2.支持向构造函数传递参数进行初始化。

  • 相关阅读:
    二分图匹配详解
    树状数组略解
    质数算法略解
    主席树详解
    线段树略解
    【题解】Luogu P2073 送花
    【题解】Luogu P1533 可怜的狗狗
    分块入门
    【题解】Luogu CF86D Powerful array
    【题解】Luogu UVA12345 Dynamic len(set(a[L:R]))
  • 原文地址:https://www.cnblogs.com/lilisblog/p/13223664.html
Copyright © 2020-2023  润新知