关于javascript:JS的面向对象理解对象原型原型链继承类

39次阅读

共计 12146 个字符,预计需要花费 31 分钟才能阅读完成。

js 有 6 中根本数据类型 Undefined、Null、Boolean、Number、String 和 Symbol。还有一种简单数据类型叫 Object。

了解对象

Object 是一种无序名值对的汇合

形式一
const person = new Object();
person.name = '张三';
person.age = 16;
person.sayName = () => {console.log(this.name);
}


// 形式二
const person = {
    name: '张三',
    age: 16,
    sayName: ()=>{console.log(this.name);
    }

}

1. 数据属性

    Object.defineProperty(person, 'name', {
        writable: true, // 是否能被重写
        configurable: true, // 是否被删除
        enumerable: true, // 是否被 for-in 遍历
        value: '张三',
    });

writable 和 configurable 为 false 的状况下,批改和删除不失效。而且在严格模式下会报错。

2. 拜访器属性

    const person = {age: 16};
    Object.defineProperty(person, 'name', {get: function() {return this._name;},
        set: function(newV) {this._name = newV;}
    });

此外还能够一次定义多个属性

    const person = {};
    Object.defineProperties(person, {
        _name: {
            configurable: true,
            enumerable: true,
        },
        name: {get: function() {return this._name;},
            set: function(newV) {this._name = newV;}
        },
        age: {get: function() {return this.age;},
            set: function(newV) {this.age = newV;}
        }
    })

3. 读取属性的个性

应用 Object.getOwnPropertyDescriptor()办法能够获得指定属性的属性描述符。

    const person = {age: 16, _name: 'zhang'};
      const descriptor = Object.getOwnPropertyDescriptor(person, 'age');
    console.log('...', descriptor.writable);  // true
    console.log('...', descriptor.enumerable);  // true
    console.log('...', descriptor.configurable);  // true
    console.log('...', descriptor.value);  // 16

创建对象

1. 工厂模式

    function createPerson(name, age) {
        return {
            name,
            age,
            sayName: function () { // 留神这里不能用箭头函数,会影响 this 的指向
                console.log(this.name);
            }
        };
    }
    let person1 = createPerson("张三", 29);
    let person2 = createPerson("李四", 27);

    person1.sayName(); // 张三
    person2.sayName(); // 李四

毛病:无奈解决对象标识问题(即新创建的对象是什么类型)。

2. 构造函数模式

注意事项

  • new 创立函数
  • 函数名应该大写。

      function Person(name, age) {
          this.name = name;
          this.age = age;
          this.sayName = ()=>{console.log(this.name);
          };
      }
      let person1 = new Person("张三", 29);
      let person2 = new Person("李四", 27);
    
      person1.sayName(); // 张三
      person2.sayName(); // 李四

    new 执行的操作

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

构造函数模式毛病:构造函数的次要问题在于,其定义的办法会在每个实例上 都创立一遍。

console.log(person1.sayName == person2.sayName); // false

3. 原型模式

    function Person() {}
    Person.prototype.name = "张三";
    Person.prototype.age = 29;
    Person.prototype.sayName = function() {console.log(this.name);
    };
    let person1 = new Person();
    person1.sayName(); // "张三"
    let person2 = new Person();
    person2.sayName(); // "张三"
    console.log(person1.sayName === person2.sayName); // true

对象原型, 原型链

1. 了解原型

无论何时,只有创立一个函数,就会依照特定的规定为这个函数创立一个 prototype 属性(指向 原型对象)。默认状况下,所有原型对象主动取得一个名为 constructor 的属性,指回与之关联的构 造函数。对后面的例子而言,Person.prototype.constructor 指向 Person。而后,因构造函数而异,可能会给原型对象增加其余属性和办法。

    function Person() {}
    console.log(Person.prototype.constructor === Person); // true

在自定义构造函数时,原型对象默认只会取得 constructor 属性,其余的所有办法都继承自 Object。每次调用构造函数创立一个新实例,这个实例的外部 [[Prototype]] 指针就会被赋值为构 造函数的原型对象。脚本中没有拜访这个 [[Prototype]] 个性的规范形式,但 Firefox、Safari 和 Chrome 会在每个对象上裸露__proto__属性,通过这个属性能够拜访对象的原型。在其余实现中,这个个性 齐全被暗藏了。关键在于了解这一点: 实例与构造函数原型之间有间接的分割,但实例与构造函数之间没有。

留神:

  1. 构造函数、原型对象和实例是 3 个齐全不同的对象

      console.log(person1 !== Person); // true
      console.log(person1 !== Person.prototype); // true
      console.log(Person.prototype !== Person);  // true
  2. 实例与构造函数没有间接分割,与原型对象有间接分割

    // 实例通过__proto__链接到原型对象,它实际上指向暗藏个性[[Prototype]]。// 构造函数通过 prototype 属性链接到原型对象
    console.log(person1.__proto__ === Person.prototype); // true
    conosle.log(person1.__proto__.constructor === Person); // true
  3. 同一个构造函数创立的两个实例,共享同一个原型对象:

    console.log(person1.__proto__ === person2.__proto__); // true

原型相干办法

  • isPrototypeOf()
    能够应用 isPrototypeOf()办法确定两个对象之间的这种关系。实质上,isPrototypeOf()会在传入参数的 [[Prototype]] 指向调用它的对象时 返回 true,如下所示:

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

    这里通过原型对象调用 isPrototypeOf()办法查看了 person1 和 person2。因为这两个例子内 部都有链接指向 Person.prototype,所以后果都返回 true。

  • Object.getPrototypeOf()
    Object 类型有一个办法叫 Object.getPrototypeOf(),返回参数的外部个性 [[Prototype]]的值。

    console.log(Object.getPrototypeOf(person1) == Person.prototype); // true console.log(Object.getPrototypeOf(person1).name); // "张三"
  • 重写对象的继承关系 办法一:Object.setPrototypeOf()

      let person = {name:'zhang'};
      let student = {grade: '一年级'};
    
      Object.setPrototypeOf(student, person);
    
      console.log(student.grade); // 一年级
      console.log(student.name); // zhang
      console.log(Object.getPrototypeOf(student) === person); // true

    Object.setPrototypeOf()可能会重大影响代码性能。所以不举荐应用

  • 重写对象的继承关系 办法二:Object.create()
    为防止应用 Object.setPrototypeOf()可能造成的性能降落,能够通过 Object.create()来创 建一个新对象,同时为其指定原型:

      let person = {name:'zhang'};
      let student = Object.create(person)
      student.grade = '一年级';
    
      console.log(student.grade); // 一年级
      console.log(student.name); // zhang
      console.log(Object.getPrototypeOf(student) === person); // true

2. 原型链

  /**
  * 失常的原型链都会终止于 Object 的原型对象 * Object 原型的原型是 null
  */
  console.log(Person.prototype.__proto__ === Object.prototype);  // true
  console.log(Person.prototype.__proto__.constructor === Object); // true
  console.log(Person.prototype.__proto__.__proto__ === null); // true
  console.log(Person.prototype.__proto__ === Person);
  • 用 instanceof 查看原型链

    console.log(person1 instanceof Person);  // true
    console.log(person1 instanceof Object);  // true
    console.log(Person.prototype instanceof Object);  // true
  • 原型层级
    在通过对象拜访属性时,会依照这个属性的名称开始搜寻。搜寻开始于对象实例自身。如果在这个 实例上发现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜寻会沿着指针进入原 型对象,而后在原型对象上找到属性后,再返回对应的值。因而,在调用 person1.sayName()时,会 产生两步搜寻。首先,JavaScript 引擎会问:“person1 实例有 sayName 属性吗?”答案是没有。而后,持续搜寻并问:“person1 的原型有 sayName 属性吗?”答案是有。于是就返回了保留在原型上的这 个函数。在调用 person2.sayName()时,会产生同样的搜寻过程,而且也会返回雷同的后果。这就是 原型用于在多个对象实例间共享属性和办法的原理。
    尽管能够通过实例读取原型对象上的值,但不可能通过实例重写这些值。如果在实例上增加了一个 与原型对象中同名的属性,那就会在实例上创立这个属性,这个属性会遮住原型对象上的属性。上面看 一个例子:

      function Person() {}
      Person.prototype.name = "张三";
      Person.prototype.age = 29;
      Person.prototype.job = "码农";
      Person.prototype.sayName = function() {console.log(this.name);
      };
      let person1 = new Person();
      let person2 = new Person();
      person1.name = "李四";
      console.log(person1.name); // "李四",来自实例
      console.log(person2.name); // "张三",来自原型
  • 应用 hasOwnProperty()办法确定属性在实例上,还是在原型对象上

      function Person() {}
      Person.prototype.name = "张三";
      Person.prototype.age = 29;
      Person.prototype.job = "码农";
      Person.prototype.sayName = function() {console.log(this.name);
      };
      let person1 = new Person();
      let person2 = new Person();
      console.log(person1.hasOwnProperty("name")); // false
    
    
      person1.name = "李四";
      console.log(person1.name); // "李四",来自实例
      console.log(person1.hasOwnProperty("name")); // true
    
      console.log(person2.name); // "张三",来自原型
      console.log(person2.hasOwnProperty("name")); // false
    
    
      delete person1.name;
      console.log(person1.name); // "张三",来自原型
      console.log(person1.hasOwnProperty("name")); // false
  • in 操作符
    in 操作符会在可 以通过对象拜访指定属性时返回 true,无论该属性是在实例上还是在原型上。

     function Person() {}
      Person.prototype.name = "张三";
      Person.prototype.age = 29;
      Person.prototype.job = "码农";
      Person.prototype.sayName = function() {console.log(this.name);
      };
      let person1 = new Person();
      let person2 = new Person();
      console.log(person1.hasOwnProperty("name")); // false
    
    
      person1.name = "李四";
      console.log(person1.name); // "李四",来自实例
      console.log(person1.hasOwnProperty("name")); // true
      console.log("name" in person1); // true
    
      console.log(person2.name); // "张三",来自原型
      console.log(person2.hasOwnProperty("name")); // false
      console.log("name" in person1); // true
    
      delete person1.name;
      console.log(person1.name); // "张三",来自原型
      console.log(person1.hasOwnProperty("name")); // false
      console.log("name" in person1); // true

继承

继承是面向对象编程中探讨最多的话题。很多面向对象语言都反对两种继承: 接口继承和实现继承。前者只继承办法签名,后者继承理论的办法。接口继承在 js 中是不可能的,因为函数没有签名。实现继承是 js 惟一反对的继承形式,而这次要是通过原型链实现的。

1. 原型链

    function Person(name) {this.name = name;}

    Person.prototype.sayName = function (){console.log(` 我是 ${this.name}`);
    }

    function Student(grade) {this.grade = grade;}

    Student.prototype = new Person('张三');
    Student.prototype.sayGrade = function () {console.log(` 我曾经 ${this.grade}了 `);
    }

    const stu1 = new Student('一年级');
    console.log(stu1.name); // 张三
    console.log(stu1.grade); // 一年级
    stu1.sayName(); // 我是张三
    stu1.sayGrade(); // 我曾经一年级了

原型链的毛病
次要问题呈现在原型中蕴含援用值的时候。前 面在谈到原型的问题时也提到过,原型中蕴含的援用值会在所有实例间共享,这也是为什么属性通常会 在构造函数中定义而不会定义在原型上的起因。在应用原型实现继承时,原型实际上变成了另一个类型 的实例。这意味着原先的实例属性摇身一变成为了原型属性。

    function Person() {this.hobby = ['唱', '跳'];
    }

    function Student() {}

    Student.prototype = new Person();

    const stu1 = new Student();
    console.log(stu1.hobby); // ['唱', '跳']
    stu1.hobby.push('rapper');
    console.log(stu1.hobby); // ['唱', '跳', 'rapper']

    const stu2 = new Student();
    console.log(stu2.hobby); // ['唱', '跳', 'rapper']

2. 盗用构造函数

为了解决原型蕴含援用值导致的继承问题,一种叫作“盗用构造函数”(constructor stealing)的技 术在开发社区流行起来 (这种技术有时也称作“对象假装”或“经典继承”)。基本思路很简略: 在子类 构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简略对象,所以能够应用 apply() 和 call()办法以新创建的对象为上下文执行构造函数。

     function Person() {this.hobby = ['唱', '跳'];
    }

    function Student() {Person.call(this);
    }

    const stu1 = new Student();
    console.log(stu1.hobby); // ['唱', '跳']
    stu1.hobby.push('rapper');
    console.log(stu1.hobby); // ['唱', '跳', 'rapper']

    const stu2 = new Student();
    console.log(stu2.hobby); // ['唱', '跳']

长处:相比于应用原型链,盗用构造函数的一个长处就是能够在子类构造函数中向父类构造函数传参。

毛病:
盗用构造函数的次要毛病,也是应用构造函数模式自定义类型的问题: 必须在构造函数中定义方法,因而函数不能重用。此外,子类也不能拜访父类原型上定义的办法,因而所有类型只能应用构造函数模式。因为存在这些问题,盗用构造函数基本上也不能独自应用。

3. 组合继承

组合继承 (有时候也叫伪经典继承) 综合了原型链和盗用构造函数,将两者的长处集中了起来。基 本的思路是应用原型链继承原型上的属性和办法,而通过盗用构造函数继承实例属性。这样既能够把方 法定义在原型上以实现重用,又能够让每个实例都有本人的属性。

    function Person(name, age) {
        this.name = name;
        this.age = age;
        this.hobby = ['唱', '跳'];
    }
    Person.prototype.sayName = function (){console.log(` 我是 ${this.name}`);
    }

    function Student({name, age}) {Person.call(this, name, age);
    }

    Student.prototype = new Person();

    const stu1 = new Student({name:'张三', age: 16});
    console.log(stu1.hobby); // ['唱', '跳']
    stu1.hobby.push('rapper');
    console.log(stu1.hobby); // ['唱', '跳', 'rapper']
    stu1.sayName(); // 我是张三


    const stu2 = new Student({name:'李四', age: 24});
    console.log(stu2.hobby); // ['唱', '跳', 'rapper']
    stu2.sayName(); // 我是李四

组合继承补救了原型链和盗用构造函数的有余,是 JavaScript 中应用最多的继承模式。而且组合继 承也保留了 instanceof 操作符和 isPrototypeOf()办法辨认合成对象的能力。

4. 原型式继承

原型式继承实用于这种状况: 你有一个对象,想在它的根底上再创立一个新对象。8 你须要把这个对象先传给 object(),而后再对返回的对象进行适当批改。

function object(o) {function F() {}
        F.prototype = o;
        return new F();}


    let person = {
        name: "张三",
        friends: ["唱", "跳"]
    };
    let anotherPerson = object(person);
    anotherPerson.name = "李四";
    anotherPerson.friends.push("rapper");
    let yetAnotherPerson = object(person);
    yetAnotherPerson.name = "王二麻子";
    yetAnotherPerson.friends.push("篮球");
    console.log(person.friends); // ['唱', '跳', 'rapper', '篮球']

js 新增的 Object.create()与这里的 object()办法成果雷同。
原型式继承非常适合不须要独自创立构造函数,但依然须要在对象间共享信息的场合。但要记住,
属性中蕴含的援用值始终会在相干对象间共享,跟应用原型模式是一样的。

5. 寄生式继承

与原型式继承比拟靠近的一种继承形式是寄生式继承(parasitic inheritance),也是 Crockford 首倡的 一种模式。寄生式继承背地的思路相似于寄生构造函数和工厂模式: 创立一个实现继承的函数,以某种 形式加强对象,而后返回这个对象。根本的寄生继承模式如下:

  function object(o) {function F() {}
        F.prototype = o;
        return new F();}

    function createObject(original) {let clone = object(original); // 通过调用函数创立一个新对象
        clone.sayName = function () { // 以某种形式加强这个对象
            console.log(this.name);
        };
        return clone; // 返回这个对象
    }

    let person = {name: "张三",};
    let anotherPerson = createObject(person);
    anotherPerson.sayName();  // 张三

寄生式继承同样适宜次要关注对象,而不在乎类型和构造函数的场景。object()函数不是寄生式 继承所必须的,任何返回新对象的函数都能够在这里应用。
毛病:通过寄生式继承给对象增加函数会导致函数难以重用,与构造函数模式相似。

6. 寄生式组合继承

组合继承其实也存在效率问题。最次要的效率问题就是父类构造函数始终会被调用两次: 一次在是 创立子类原型时调用,另一次是在子类构造函数中调用。实质上,子类原型最终是要蕴含超类对象的所 有实例属性,子类构造函数只有在执行时重写本人的原型就行了。
寄生式组合继承通过盗用构造函数继承属性,但应用混合式原型链继承办法。基本思路是不通过调用父类构造函数给子类原型赋值,而是获得父类原型的一个正本。说到底就是应用寄生式继承来继承父 类原型,而后将返回的新对象赋值给子类原型。

 function object(o) {function F() {}
        F.prototype = o;
        return new F();}

    function inheritPrototype(child, parent) {let prototype = object(parent.prototype); // 创建对象
        prototype.constructor = child; // 加强对象
        child.prototype = prototype; // 赋值对象
    }


    function Person(name, age) {
        this.name = name;
        this.age = age;
        this.hobby = ['唱', '跳'];
    }
    Person.prototype.sayName = function (){console.log(` 我是 ${this.name}`);
    }

    function Student({name, age}) {Person.call(this, name, age);
    }

    inheritPrototype(Student, Person);


    Student.prototype.sayAge = function (){console.log(` 我往年 ${this.age}`);
    }


    const stu1 = new Student({name:'张三', age: 16});
    console.log(stu1.hobby); // ['唱', '跳']
    stu1.hobby.push('rapper');
    console.log(stu1.hobby); // ['唱', '跳', 'rapper']
    stu1.sayName(); // 我是张三
    stu1.sayAge(); // 我往年 16


    const stu2 = new Student({name:'李四', age: 24});
    console.log(stu2.hobby); // ['唱', '跳', 'rapper']
    stu2.sayName(); // 我是李四
    stu2.sayAge(); // 我往年 24
   

这个 inheritPrototype()函数实现了寄生式组合继承的外围逻辑。这个函数接管两个参数: 子 类构造函数和父类构造函数。在这个函数外部,第一步是创立父类原型的一个正本。而后,给返回的 prototype 对象设置 constructor 属性,解决因为重写原型导致默认 constructor 失落的问题。最初将新创建的对象赋值给子类型的原型。
这里只调用了一次 Person 构造函数,防止了 Student.prototype 上不必要也用不到的属性,因而能够说这个例子的效率更高。而且,原型链依然放弃不变,因而 instanceof 操作符和 isPrototypeOf()办法失常无效。寄生式组合继承能够算是援用类型继承的最佳模式。

class 是 es6 新增的定义,其实能够了解成一种语法糖,实质上就是一个函数。

class Person {}

console.log(Person); // class Person {}
console.log(typeof Person); // function

类的形成
类能够蕴含构造函数办法、实例办法、获取函数、设置函数和动态类办法,但这些都不是必须的。

    class Person {constructor(name) {console.log('constructor');
            this.name = name;
        }

        sayAge(){console.log('age....')
        }

        static sayHi() {console.log('sayHi');
        }
    }
  • 类的继承

      class Person {constructor({name, age}) {
              this.name = name;
              this.age = age;
          }
    
          sayName() {console.log(` 我是 ${this.name}`)
          }
      }
    
    
      class Student extends Person{constructor(props) {super(props);
              this.grade = props.grade;
          }
      }
    
      const stu1 = new Student({name: '张三', age: 14, grade:'一年级'});
    
      console.log(stu1.name); // 张三
      console.log(stu1.grade); // 一年级
      stu1.sayName(); // 我是张三

    尽管类继承应用的是新语法 extends,但背地仍旧应用的是原型链。

正文完
 0