美文网首页
第六章 面向对象的程序设计(js高级程序设计)

第六章 面向对象的程序设计(js高级程序设计)

作者: 简默丶XS | 来源:发表于2019-02-20 22:04 被阅读0次

    Object-Oriented 面向对象

    理解对象

    • 对象属性分为 【数据属性】 和 【访问器属性】
    • 对象属性中的【数据属性】
      1. configurable 描述该属性能否通过delete删除或被重新定义(为false时defineProperty也无法使用了)
      2. enumerable 能否被for-in循环返回属性
      3. writable 能否修改属性的值
      4. value 这个属性的数据值
    • 利用defineProperty来定义数据属性


      因为writable属性被设置为false,我尝试修改xusheng的值,但是无效
    • 对象属性中的的 【访问器属性】
    1. configurable 描述该属性能否通过delete删除或被重新定义(为false时defineProperty也无法使用了)
    2. enumerable 能否被for-in循环返回属性
    3. get 在读取属性时调用的函数。
    4. set 在写入属性时调用的函数。
    当获取_name值时,调用get方法得到name的值。当设置_name值时,其实时将name值改为了‘默认值’

    _ name 前面 _ 的书写代表只能通过对象方法访问的属性。

    • 定义多个属性:defineProperties可以创建多个对象属性


      例子创建了_year和edition两个数据属性和year访问器属性
    • 读取属性的特性
      通过getOwnPropertyDescriptor获取到的上面例子的属性描述符
      var descriptor = Object.getOwnPropertyDescriptor(book, "_year");

    上述book对象的_ year属性的属性描述符
    • 创建对象方法:
      1. new Object()
      2. 对象字面量{ }
        缺点:使用同一个接口创建很多对象,会产生大量的重复代码。

    其他创建对象的方法

    1. 工厂模式
    这种模式抽象了创建具体对象的过程,用函数来封装以特定接口创建对象的细节。

    工厂模式用函数封装了创建对象的过程
    缺点:工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

    2. 构造函数模式
    构造函数可以用来创建特定类型的对象

    书中的案例
    • 构造函数开头字母需要大写(Person)

    • 在new 构造函数时,程序会执行以下步骤

      1. 创建一个新对象
      2. 将构造函数的作用域赋值给新对象
      3. 执行构造函数中的代码
      4. 返回新对象
    • 对象的 constructor 是用来标识对象类型的

    • 通过构造函数创建对象有一个constructor属性,指向原构造函数

    • 创建得对象是构造函数的实例


      实例和构造函数关系
    • 以上构造函数带来的问题,相同作用的函数被反复复制,改进:

    sayName其实做了同一件事,但是却被创建成了两个不同的function,浪费 将sayName拎出来
    • 新的问题:全局下又不可能有多个类似于sayName函数的东西,会很乱

    3. 原型模式

    关系图 实例的sayName均来自构造函数的原型 构造函数同样实现效果

    回到问题本质,与构造函数的区别在哪里?
    新对象的这些属性和方法是由所有实例共享(不会再创建相同作用的函数)的。换句话说,person1 和 person2 访问的都是同一组属性和同一个 sayName()函数

    • 理解原型对象
    我的关系图:person1实例与构造函数没有直接关系,person1实例可以调用sayName是因为他在原型对象上进行查找.!注意这里有个错误:应该是__proto__。两个下划线! 书中关系图
    • __proto__ 是两个下划线!!

    • 确定实例和原型对象关系可以用:

    1. isPrototypeOf()判断对象是否和原型对象有关
    2. Object.getPrototypeOf()获得对象的原型对象

    alert(Person.prototype.isPrototypeOf(person1)); //true
    alert(Person.prototype.isPrototypeOf(person2)); //true

    Object.getPrototypeOf(person1) == Object.getPrototypeOf(person1) //获得person1对象指向的原型对象

    • 当调用person1.sayname时。解析器会现在实例对象上查找sayname属性,如果找不到,再在原型对象上查找sayname属性。所以实例上的属性会屏蔽掉原型上同名属性

    • 通过delate操作符可以删除实例上的属性,从而恢复原型的访问

    • 利用hasOwnProperty(“属性名”)可以判断这个属性是来自于原型对象还是自己本身

    person1实例上name属性来自本身还是来自于原型对象上
    • 与hasOwnProperty不同的是,使用in操作符只要能访问到该属性都会返回true(无论是原型对象上还是实例对象上)
    alert(person1.hasOwnProperty("name")); //false 
    alert("name" in person1); //true name属性在实例对象属性中
    
    • hasPrototypeProperty(person1,"name") 实例对象要能访问到原型上的属性才能返回true,即使原型对象上有属性name,但是实例对象上的name将原型对象上的name屏蔽,所以依然返回false
    var person = new Person(); 
    alert(hasPrototypeProperty(person, "name")); //true  
    person.name = "Greg"; 
    alert(hasPrototypeProperty(person, "name")); //false  实例对象要能访问到原型上的属性才能返回true
    
    • for in 枚举属性
      原则:所有开发人员定义的属性都是可以枚举的属性,所以对象实例属性和原型对象属性都是可以被枚举的。


      图中红色框中属性都可被枚举,黄色框中不可被枚举,因为他们来自原型链中的Object原型属性
    • 取得对象上所有可枚举的实例属性:Object.keys()

    • 使用Object.key()取得对象上可枚举的属性组成的字符串数组

    var person = new Person()
    Object.key(person) //"say"  person实例对象上的属性,原型对象上的不会被
    console.log(Object.keys(person.__proto__))// "age,job,name,sayName" 原型上的属性
    
    • Object.getOwnPropertyNames(object) 可以列出对象上所有的属性,即使是不可枚举的属性,你可以试着枚举出继承的Object对象上的属性
    console.log(Object.getOwnPropertyNames(person.__proto__.__proto__))
    
    • 换一种方式定义构造函数原型上的属性
    function Person(){ 
    } 
    Person.prototype = { 
     constructor : Person,  //如果没有这句,constructor 属性就指不到构造函数了
     name : "Nicholas", 
     age : 29, 
     job: "Software Engineer", 
     sayName : function () { 
     alert(this.name); 
     } 
    }; 
    

    此方法带来的问题:constructor 的[[Enumerable]]值为true可枚举了

    使用Object.defineProperty重设构造函数

    Object.defineProperty(Person.prototype, "constructor", { 
     enumerable: false, 
     value: Person 
    }); //此写法适用于es5
    
    • 原型的动态性:很好理解,实例对象(person1)的prototype属性指针指向原型对象,所以在原型对象上多次修改属性值,在实例上也同样能被正确的访问
    var friend = new Person(); 
    Person.prototype.sayHi = function(){ 
     alert("hi"); 
    }; 
    friend.sayHi(); //"hi"(没有问题!)
    

    当然,你不能重写原型对象,切断与构造函数的联系

    function Person(){ 
    } 
    var friend = new Person(); 
    
    Person.prototype = { 
     constructor: Person, 
     name : "Nicholas", 
     age : 29, 
     job : "Software Engineer", 
     sayName : function () { 
     alert(this.name); 
     } 
    }; 
    friend.sayName(); //error 
    
    在new 构造函数时,构造函数会为实例创造指针__proto__指向原型对象。重写原型后,旧的实例依然和旧的原型保持着联系,和新的原型没有联系
    • 原生对象的原型:原生(Array,String)构造函数都在其原型(prototype)上定义了方法,所以我们可以通过例如String.substring()来操作字符串实例
    String.prototype.startsWith() =function(){
      ...//你可以给原生对象添加属性,让所有实例化的字符串都能访问,但是不建议这样做
    }
    
    
    • 原型对象的问题:修改原型对象上的属性值会对其他实例对象造成影响(我们有时候需要它[所有string的实例都能使用String.substring()],有时候又不需要它)

    4. 组合使用构造函数和原型模式

    • 书中想表达的意思就是物尽其用,将实例属性放到构造函数中定义,将用于共享的属性和方法放到原型对象里
    function Person(name, age, job){ 
     this.name = name; 
     this.age = age; 
     this.job = job; 
     this.friends = ["Shelby", "Court"]; 
    } 
    Person.prototype = { 
     constructor : Person, 
     sayName : function(){ 
     alert(this.name); 
     } 
    } 
    

    5. 动态原型模式:在构造函数中使用原型模式,并且避免重复挂载到原型模式
    它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点
    换句话说,第一次new Person()的时候,sayName肯定是不存在的,所以会挂载sayName和其他方法到原型上,而下一次newPerson的时候,sayName()已经在原型上了[注意if判断的作用],sayName包括其他if语句中的方法都不会被重复挂载

      function Person(name, age, job) {
        //属性
        this.name = name;
        this.age = age;
        this.job = job; //方法
        if (typeof this.sayName != "function") {
          Person.prototype.sayName = function () {
            alert(this.name);
          };
    
          Person.prototype.sayAge = function () {
            alert(this.age);
          };
        }
      }
    

    6. 寄生构造函数模式
    类似于工厂模式,不过使用new function()的形式创建

    function Person(name, age, job){ 
     var o = new Object(); 
     o.name = name; 
     o.age = age; 
     o.job = job; 
     o.sayName = function(){ 
     alert(this.name); 
     }; 
     return o; 
    } 
    var friend = new Person("Nicholas", 29, "Software Engineer"); 
    friend.sayName(); //"Nicholas" 
    

    这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊
    数组。由于不能直接修改 Array 构造函数,因此可以使用这个模式
    注意:返回的对象与构造函数或者与构造函数的原型属性之间没有关系。 instanceof 不能溯源,所以建议在可以使用其他模式的情况下,不要使用这种模式。

    7. 稳妥构造函数模式(闭包)

    function Person(name, age, job){ 
     
     //创建要返回的对象
     var o = new Object(); 
     //可以在这里定义私有变量和函数
     //添加方法
     o.sayName = function(){ 
     alert(name); 
     }; 
     //返回对象
     return o; 
    } 
    var friend = Person("Nicholas", 29, "Software Engineer"); 
    friend.sayName(); //"Nicholas" 
    

    除了使用 sayName()方法之外,没有其他办法访问 name 的值(有点用到了闭包,让匿名函数返回私有变量)
    同样和寄生构造模式一样不能溯源,不能使用instanceof

    继承

    • 函数签名(或者类型签名,抑或方法签名)定义了 函数或方法的输入与输出。JavaScript 是一种类型松散或动态语言。这意味着您不必提前声明变量的类型。以下是java的函数签名
    public static void main(String[] args)
    
    • 继承分为接口继承和实现继承
      接口继承:接口是一种特殊的抽象类,即继承函数的签名,故js中没有接口继承
      实现继承:实现继承是继承函数实际的方法

    • 子类型超类型
      子类型:继承者
      超类型:被继承者(下文用父类型)

    1.原型链

    继承图解A继承B
    • 注意此时的 instance 应该是指向B的原型,因为A的prototype被b的实例重写了。

    • 在实例中搜索该属性。如果没有找到该属性,则会继续搜索实例的原型。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上

    • 所有函数的默认原型都是 Object 的实例:原型链的底层永远都是Object。因此默认原型都会包含一个内部指针,指向 Object.prototype(也就是继承于Object原生)。这也是为什么所有自定义类型都会继承toString(),valueOf()方法的原因。上图的B其实还继承了原生Object,如下图。


      默认原型都会指向Object.prototype,
    • 确定原型和实例的关系的方式:
      1.instanceof

    alert(instance instanceof Object); //true 
    alert(instance instanceof SuperType); //true 
    alert(instance instanceof SubType); //true
    
    1. isPrototypeOf
    alert(Object.prototype.isPrototypeOf(instance)); //true 
    alert(SuperType.prototype.isPrototypeOf(instance)); //true 
    alert(SubType.prototype.isPrototypeOf(instance)); //true 
    
    • 在完成继承后再添加额外的方法,否则额外的方法会被继承覆盖掉。先.prototype = new Function(),再.prototype.xx=xx.更不能使用.prototype={ }字面量重写原型
    • 原型继承带来的引用类型的问题:原型中包含引用类型,A构造函数的原型继承于B的实例,基于A创建M实例,修改引用类型值,那么B的原型中的值也相应会改变,如果再基于A创建N实例,那么M,N就会拥有相同的引用。
      不能向超类型的构造函数传递参数

    2.借用构造函数

    • 利用apply或者call在自己的构造函数中调用别人的构造函数
    function SuperType(name){ 
     this.name = name; 
    } 
    function SubType(){ 
     //继承了 SuperType,同时还传递了参数
     SuperType.call(this, "Nicholas"); 
     
     //实例属性
     this.age = 29; 
    } 
    var instance = new SubType(); 
    alert(instance.name); //"Nicholas"; 
    alert(instance.age); //29 
    
    • 缺点很明显:方法都在构造函数中定义,因此函数复用就无从谈起。而且父类型原型中的方法子类型也用不了

    3.组合继承

    • 组合继承是ji中常用的实现继承的方式,将原型继承和构造函数继承集合
      function f1(name) {
        this.name = name
        this.colors = ['blue', 'red', 'green'] //将需要被继承的引用类型定义在构造函数中
      }
    
      f1.prototype.sayName = function () {
        alert(this.name)
      }
    
      function f2(name, age) {
        f1.call(this, name)  //此时,f2上已经继承f1构造函数中的属性
        this.age = age
      }
    
      f2.prototype = new f1(); //此时,f2已经继承f1原型对象上的属性
    
      var F2 = new f2()
    
      for (const x in F2) {
        console.log(x)//name color age sayname
      }
    

    4.原型式继承

    • 将一个对象作为子对象的原型对象实现继承
      function object(o) {
        function F() { }
        F.prototype = o; //person对象被共享到了通过object创造出来的对象的原型上
        return new F();
      }
      var person = {
        name: "Nicholas",
        friends: ["Shelby", "Court", "Van"]
      };
      debugger
      var anotherPerson = object(person);
      anotherPerson.name = "Greg";
      anotherPerson.friends.push("Rob");
      var yetAnotherPerson = object(person);
      yetAnotherPerson.name = "Linda";
      yetAnotherPerson.friends.push("Barbie");
      alert(person.friends); //"Shelby,Court,Van,Rob,Barbie" 
    
    • Es5规范化方法:Object.creat()
    var person = { 
     name: "Nicholas", 
     friends: ["Shelby", "Court", "Van"] 
    }; 
    var anotherPerson = Object.create(person); 
    anotherPerson.name = "Greg"; 
    anotherPerson.friends.push("Rob"); 
     
    var yetAnotherPerson = Object.create(person); 
    yetAnotherPerson.name = "Linda"; 
    yetAnotherPerson.friends.push("Barbie"); 
    alert(person.friends); //"Shelby,Court,Van,Rob,Barbie" 
    

    在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式
    继承是完全可以胜任的.

    • 个人理解:其实就是原型链继承的缩写方案,同样存在原型链继承引用类型被共享的问题

    5.寄生式继承

      function createAnother(original) {
        var clone = Object.create(original); //通过调用函数创建一个新对象
        clone.sayHi = function () { //以某种方式来增强这个对象
          alert("hi");
        };
        return clone; //返回这个对象
      }
      var person = {
        name: "Nicholas",
        friends: ["Shelby", "Court", "Van"]
      };
      var anotherPerson = createAnother(person);
      anotherPerson.sayHi(); //"hi" 
    

    注意点: clone.sayHi 可以给新对象上添加属性,和寄生构造函数模式有点像,createAnother存在的意义就是在新的实例上添加公有属性

    6.寄生组合式继承

    • 还记得组合继承模式吗?通过f2.call(this)和.protype=new f2()分别调用了两次父类型,寄生组合式继承就是为了解决这个问题。
    • inheritPrototype接受两个参数,分别是子类型的构造函数和夫类型的构造函数
    function inheritPrototype(subType, superType){ 
     var prototype = Object.creat(superType.prototype); //创建对象
     prototype.constructor = subType; //增强对象
     subType.prototype = prototype; //指定对象
    } 
    function SuperType(name){ 
     this.name = name; 
     this.colors = ["red", "blue", "green"]; 
    } 
    SuperType.prototype.sayName = function(){ 
     alert(this.name); 
    }; 
    function SubType(name, age){ 
     SuperType.call(this, name); 
     this.age = age; 
    } 
    inheritPrototype(SubType, SuperType);   //SubType.protoype = new SuperType();
    SubType.prototype.sayAge = function(){ 
     alert(this.age); 
    }; 
    
    • 只调用了一次 SuperType 构造函数,并且因此避免了在 SubType. prototype 上面创建不必要的、多余的属性
    • 个人觉得是拷贝父类型的prototype到子类型的protype上,inheritPrototype的作用其实就是SubType.protoype = new SuperType();

    ps:终于看完了,真特么累,感觉迷迷糊糊的,肯定要刷第二遍的~。

    相关文章

      网友评论

          本文标题:第六章 面向对象的程序设计(js高级程序设计)

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