JS的面向对象(理解对象,原型,原型链,继承,类)

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

JS的面向对象(理解对象,原型,原型链,继承,类)
文章图片

在自定义构造函数时,原型对象默认只会获得 constructor 属性,其他的所有方法都继承自 Object。每次调用构造函数创建一个新实例,这个实例的内部[[Prototype]]指针就会被赋值为构 造函数的原型对象。脚本中没有访问这个[[Prototype]]特性的标准方式,但 Firefox、Safari 和 Chrome 会在每个对象上暴露__proto__属性,通过这个属性可以访问对象的原型。在其他实现中,这个特性 完全被隐藏了。关键在于理解这一点:实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。
JS的面向对象(理解对象,原型,原型链,继承,类)
文章图片

注意:
  1. 构造函数、原型对象和实例是 3 个完全不同的对象
    console.log(person1 !== Person); // true console.log(person1 !== Person.prototype); // true console.log(Person.prototype !== Person); // true

  2. 【JS的面向对象(理解对象,原型,原型链,继承,类)】实例与构造函数没有直接联系,与原型对象有直接联系
    // 实例通过__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.原型链 JS的面向对象(理解对象,原型,原型链,继承,类)
文章图片

/** * 正常的原型链都会终止于 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")); // falseperson1.name = "李四"; console.log(person1.name); // "李四",来自实例 console.log(person1.hasOwnProperty("name")); // trueconsole.log(person2.name); // "张三",来自原型 console.log(person2.hasOwnProperty("name")); // falsedelete 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")); // falseperson1.name = "李四"; console.log(person1.name); // "李四",来自实例 console.log(person1.hasOwnProperty("name")); // true console.log("name" in person1); // trueconsole.log(person2.name); // "张三",来自原型 console.log(person2.hasOwnProperty("name")); // false console.log("name" in person1); // truedelete 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(); // 我今年16const 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,但背后依旧使用的是原型链。

    推荐阅读