第7章 面向对象与原型

1. 理解原型

const device = {
    powerSource: "battery",
    displayDevice: true
}
const phone = { size: "small" }
const huaweiPhone = { os: "HarmonyOS" }

// Object.setPrototypeOf需要两个对象作为参数,
// 它将第二个对象设置为第一个对象的原型
Object.setPrototypeOf(phone, device);
// 可以直接访问原型的属性
console.log(phone.powerSource);
// battery

Object.setPrototypeOf(huaweiPhone, phone);
// 也可以直接访问原型的原型的属性,即原型是链状的
// 访问属性时自下而上层层查找
console.log(huaweiPhone.displayDevice);
// true

// 用in操作符判断某个对象是否具有某个属性
console.log("size" in huaweiPhone);
// true

2. 对象构造器与原型

  • 每一个函数都有一个原型对象,该原型对象将被自动设置为通过该函数创建的对象的原型

2.1 实例与原型

function Students(){}

// 在对象的原型对象上添加属性
Students.prototype.study = function() {
    return true;
}

// 通过new作为构造函数调用,新建实例
const stu1 = new Students();
// 实例拥有了原型对象的属性
console.log("study" in stu1);
// true

// 直接调用函数
const stu2 = Students();
console.log("study" in stu2);
// Uncaught TypeError: Cannot use 'in' operator to search for 'study' in undefined
  • 每个函数都具有一个原型对象
  • 每个函数的原型都有一个constructor属性,该属性指向函数本身
graph LR; Students.prototype.constructor-->Students
  • constructor对象的原型设置为新创建的对象的原型
graph LR; stu1-.new.->Students.prototype.constructor.prototype=Students.prototype

2.2 实例属性:初始化过程的优先级

// 在实际代码中不推荐这么做
function Student() {
    this.gender = "Male";
    // 实例方法与原型方法同名
    this.sayHi = function() {
        return "Hi!";
    }
}

// 原型方法与实例方法同名
Student.prototype.sayHi = function() {
    return "I refuse to.";
}

const stu = new Student();

// 实例会隐藏(并不是替换)原型中与实例方法重名的方法
// 在构造函数内部,关键字this指向新创建的对象
// 所以在构造函数内添加的属性直接在新的实例上
// 查找时是先查找实例内部再查找原型中
console.log(stu.sayHi());
// Hi!

在函数的原型上创建对象方法可以使一个方法由所有对象实例共享

2.3 JS动态特性的副作用:通过原型,一切都可以在运行时修改

function Student(){
    this.type = "Primary";
}

const a1 = new Student();

// 在原型上添加方法
Student.prototype.getInfo = function(){
    return this.type;
}

// 实例可以正常访问
console.log(a1.getInfo());
// Primary

// 将原型指向另一个对象,并在对象里定义方法
Student.prototype = {
    getName: function() {
        return "Classified";
    }
}

// 之前实例化的对象依旧可以访问原来的方法
console.log(a1.getInfo());
// Primary

// 但不能使用新对象的方法
console.log(a1.getName());
// Uncaught TypeError: a1.getName is not a function

// 重新实例化一个对象
const a2 = new Student();

// 可以使用新方法了
console.log(a2.getName());
// Classified

// 但不能使用之前原型上的方法
console.log(a2.getInfo());
// Uncaught TypeError: a2.getInfo is not a function

对象与函数原型之间的引用关系是在对象创建时建立的,原型对象指向的改变并不会影响改变前就已经实例化的对象,即函数的原型可以被任意替换,但已经构建的实例引用旧的原型

2.4 通过构造函数实现对象类型

  • 通过constructor属性访问创建该对象所用的函数,这个特性可以用于类型校验
function Student(){}

const a1 = new Student();

// 通过typeof只能得知a1是个对象
console.log(typeof a1);
// object

// 通过instanceof只能得知a1是Student的实例
console.log(a1 instanceof Student);
// true

// 通过对象的constructor属性可以得到其构造函数引用
console.log(a1.constructor === Student);
// true

// 所以可以用constructor属性来创建对象
const a2 = new a1.constructor();

console.log(a2 instanceof Student);
// true

可以用constructor属性创建对象,即使原始构造函数已经不再作用域内。但如果重写了constructor属性,那么原始值就会丢失了

3. 实现继承

3.1 子类的原型是父类的实例实现继承

function People() {}

People.prototype.dance = function() {
    console.log("Dancing");
}

function Student() {}

// 子类的原型是父类的实例
Student.prototype = new People();

const a1 = new Student();

// 可以调用
a1.dance();
// Dancing        
console.log(a1 instanceof Student);
// true        
console.log(a1 instanceof Object);        
// true
console.log(a1 instanceof People);        
// true

使用People的原型对象作为Student的原型,即
People.prototype = Student.prototype
也可以实现继承,但强烈不建议使用。

缺点:People原型上发生的所有变化都被同步到Student原型上

优点:所有继承函数的原型将实时更新

3.2 重写constructor属性的问题

原型的重写导致constructor指向父类

function People() {}

People.prototype.dance = function() {
    console.log("Dancing");
}

function Student() {}

// 子类的原型是父类的实例,但也重写了原型对象,导致后面的问题
Student.prototype = new People();

const a1 = new Student();

// a1的constructor属性指向了父类
console.log(a1.constructor === People);
// true
console.log(a1.constructor === Student);
// false

配置对象的属性

属性描述 作用
configurable true: 可以删除或修改
false: 不允许修改
enumerable true: 可以被for-in遍历到
value 指定属性的值,默认为undefined
writable true: 可以通过赋值语句修改属性值
get 默认为undefined
定义getter函数,当访问属性时发生调用
不能与value和writable同时使用
set 默认为undefined
定义setter函数,当设置属性时发生调用
不能与value和writable同时使用
const book = {}
book.name = "Ninja";
book.price = 99.00;

// 内置方法配置属性,接收三个参数:目标对象,属性,配置(对象表示)
Object.defineProperty(book, "content", {
    configurable: false,
    enumerable: false,
    value: "Classified",
    writable: true
});

// 属性成功添加,可以访问
console.log("content" in book);
// true

// content属性的enumerable设置为false,所以for-in无法遍历
for(let prop in book) {
    console.log(prop);
}
// name 
// price

解决constructor属性被覆盖的问题

function People() { }

People.prototype.dance = function () {
    console.log("Dancing");
}

function Student() { }

// 子类的原型是父类的实例,但也重写了原型对象,导致constructor属性被覆盖
Student.prototype = new People();

// 注意要配置的constructor属性是在原型上的
Object.defineProperty(Student.prototype, "constructor", {
    enumerable: false,
    value: Student      // 让constructor重新指向原来的构造器
});

const a1 = new Student();

console.log(a1.constructor === People);
// false
console.log(a1.constructor === Student);
// true    -> constructor指向正确

3.3 instanceof操作符:基于原型链的检测

function Student() { }

const a1 = new Student();

// 在原型链中
console.log(a1 instanceof Student);
// true

// 修改原型
Student.prototype = {};

// a1已经不在Student的原型链中
console.log(a1 instanceof Student);
// false

4. 在ES6中使用JS的class

// class关键字定义类
class People{
    // 定义构造函数
    constructor(name) {
        this.name = name;
    }
    // 定义实例方法
    toString() {
        return this.name;
    }
}

// extends关键字实现继承
class Student extends People{
    constructor(name, age) {
        // super关键字用于访问和调用父类上的静态方法
        // 在构造函数中出现时,必须在使用this关键字之前使用
        super(name);    // 调用父类的构造函数
        this.age = age;
    }

    sayHi() {
        return "Hi!";
    }

    // static关键字定义静态方法
    static compare(stu1, stu2) {
        return stu1.age - stu2.age;
    }
}

const stu1 = new Student("Wango", 24);

// 实例化对象有效
console.log(stu1 instanceof Student);
// true

// 继承有效
console.log(stu1 instanceof People);
// true

const stu2 = new Student("Lily", 25);

// 调用静态方法有效
console.log(Student.compare(stu1, stu2));
// -1

// 调用父类方法有效
console.log(stu1.toString());
// Wango
console.log(stu2.toString());
// Lily

ES6引入关键字class,但是底层依然是基于原型实现,以上代码基本等价于一下代码,但要注意下列代码中原型继承的缺陷

function People(name) {
    this.name = name;
}
People.prototype.toString = function() {
    return this.name;
}

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

Student.prototype.sayHi = function() {
    return "Hi!";
}

Student.compare = function(stu1, stu2) {
    return stu1.age - stu2.age;
}

// 使用原型继承的一大缺陷:
// 向父类传递的参数会成为所有子类实例的初始化数据
Student.prototype = new People("Wango");

Object.defineProperty(Student.prototype, "constructor", {
    enumerable: false,
    value: Student
});


const stu1 = new Student(24);

// 实例化对象有效
console.log(stu1 instanceof Student);
// true

// 继承有效
console.log(stu1 instanceof People);
// true

const stu2 = new Student(25);

// 调用静态方法有效
console.log(Student.compare(stu1, stu2));
// -1

// 调用父类方法
console.log(stu1.toString());
// Wango
console.log(stu2.toString());
// Wango
// 给父类的参数成了初始化数据,原型继承有缺陷,还有其他几种继承方式本书暂未介绍
原文地址:https://www.cnblogs.com/hycstar/p/14028077.html