2024最新版JavaScript逆向爬虫教程-------基础篇之面向对象

目录

  • 一、概念
  • 二、对象的创建和操作
    • [2.1 JavaScript创建对象的方式](#2.1 JavaScript创建对象的方式)
    • [2.2 对象属性操作的控制](#2.2 对象属性操作的控制)
    • [2.3 理解JavaScript创建对象](#2.3 理解JavaScript创建对象)
      • [2.3.1 工厂模式](#2.3.1 工厂模式)
      • [2.3.2 构造函数](#2.3.2 构造函数)
      • [2.3.3 原型+构造函数](#2.3.3 原型+构造函数)
  • 三、继承
    • [3.1 通过原型链实现继承](#3.1 通过原型链实现继承)
    • [3.2 借用构造函数实现继承](#3.2 借用构造函数实现继承)
    • [3.3 寄生组合式继承](#3.3 寄生组合式继承)
      • [3.3.1 对象的原型式继承](#3.3.1 对象的原型式继承)
      • [3.3.2 寄生组合式继承的实现](#3.3.2 寄生组合式继承的实现)
    • [3.4 对象的方法补充](#3.4 对象的方法补充)
    • [3.5 原型继承关系](#3.5 原型继承关系)

一、概念

① 面向对象是现实的抽象方式。 对象是 JavaScript 中一个非常重要的概念,这是因为对象可以将多个相关联的数据封装到一起,更好的描述一个事物:

  1. 比如我们可以描述一辆车:Car,具有颜色(color)、速度(speed)、品牌(brand)、价格(price),行驶(travel)等等;
  2. 比如我们可以描述一个人:Person,具有姓名(name)、年龄(age)、身高(height),吃东西(eat)、跑步(run)等等;

用对象来描述事物,更有利于我们将现实的事物,抽离成代码中某个数据结构,所以有一些编程语言就是纯面向对象的编程语言,例如:Java;你在实现任何现实抽象时都需要先创建一个类,根据类再去创建对象;

② JavaScript的面向对象。 虽然说在 JavaScript 编程语言中,函数是一等公民,但是 JavaScript 不仅支持函数式编程,也支持面向对象编程。JavaScript 对象被设计成了一组属性的无序集合,像是一个哈希表,由 key 和 value 组成,key 为一个标识符名称,而 value 可以是任意类型的值,也可以是其他对象或者函数类型,当函数作为对象的属性值时,这个函数就可以称之为对象的方法。

二、对象的创建和操作

2.1 JavaScript创建对象的方式

一般地,常用于创建对象的方式有两种,早期经常使用 Object 类,通过 new 关键字来创建一个对象,有点类似于 Java 中创建对象,后来为了方便就直接使用对象字面量的方式来创建对象了,这种形式看起来更加的简洁,并且对象和属性之间的内聚性也更强,所以这种方式后来就流行了起来。创建 Object 的一个新实例,然后再给它添加属性和方法,如下例所示:

javascript 复制代码
let person = new Object(); // 创建一个空对象
person.name = "Amo"; // 往对象中添加属性 
person.age = 18;
person.job = "Spider Engineer";
person.sayName = function() {
    console.log(this.name);
};

使用对象字面量则可以这样写:

javascript 复制代码
let person = {
    name: "Amo",
    age: 18,
    job: "Spider Engineer",
    // sayName(){  // es6后的写法
    //     console.log(this.name)
    // }
    // 常规写法
    sayName: function (){
        console.log(this.name)
    }
}

这个例子中的 person 对象跟前面例子中的 person 对象是等价的,它们的属性和方法都一样。这些属性都有自己的特征,而这些特征决定了它们在 JavaScript 中的行为。

2.2 对象属性操作的控制

在前面我们的属性都是直接定义在对象内部,或者直接添加到对象内部的,但是这样来做的时候我们就不能对这个属性进行一些限制:比如这个属性是否是可以通过 delete 删除?这个属性是否在 for-in 遍历的时候能被遍历出来?如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用 属性描述符。 通过属性描述符可以精准的添加或修改对象的属性;属性描述符需要使用 Object.defineProperty() 方法来对属性进行添加或者修改;

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。语法格式如下:

javascript 复制代码
Object.defineProperty(obj, prop, descriptor)
// ①:obj: 要定义属性的对象
// ②:prop: 一个字符串或 Symbol,指定了要定义或修改的属性键。
// ③:descriptor: 要定义或修改的属性的描述符。
// ④返回值: 传入函数的对象,其指定的属性已被添加或修改。

什么是 属性描述符? 顾名思义就是对对象中的属性进行描述,简单来说就是给对象某个属性指定一些规则。属性描述符主要分为数据属性描述符和访问器属性描述符两种类型。对于属性描述符中的属性是否两者都可以设置呢?其实数据属性和访问器属性描述符两者是有区别,下面的表格统计了两者可用和不可用的属性:

数据属性描述符和访问器属性描述符所担任的角色不一样,下面就来详细介绍一下,它们两者的区别。

① 数据属性描述符。 包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。从上表可以看出数据属性有4个特性描述它们的行为:

  1. configurable:表示是否可以通过 delete 删除对象属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符。当通过 new Object() 或者字面量的方式创建对象时,其中的属性的 configurable 默认为 true,当通过属性描述符定义一个属性时,其属性的 configurable 默认为 false。
  2. enumerable:表示是否可以通过 for-in 或者 Object.keys() 返回该属性。当通过 new Object() 或者字面量的方式创建对象时,其中的属性的 enumerable 默认为 true,当通过属性描述符定义一个属性时,其属性的 enumerable 默认为 false。
  3. writable:表示是否可以修改属性的值。当通过 new Object() 或者字面量的方式创建对象时,其中的属性的 writable 默认为 true,当通过属性描述符定义一个属性时,其属性的 writable 默认为 false。
  4. value:含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。这个特性的默认值为 undefined。

例子:

javascript 复制代码
const o = {
    name: "Amo"
}; // 创建一个新对象

// 通过 defineProperty 使用数据描述符添加对象属性的示例
Object.defineProperty(o, "age", {
    value: 37,// age属性的值,默认undefined
    writable: false,// age属性是否可以写入(修改),默认false
    enumerable: true, // age属性是否可以枚举,默认false
    configurable: true,//a 属性是否可以删除,默认false
});
// 'age' 属性存在于对象 o 中,其值为 37
console.log(o)  // { name: 'Amo', age: 37 }
console.log(Object.keys(o))
for(const key in o){
    console.log(key)
}
// 当writable为false,age属性的值是不可被修改的 严格模式下会报错
o.age = 18
console.log(o) //{ name: 'Amo', age: 37 }
delete o['age'] // 当configurable为true时,age属性是可被删除的
console.log(o) // { name: 'Amo' }

访问器属性不包含数据值。相反,它们包含一个 获取(getter)函数和一个设置(setter)函数,不过这两个函数不是必需的。在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效的值。在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。访问器属性同样有4个特性描述它们的行为:

  1. configurable:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。当通过访问器属性描述符定义一个属性时,其属性的 configurable 默认为 false。
  2. enumerable:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。当通过访问器属性描述符定义一个属性时,其属性的 enumerable 默认为 false。
  3. get:获取函数,在读取属性时调用。默认值为 undefined。
  4. set:设置函数,在写入属性时调用。默认值为 undefined。

访问器属性同样是不能直接定义的,必须使用 Object.defineProperty()。下面是一个例子:

javascript 复制代码
// 通过 defineProperty 使用访问器属性描述符添加对象属性的示例
o = {
    name: "Amo",
    _age: 38,
}
Object.defineProperty(o, "age", {
    enumerable: true,
    configurable: true,
    get: function () {
        console.log("age属性被访问了")
        return this._age
    },
    set: function (newValue) {
        console.log("age属性被设置了")
        this._age = newValue
    }
});
console.log(o.age); // age属性被访问了
o.age = 18 // age属性被设置了
console.log(o.age); // 18 age属性被访问了

get 和 set 的使用场景(规范):

  • 隐藏某一个私有属性,不希望直接被外界使用和赋值。如上面代码中的 _age 表示不想直接被外界使用,外界通过使用 age 的 set 和 get 才能来访问和设置 _age 了。
  • 如果希望截获某一个属性它访问和设置值的过程。

同时给多个属性定义属性描述符。 在一个对象上同时定义多个属性的可能性是非常大的。为此, ECMAScript 提供了 Object.defineProperties() 方法。这个方法可以通过多个描述符一次性定义多个属性。它接收两个参数:要为之添加或修改属性的对象和另一个描述符对象,其属性与要添加或修改的属性一一对应。比如:

js 复制代码
let book = {};
Object.defineProperties(book, {
    year_: {
        value: 2017,
    },

    edition: {
        value: 1
    },

    year: {
        get() {
            return this.year_;
        },

        set(newValue) {
            if (newValue > 2017) {
                this.year_ = newValue;
                this.edition += newValue - 2017;
            }
        }
    }
});

这段代码在 book 对象上定义了两个数据属性 year_edition,还有一个访问器属性 year。所有的属性都是同时定义的,并且数据属性的 configurable、enumerable 和 writable 特性值都是 false。

读取属性的特性。 使用 Object.getOwnPropertyDescriptor() 方法可以取得指定属性的属性描述符。这个方法接收两个参数:属性所在的对象和要取得其描述符的属性名。返回值是一个对象,对于访问器属性包含 configurable、enumerable、get 和 set 属性,对于数据属性包含 configurable、enumerable、writable 和 value 属性。比如:

javascript 复制代码
let book = {};
Object.defineProperties(book, {
    year_: {
        value: 2017
    },

    edition: {
        value: 1
    },

    year: {
        get: function () {
            return this.year_;
        },

        set: function (newValue) {
            if (newValue > 2017) {
                this.year_ = newValue;
                this.edition += newValue - 2017;
            }
        }
    }
});

let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value);          // 2017
console.log(descriptor.configurable);   // false
console.log(typeof descriptor.get);     // "undefined"
// let descriptor = Object.getOwnPropertyDescriptor(book, "year");
// console.log(descriptor.value);          // undefined
// console.log(descriptor.enumerable);     // false
// console.log(typeof descriptor.get);     // "function"

ECMAScript 2017 新增了 Object.getOwnPropertyDescriptors() 静态方法。这个方法实际上会在每个自有属性上调用 Object.getOwnPropertyDescriptor() 并在一个新对象中返回它们。对于上面的例子,使用这个静态方法会返回如下对象:

javascript 复制代码
let book = {};
Object.defineProperties(book, {
    year_: {
        value: 2017
    },

    edition: {
        value: 1
    },

    year: {
        get: function() {
            return this.year_;
        },

        set: function(newValue){
            if (newValue > 2017) {
                this.year_ = newValue;
                this.edition += newValue - 2017;
            }
        }
    }
});
console.log(Object.getOwnPropertyDescriptors(book));

运行结果如下图所示:

2.3 理解JavaScript创建对象

虽然使用 Object 构造函数或对象字面量可以方便地创建对象,但这些方式也有明显不足:创建具有同样接口的多个对象需要重复编写很多代码。在介绍 ES6 的类之前,本小节会循序渐进地介绍被类取代的那些底层概念。

2.3.1 工厂模式

工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。下面的例子展示了一种按照特定接口创建对象的方式:

javascript 复制代码
function createPerson(name, age, job) {
    let obj = {}  //创建一个空对象
    obj.name = name //设置对应属性值
    obj.age = age
    obj.job = job
    //公共方法
    obj.desc = function () {
        console.log(`我的名字是${this.name},今年芳龄${this.age},是一名${this.job}`)
    }
    //将对象返回
    return obj
}

let person1 = createPerson('amo', 18, 'teacher')
let person2 = createPerson('jerry', 38, 'doctor')
console.log(person1)
console.log(person2)
person1.desc()
person2.desc()

这里,函数 createPerson() 接收3个参数,根据这几个参数构建了一个包含 Person 信息的对象。可以用不同的参数多次调用这个函数,每次都会返回包含3个属性和1个方法的对象。这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)。

2.3.2 构造函数

工厂模式创建对象有一个比较大的问题:我们在打印对象时,对象的类型都是 Object 类型。但是从某些角度来说,这些对象应该有一个他们共同的类型,下面我们来看一下另外一种模式:构造函数的方式。

什么是构造函数?

  1. 构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数;
  2. 在其他面向对象的编程语言里面,构造函数是存在于类中的一个方法,称之为构造方法;
  3. 如果一个普通的函数被使用 new 操作符来调用了,那么就可以称这个函数为一个构造函数;
  4. 一般规定构造函数的函数名首字母大写;

new 操作符调用函数的作用,当一个函数被new操作符调用了,默认会进行如下几部操作:

text 复制代码
① 在内存中创建一个新的对象(空对象)
② 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性
③ 构造函数内部的this,会指向创建出来的新对象
④ 执行函数的内部代码(函数体代码)
⑤ 如果构造函数没有返回对象,则默认返回创建出来的新对象

示例代码:

javascript 复制代码
function Person(name, age) {
    this.name = name
    this.age = age

    this.sayHello = function () {
        console.log(`My name is ${this.name}, I'm ${this.age} years old.`)
    }
}

const p1 = new Person('curry', 30)
const p2 = new Person('kobe', 24)
console.log(p1) // Person { name: 'curry', age: 30, sayHello: [Function (anonymous)] }
console.log(p2) // Person { name: 'kobe', age: 24, sayHello: [Function (anonymous)] }

这个构造函数可以确保我们的对象是 Person 的类型的,美中不足的是我们需要为每个对象的函数去创建一个函数对象实例。

2.3.3 原型+构造函数

对象的原型: JavaScript 中每个对象都有一个特殊的内置属性 [[prototype]](我们称之为隐式原型),这个特殊的属性指向另外一个对象。那么这个属性有什么用呢?前面介绍了,当我们通过对象的 key 来获取对应的 value 时,会触发对象的 get 操作;首先,get 操作会先查看该对象自身是否有对应的属性,如果有就找到并返回其值;如果在对象自身没有找到该属性就会去对象的 [[prototype]] 这个内置属性中查找。

那么对象的 [[prototype]] 属性怎么获取呢?主要有两种方法:

  1. 通过对象的 __proto__ 属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题);
  2. 通过 Object.getPrototypeOf() 方法获取;

示例:

javascript 复制代码
const obj = {
    name: 'amo',
    age: 30
}

console.log(obj.__proto__)
console.log(Object.getPrototypeOf(obj))

函数的原型: 所有的函数都有一个 prototype 属性,并且只有函数才有这个属性。前面提到了 new 操作符是如何在内存中创建一个对象,并给我们返回创建出来的对象,其中第二步这个对象内部的 [[prototype]] 属性会被赋值为该构造函数的 prototype 属性。将代码与图结合,来看一下具体的过程。

javascript 复制代码
function Person(name, age) {
    this.name = name
    this.age = age
}

const p1 = new Person('amo', 18)
const p2 = new Person('jerry', 20)
// 验证:对象(p1\p2)内部的[[prototype]]属性(__proto__)会被赋值为该构造函数(Person)的prototype属性;
console.log(p1.__proto__ === Person.prototype) // true
console.log(p2.__proto__ === Person.prototype) // true
console.log(Person.prototype.constructor) //[Function: Person]

内存表现:

  1. p1 和 p2 的原型都指向 Person 函数的 prototype 原型;
  2. 其中还有一个 constructor 属性,默认原型上都会有这个属性,并且指向当前的函数对象;

结合对象和函数的原型,让所有的对象去共享这些函数,创建对象:

javascript 复制代码
function Person(name, age) {
    this.name = name
    this.age = age
}

Person.prototype.sayHello = function () {
    console.log(`My name is ${this.name}, I'm ${this.age} years old.`)
}

const p1 = new Person('amo', 18)
const p2 = new Person('jerry', 24)

console.log(p1.sayHello === p2.sayHello) // true

如果我们需要在原型上添加过多的属性,通常我们会重新整个原型对象:

前面我们说过,每创建一个函数,就会同时创建它的 prototype 对象,这个对象也会自动获取 constructor 属性;而我们这里相当于给 prototype 重新赋值了一个对象,那么这个新对象的 constructor 属性,会指向 Object 构造函数,而不是 Person 构造函数了。如果希望 constructor 指向 Person,那么可以手动添加,如下:

手动添加的方式固然可以,但是也会造成 constructor 的 [[Enumerable]] 特性被设置了 true。默认情况下,原生的 constructor 属性是不可枚举的,如果希望解决这个问题,就可以使用我们前面介绍的 Object.defineProperty() 函数了,如下:

三、继承

面向对象的三大特性:封装、继承和多态。在 二、对象的创建和操作 一节中我们简单的了解了封装的过程,也就是把对象的属性和方法封装到一个函数中,本小节讲一下 JavaScript 中继承的实现,继承是面向对象中非常重要的特性,它可以帮助我们提高代码的复用性。继承主要的思想就是将重复的代码逻辑抽取到父类中,子类只需要通过继承父类,就可以使用父类中的方法,但是在实现 JavaScript 继承之前,需要先了解一个重要的知识点 原型链 当我们从一个对象上获取属性时,如果在当前对象自身没有找到该属性的话,就会去它原型上面获取,如果原型中也没有找到就会去它原型的原型上找,沿着这么一条线进行查找,那么这条线就是我们所说的原型链了。示例代码:

对应的内存中的查找过程:

当通过原型链查找某个属性时,一直找不到的话会一直查找下去么?肯定是不会的,JavaScript 的原型链也是有尽头的,这个尽头就是 Object 的原型。

Object 的原型: 事实上,不管是对象还是函数,它们原型链的尽头都是 Object 的原型,也称之为顶层原型,我们可以打印看看这个顶层原型长什么样。node 环境中:

javascript 复制代码
console.log(Object.prototype) //[Object: null prototype] {}

在浏览器中:

Object.prototype 上有很多默认的属性和方法,像 toString、hasOwnProperty 等;如果我们再次打印 Object.prototype 的原型,这个原型属性已经指向了 null,如下:

当使用 new 操作符调用构造函数时,其对象的 [[prototype]] 会指向该构造函数的原型 prototype,其实 Object 也是一个构造函数,因为我们可以使用 new 操作符来调用它,创建一个空对象。

javascript 复制代码
const obj = new Object()

obj.name = 'amo'
obj.age = 30

console.log(obj.__proto__ === Object.prototype) // true
console.log(obj.__proto__) // [Object: null prototype] {}
console.log(obj.__proto__.__proto__) // null

内存表现:

从 Object 的原型可以得出一个结论 原型链最顶层的原型对象就是 Object 的原型对象, 这也就是为什么所有的对象都可以调用 toString 方法了,如下:

从继承的角度来讲就是 Object是所有类的父类。 更为复杂的原型链关系内存图:

3.1 通过原型链实现继承

定义一个父类 Person 和子类 Student;父类中存放公共的属性和方法供子类使用;核心:将父类的实例化对象赋值给子类的原型; 示例代码:

javascript 复制代码
// 定义Person父类公共的属性
function Person() {
    this.name = 'curry'
    this.age = 30
}

// 定义Person父类的公共方法
Person.prototype.say = function () {
    console.log('I am ' + this.name)
}

// 定义Student子类特有的属性
function Student() {
    this.sno = 101111
}

// 实现继承的核心:将父类的实例化对象赋值给子类的原型
Student.prototype = new Person()

// 定义Student子类特有的方法
Student.prototype.studying = function () {
    console.log(this.name + ' studying')
}

// 实例化Student
const stu = new Student()
console.log(stu.name) // curry
console.log(stu.age) // 30
console.log(stu.sno) // 101111
stu.say() // I am curry
stu.studying() // curry studying

内存表现:

缺点:

  1. 从内存表现图中就可以看出,当打印 stu 对象时,name 和 age 属性是看不到的,因为不会打印原型上的东西;
  2. 当父类中的属性为引用类型时,子类的多个实例对象会共用这个引用类型,如果进行修改,会相互影响;
  3. 在使用该方案实现继承时,属性都是写死的,不支持动态传入参数来定制化属性值;

3.2 借用构造函数实现继承

针对 3.1 通过原型链实现继承 的缺点,可以借用构造函数来进行优化。在子类中通过 call 调用父类,这样在实例化子类时,每个实例就可以创建自己单独属性了;示例代码:

javascript 复制代码
// 定义Person父类公共的属性
function Person(name, age) {
    this.name = name
    this.age = age
}

// 定义Person父类的公共方法
Person.prototype.say = function () {
    console.log('I am ' + this.name)
}

// 定义Student子类特有的属性
function Student(name, age, sno) {
    // 通过call调用Person父类,创建自己的name和age属性
    Person.call(this, name, age)
    this.sno = sno
}

// 实现继承的核心:将父类的实例化对象赋值给子类的原型
Student.prototype = new Person()

// 定义Student子类特有的方法
Student.prototype.studying = function () {
    console.log(this.name + ' studying')
}

// 实例化Student
const stu1 = new Student('curry', 30, 101111)
const stu2 = new Student('kobe', 24, 101112)
console.log(stu1) // Person { name: 'curry', age: 30, sno: 101111 }
console.log(stu2) // Person { name: 'kobe', age: 24, sno: 101112 }

内存表现:

缺点: 在实现继承的过程中,Person 构造函数被调用了两次,一次在 new Person(),一次在 Person.call();在 Person 的实例化对象上,也就是 stu1 和 stu2 的原型上,多出来了没有使用的属性 name 和 age;

3.3 寄生组合式继承

通过上面的两种方案,我们想实现继承的目的是重复利用另外一个对象的属性和方法,如果想解决方案二中的缺点,那么就要减少 Person 的调用次数,避免去执行 new Person(),而解决的办法就是可以新增一个对象,让该对象的原型指向 Person 的原型即可。

3.3.1 对象的原型式继承

将对象的原型指向构造函数的原型的过程就叫做对象的原型式继承,主要可以通过以下三种方式实现:

封装一个函数,将传入的对象赋值给构造函数的原型,最后将构造函数的实例化对象返回;

javascript 复制代码
function createObj(o) {
    // 定义一个Fn构造函数
    function Fn() {
    }

    // 将传入的对象赋值给Fn的原型
    Fn.prototype = o
    // 返回Fn的实例化对象
    return new Fn()
}

const protoObj = {
    name: 'curry',
    age: 30
}

const obj = createObj(protoObj) // 得到的obj对象的原型已经指向了protoObj
console.log(obj.name) // curry
console.log(obj.age) // 30
console.log(obj.__proto__ === protoObj) // true

改变上面方法中的函数体实现,使用 Object.setPrototypeOf() 方法来实现,该方法设置一个指定的对象的原型到另一个对象或null;

javascript 复制代码
function createObj(o) {
    // 定义一个空对象
    const newObj = {}
    // 将传入的对象赋值给该空对象的原型
    Object.setPrototypeOf(newObj, o)
    // 返回该空对象
    return newObj
}

直接使用 Object.create() 方法,该方法可以创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__

javascript 复制代码
const protoObj = {
    name: 'curry',
    age: 30
}

const obj = Object.create(protoObj)
console.log(obj.name) // curry
console.log(obj.age) // 30
console.log(obj.__proto__ === protoObj) // true

3.3.2 寄生组合式继承的实现

寄生式继承就是将对象的原型式继承和工厂模式进行结合,即封装一个函数来实现继承的过程。而这样结合起来实现的继承,又可以称之为寄生组合式继承。下面就看看具体的实现过程吧。

  1. 创建一个原型指向 Person 父类的对象,将其赋值给 Student 子类的原型;

  2. 在上面的实现方案中,Student 子类的实例对象的类型都是 Person,可以通过重新定义 constructor 来优化;

    javascript 复制代码
    // 定义Person父类公共的属性
    function Person(name, age) {
        this.name = name
        this.age = age
    }
    
    // 定义Person父类的公共方法
    Person.prototype.say = function () {
        console.log('I am ' + this.name)
    }
    
    // 定义Student子类特有的属性
    function Student(name, age, sno) {
        // 通过call调用Person父类,创建自己的name和age属性
        Person.call(this, name, age)
        this.sno = sno
    }
    
    // 调用Object.create方法生成一个原型指向Person原型的对象,并将这个对象赋值给Student的原型
    Student.prototype = Object.create(Person.prototype)
    // 定义Student原型上constructor的值为Student
    Object.defineProperty(Student.prototype, 'constructor', {
        configurable: true,
        enumerable: false,
        writable: true,
        value: Student
    })
    
    // 定义Student子类特有的方法
    Student.prototype.studying = function () {
        console.log(this.name + ' studying')
    }
    
    // 实例化Student
    const stu1 = new Student('curry', 30, 101111)
    const stu2 = new Student('kobe', 24, 101112)
    console.log(stu1) // Student { name: 'curry', age: 30, sno: 101111 }
    console.log(stu2) // Student { name: 'kobe', age: 24, sno: 101112 }

内存表现:

总结:多个地方用到了继承,可以将上面的核心代码赋值在一个函数里面,如果不想用 Object.create(),也可以使用上面封装的 createObj 函数;

javascript 复制代码
function createObj(o) {
    function Fn() {
    }

    Fn.prototype = o
    return new Fn()
}

/**
 * @param {function} SubClass
 * @param {function} SuperClass
 */
function inherit(SubClass, SuperClass) {
    SubClass.prototype = createObj(SuperClass.prototype)
    Object.defineProperty(SubClass.prototype, 'constructor', {
        configurable: true,
        enumerable: false,
        writable: true,
        value: SubClass
    })
}

寄生组合式实现继承的原理其实就是创建一个空对象用于存放子类原型上的方法,并且这个对象的原型指向父类的原型。

3.4 对象的方法补充

Object.hasOwnProperty(): 对象是否有某一个属于自己的属性(不是在原型上的属性)
in/for in 操作符: 判断某个属性是否在某个对象或者对象的原型上
instanceof: 用于检测构造函数的 pototype,是否出现在某个实例对象的原型链上
Object.isPrototypeOf(): 用于检测某个对象,是否出现在某个实例对象的原型链上

3.5 原型继承关系


相关推荐
前端百草阁13 分钟前
【TS简单上手,快速入门教程】————适合零基础
javascript·typescript
彭世瑜13 分钟前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
Backstroke fish15 分钟前
Token刷新机制
前端·javascript·vue.js·typescript·vue
zwjapple15 分钟前
typescript里面正则的使用
开发语言·javascript·正则表达式
小五Five16 分钟前
TypeScript项目中Axios的封装
开发语言·前端·javascript
临枫54117 分钟前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript
酷酷的威朗普18 分钟前
医院绩效考核系统
javascript·css·vue.js·typescript·node.js·echarts·html5
前端每日三省18 分钟前
面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们?
开发语言·前端·javascript
小刺猬_98518 分钟前
(超详细)数组方法 ——— splice( )
前端·javascript·typescript
契机再现19 分钟前
babel与AST
javascript·webpack·typescript