Js 面向对象-继承实现

上篇讲了用 构造函数 + 原型 的方式, 通过 new 关键字去创建一个对象. 这个 new 关键字会做 4个事情:

  • 创建一个新对象 obj
  • 让 obj 的隐式原型 [[prototype]] 去指向其构造函数的 显示原型 prototype
  • 绑定 this 为创建的对象, 并执行构造函数代码
  • 若构造函数没有返回对象, 则自动返回刚创建的对象
// 构造函数
fuction Person() {}

var p1 = new Person()
var p2 = new Person()

console.log(p1.__proto__ === Person.prototype) // true
console.log(p1.__proto__ === Person.prototype) // true

// constructor 属性, 也是引用
cosole.log(Person.prototype.constructor === Person) // true

最为关键的就是第二步的理解, 涉及到了 原型, 我们将其类比理解为 父类 应该也是行的. 然后对于 js 函数的理解:

  • 所有对象都有内置的隐式原型 [[prototype]] 引用, 指向一个对象 (向上)
  • 函数(除箭头) 都有一个显示的 prototype 属性 (引用), 也指向一个对象 (向下)
  • 函数也是对象, 因此函数既有显示原型对象 和 隐式原型对象 (二者不等)

在 js 中, 上述的 Person() 被称为构造函数, 从面向对象的编程范式来看, Person 也可以称为 .

类继承

面相对象的三大特性: 封装, 继承, 多态, 本篇就是来学习在 js 中是如何实现继承的, 这个非常关键, 因为 js 更多是以脚本语言著称的, 不想 Java, C++ 这种内置就用, 直接就用, 因此 js 在实现面向对象特性则需要做更大的巧妙设计.

这里用 Java 来做一个类比:

// Animal.java
class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    public void speak() {
        System.out.println(name + " 发出声音。");
    }
}

// Dog.java 
class Dog extends Animal {
    public Dog(String name) {
        super(name); // 调用父类构造函数
    }

    @Override
    public void speak() {
        System.out.println(name + " 汪汪叫。");
    }
}

// Main.java
public class Main {
    public static void main(String[] args) {
        Animal a = new Animal("动物");
        Dog d = new Dog("旺财");

        a.speak(); // 输出:动物 发出声音。
        d.speak(); // 输出:旺财 汪汪叫。
    }
}

这里的 Dog 类 通过 extends 关键字继承了父类 Animal, 然后它就天然能获取到父类的属性, 方法等, 同时也能进行拓展和重写.

这看上去似乎非常直观, 我们甚至觉得本就该如此, 但在 js 中要实现类似的效果, 则是非常曲折的过程, 也经历了好多年的发展呢.

Js 原型链

上篇再说对象原型的时候知道, 当从一个对象上获取属性时, 若当前对象没有, 则会从它的原型上去获取.

// 原型链查找
var obj = {
  name: "youge", 
  age: 18
}

// [[GET]] 操作
// 1. 在对象当前属性中查找
// 2. 若找不到, 则会从原型对象上去找 (__proto__)
// 3. 若还找不到, 则会继续沿着 obj.__proto__.__proto__.__proto__ ...查找
// 4. 这个查找的节点则构成了所谓的原型链, 知道顶层原型对象也没找到则返回 undefined

// 原型链 
console.log(obj.__proto__ === Object.prototype)  // true, 到顶啦
console.log(obj.__proto__.__proto__ === null) // true

console.log(obj.city) // undefined

// 在原型链条的任意节点添加上 city 属性都能被找到
obj.__proto__.city = '长安镇'

console.log(obj.city) // 长安镇

顶层原型

原型链的尽头就是 [Object: null prototype] {}. 它是一个 object 类型, 只是长得有点奇怪.

// 顶层原型
const obj = Object.create(null)

console.log(obj) // [Object: null prototype] {}
typeof obj // object

  • {}: 表示它是一个空的对象字面量, 没有自己的可枚举属性
  • [Object: null prototype] : 表示它的原型 [[prototype]] 是 null, 而非 Object.prototype

顶层原型的特殊在于它的原型是 null, 且该对象上有很多默认的属性和方法, 方便给后代们继承用的. 现在来理解一下这个它是如何产生的.

// 构造函数创建对象


function Person() {}
var p = new Person()

// new 会做4件事:
// 1. 创建一个新对象 p
// 2. this -> p; p.__proto__ -> Person.prototype
// 3. 执行 Person 函数
// 4. 若函数无返回对象, 则返回 p 对象

这里补充一下, 普通函数对象的隐式原型是内置的 Function 函数, 即它是所有函数的 "父类".

那对于普通对象来说, 它的隐式原型也是内置的 Object 函数, 它是所有类的 "父类".

// 普通对象的构造函是 Object() 函数

var obj1 = {}
var obj2 = new Object()

// new 同样会做4件事: 
// 1. 创建一个新对象 obj1
// 2. this -> obj1; obj1.__proto__ -> Object.prototype 
// ...

这里的 Object.prototype 对象, 就是顶层的 [Object: null prototype] {} 对象原型, 就到头了.

Object 和 Function

在 js 中, 它俩都是内置的函数, 函数也是特殊对象. Object 是所有类, 普通对象的 父类; Function 则是所有函数的 "父类", 二者有点像 概念.

// Object 和 Function 都是 js 内置的函数, 函数也是对象

console.log(typeof Object)  // function 
console.log(typeof Function) // function 

下面的代码如果都能理解, 则说明对于对象, 函数, 原型就理解得差不多啦.

var foo = new Function()
console.log(typeof foo) // function

var obj = new Object()
console.log(typeof obj)  // object 

// 作为对象, prototype 是给 其子类 共享的显示原型对象
console.log(foo.__proto__ === Function.prototype) // true
console.log(obj.__proto__ === Object.prototype) // true, 顶层原型
console.log(Function.prototype.__proto__ === Object.prototype) // true

console.log(Object.prototype.__proto__ === null) // true

// 但并非所有对象的 __proto__ 指向顶层原型, 比如函数就是指向 Function.prototype

// 作为函数, 所有函数的 __proto__ 都指向 Function, 包括它自身
console.log(Object.__proto__ === Function.prototype) // true
console.log(Function.__proto__ === Function.prototype)  // 自指, true

//
console.log(Function.prototype === Object.prototype) // false
console.log(Function.__proto__ === Object.prototype) // false
console.log(Object.__proto__ === Function.prototype) // true

  • Function 是所有函数的 "父类", 因此**任意函数的 __ proto __ 都指向 Function.prototype; **

  • Object 是函数; Function 也是函数 (自指)

  • Object 是部分对象(普通)的 "父类", 因此 存在特殊对象(函数)的 __ proto __ 不指向 Object.prototype;

  • 函数是特殊对象, 函数的 __ proto __ ! == Object.prototype 而是指向 Function.prototype

构造关系看, Function 和 Object 是 "平级" 的内部构造器

原型链 看, Function 的原型链, 包含了 Object.prototype

感觉还是 Function 要更厉害一些呀, 因为它是所有函数的爸爸, Object 也是函数, 但 Function 却只是特殊对象.

继承实现-by 原型链

前面铺垫了那么多关于函数, 对象, 原型, 原型链的东西, 最终目的就是来通过原型链来实现 js 的继承, 只要真正了解了 js 的继承实现, 那对于原型链和面向对象就完全拿捏了, 直接起飞.

// 不手动实现继承是不能直接用的, 毕竟没有关联吗

// 1. 父类, 公共的属性和方法
function Person() {
  this.name = 'youge'
}

// 公共方法添加到原型, 实现共享
Person.prototype.eating = function() {
  console.log(this.name + ' is eating')
}


// 2. 子类, 特殊属性和方法
function Student() {
  this.sno = 123
}

// 学生类, 也有给下面学生的共用方法, 加到原型
Student.prototype.studying = function() {
  console.log(this.name + " is studying")
}


// 学生实例 -> 学生类 -> 人类 
var stu = new Student()

console.log(stu.name)  // undefined
stu.eating() // 报错, 因为找不到, 为 null, 调用 null 不行的

这里继承的关键在于, 如何让 Student类的 实例对象 stu 能获取到 Person 类 的属性和方法.

  • Var p = new Person(), 则 p 对象是可以访问 Person 及其原型的, 且 p.__ proto __ === Person.prototype
  • Var s = new Student(), 则 s 对象是可以访问 Student 及其原型的, 且 s.__ proto __ === Stdent.prototype
  • s 要访问 p 的显示原型, 则可通过 Student.prototype = new Person() === p, 间接 访问 Person显示原型啦
// 2. 子类, 特殊属性和方法
function Student() {
  this.sno = 123
}

// 方法: 在创建 stu 之前, 将其默认原型对象 Student.prototype -> new Person()

// 因为: new Person() 的 [[prototype]] -> Person.prototype
// 所以: new Student() -> Student.prototype -> Person.prototype
Student.prototype = new Person()

// 学生类, 也有给下面学生的共用方法, 加到原型
Student.prototype.studying = function() {
  console.log(this.name + " is studying")
}

就是这个关键的一步指向Student.prototype = new Person() 就实现 s 访问 p显示实现, 即 S类 继承 P类.

注意:

千万不能写成 Studnet.prototype = Person.prototye, 这会让 S 和 P 共享原型那就乱伦啦!!!

放的位置一定要在子类 Student 添加原型方法之前, 不让就失效了.

这种通过原型链的巧妙设计就能实现类之间的继承, 别看只有一行代码, 要真能理解, 说明对象和原型确实是理解到为了的.

但是这种方式会存在几个较大的弊端:

  • 不能给父类的构造函数传参, 那有个屁用!
  • 原型上的引用会被共享, 无法互相隔离!
  • 父类构造函数被过早执行, 产生副作用!
// 原型链实现继承的弊端

// 1. 打印 stu 对象, 某些属性是看不到的
console.log(stu)  // Person { sno: 123 }, 继承的属性看不到

// 2. 创建出来两个 stu 对象
var stu1 = new Student()
var stu2 = new Student()

// 父类 Person 上的属性被继承后变不能互相独立
stu1.friends.push('cj') // stu2 也会受到影响

console.log(stu1.friends) // [ 'cj' ]
console.log(stu2.friends) // ['cj' ]

// 换个写法就能隔离, 即加在当前对象上, 而非原型
stu1.friends = ['aa', 'bb']

console.log(stu1.friends) // [ 'aa', 'bb' ]
console.log(stu2.friends) // ['cj' ]

// 3. 不好传参数
// 这个参数要在 Person 中处理比较麻烦
var stu3 = new Student('cjj')

继承实现-by 借用构造函数

用原型链实现继承的弊端在于, 无法传参, 原型引用会被共享, 父类构造函数重复执行, 实例对象属性不全等.

原型链实现: Student.prototype = new Person()

可以通过一种叫 constructor stealing 的技术来解决原型链继承的弊端. 首先来解决不能传参的问题.

// 2. 子类, 特殊属性和方法
function Student(name, age, friends, sno) {
  this.name = name 
  this.age = age 
  this.friends = friends 
  this.sno = 123
}

这样直接加参数在 子类 是不行的, 这里的 name, age, friends 应属于公共属性, 应该放在父类 Person 处理才好.

// name, age, friends 是共用的, 不能放 Student 写死
Person -> Student 
Person -> Teacher

关键一步, 将这个初始化放在父类 Person 执行, 则通过 call 的方式调用 Person() 此时 this 为 Student 呀

// by借用构造函数, 实现继承

// 1. 父类, 公共的属性和方法
function Person(name, age, friends) {
  // 在 Student 中执行 Person.call(this), 这个 this -> stu
  this.name = name 
  this.age = age 
  this.friends = friends
}

// 2. 子类, 特殊属性和方法
function Student(name, age, friends, sno) {
  // 通过 call 方式调用 Person, 此时 this 是 stu
  Person.call(this, name, age, friends, sno)
  this.sno = 123
}

关键就是这精妙一步呀:

// 在 Students 中调用父类 Person, 则 this 是 stu 
// 这样传递参数过去的时候, 这些参数是绑定在 stu = new Studnet 之上的
Person.call(this, name, age, friends, sno)

这样就巧妙解决了实例化对象时不好传参的弊端, 同时也解决了对象间数据隔离的问题和原型属性访问不到的问题.

// by借用构造函数, 实现继承

// 1. 父类, 公共的属性和方法
function Person(name, age, friends, sno) {
  // 在 Student 中执行 Person.call(this), 这个 this -> stu
  this.name = name 
  this.age = age 
  this.friends = friends
  this.sno = sno
}

// 2. 子类, 特殊属性和方法
function Student(name, age, friends, sno) {
  // 通过 call 方式调用 Person, 此时 this 是 stu
  Person.call(this, name, age, friends, sno)
}

Student.prototype = new Person()

// 公共方法添加到原型, 实现共享
Person.prototype.eating = function() {
  console.log(this.name + ' is eating')
}

// 学生类, 也有给下面学生的共用方法, 加到原型
Student.prototype.studying = function() {
  console.log(this.name + " is studying")
}

// 学生实例 -> 学生类 -> 人类 

// 1. 可以传参
var stu1 = new Student('cj', 30, ['yaya'], 111)
var stu2 = new Student('zs', 20, ['moni'], 123)

// 2. 互不影响, 加在了实例对象上
stu1.friends.push('jack')

// 3, 父类属性也可见
console.log(stu1)
console.log(stu2)

Person { name: 'cj', age: 30, friends: [ 'yaya', 'jack' ], sno: 111 }
Person { name: 'zs', age: 20, friends: [ 'moni' ], sno: 123 }
// 借用构造函数也有弊端: 

// 1. Person 函数至少被调用 2 次
// Student.prototype = new Person(); Person.call()  

// 2. stu 的原型对象 p = new Person(), 会多出来这些 name , age 等属性

相较于原型链方式的大弊端, 基于它之上的借用版本的弊端则算是不致命, 更多是浪费内存而已.

  • 父类会被重复调用: 之类原型对象指向 和 在子类中调用父类, 都会执行
  • 实例对象的原型对象上会多出一些 "已有" 的属性, 因为使用了借用吗, 有留痕的啦

那如果我们直接将 子类的原型赋值给父类的原型, 上面两个问题就解决啦, 但是坚决不能这样做的哈.

// 绝对不行的哦
Student.prototype = Person.prototype

这样子类和父类就共享一个原型对象了, 这样就乱伦了, 给子类原型加东西, 父类原型也会同步加, 这是不对的.

小结一下

通过上面的原型链 + 借用构造函数的方式已不断完善 js 类继承的方式, 做的是事情大致是这样:

  • Person 类 / 构造函数, 有自己的原型 [[prototype]] 对象
  • Student 类 / 构造函数, 有自己的原型 [[prototype]] 对象

为了要 Student 类 能够 继承 Person 类, 我们先创建一个新的对象, 比如叫 obj

  • 先让 obj 作为 Student 类的原型 (
  • 再让 Person 类, 作为 obj 对象的原型
// 创建对象
var stu = new Student()
var obj = new Person()

// new 内置实现
stu.__proto__ === Stuent.prototype
obj.__proto__ === Person.prototype

// 构造链条关系
Stuent.prototype = obj

// 于是: 
stu.__proto__.__proto__ === Person.prototype 

// 这样就 stu 就能访问整个链条上的属性啦 

继承实现-by 原型式继承函数

回顾 js 想要实现继承的目的是 重复利用另外一个对象的属性和方法

在 2006年, 一个前端大佬道格拉斯.克罗克福德 (JSON 的创立者) 写的一篇文章中提到了一种继承方法, 通过原型式函数来实现继承, 而非通过构造函数.

// 原型式继承函数 

var parentObj = {
  name: 'youge',
  age: 18,
  eating: function() {
    console.log(this.name + ' is eating')
  }
}

// 一个对象 A 实现继承的目的, 重复利用 B 对象的属性和方法

// 对于上面的 parentObj 对象来说, 我们现在要创建一个新对象 childObj
// 并实现让 childObj 的原型是 parentObj, 即 childObj.__proto__ === parentObj

// 实现这个 createChildObj 的函数: 
// 传入一个对象A, 返回一个对象 B, 让 B.__proto__ === A 即可

function createChildObj(parentObj) {
  var childObj = {}

  // 如何让 childObj 的原型 是 parentObj 呢?

  return childObj
}

关键就是如何来实现这个 createChildObj(parentObj) 函数. 我们之前学过, 创建对象可以通过 new 关键字实现

function Foo() {}

var obj = new Foo()

// new 会帮我实现 obj.__proto__ === Foo.prototye 

因此, 可先创建 Foo 构造函数, 先让它的原型设置为 parentObj, 然后再通过 Foo() 来 new 一个新对象 obj

那这样不就是实现了 obj 的原型是 parentObj 了嘛, 哇真的是有点高级呀!

function createChildObj(parentObj) {
  
  // 如何让 childObj 的原型 是 parentObj 呢?
  // 可利用 new 的特性!
  function Foo() {}

  Foo.prototype = parentObj 
  var childObj = new Foo() // childObj.__proto__ === Foo.protoytpe
  
  return childObj
}

太过于巧妙啦!!!

当然, 从 es5, es6 发展以来, 则有更直接的内置方法来实现 Object.setPrototypeOf()

function createChildObj2(parentObj) {
  var childObj = {}
  // 直接设置 childObj 对象的原型 为 parentObj 即可
  Object.setPrototypeOf(childObj, parentObj)

  return childObj
}

随着 ECMA 的发展, 还有内置更先进的方法 Object.create(obj) , 返回一个对象, 并将其原型设置为 obj

function createChildObj3(parentObj) {
  // 返回一个对象, 并设置对象的原型为 parentObj
  
  return Object.create(parentObj)
}

完整实例如下:

// 原型式继承函数 

var parentObj = {
  name: 'youge',
  age: 18,
  eating: function() {
    console.log(this.name + ' is eating')
  }
}

function createChildObj(parentObj) {
  function Foo() {}
  Foo.prototype = parentObj 
  var childObj = new Foo() 
  
  return childObj
}

function createChildObj2(parentObj) {
  var childObj = {}
  // 直接设置 childObj 对象的原型 为 parentObj 即可
  Object.setPrototypeOf(childObj, parentObj)
  return childObj
}

function createChildObj3(parentObj) {
  // 返回一个对象, 并设置对象的原型为 parentObj
  return Object.create(parentObj)
}


// 验证 
// var obj2 = createChildObj(parentObj)
// var obj2 = createChildObj2(parentObj)
var obj2 = createChildObj2(parentObj)

console.log(obj2) // {}

// { name: 'youge', age: 18, eating: [Function: eating] }
console.log(obj2.__proto__) 

console.log(obj2.name) // yoluge

原型式继承函数, 就是通过类似用 Object.cretate(obj) 的方式来实现 普通对象的继承.

var parentObj = {name: 'youge', age: 18}

// 内部: childObj.__proto__ -> childObj
var childObj = Object.create(parentObj)

// 这样 childObj 对象 就可以重复利用 parentObj 对象的属性方法啦, 从而实现继承

但我们最终想要实现的是, 通过构造函数的方式实现完美继承.

继承实现-by 寄生式继承函数

寄生式 (Parasitic) 继承也是上面这个大佬提出来的, 大致思路是结合 原型类继承+工厂模式 的方法, 即通过封装一个继承过程的函数, 在内部一某种很是来增强对象, 最后将这个对象返回, 但这种方法已经没有啥人用了的哈, 了解为主.

// 寄生式继承函数

var personObj = {
  name: "youge",
  running: function() {
    console.log(this.name + ' is running')
  }
}

function createStudent(name) {
  // 创建新对象, 并设置其原型为 personObj
  var stu = Object.create(personObj)

  // 然后针对这个 stu 对象进行额外增强
  stu.age = 18
  stu.studying = function() {
    console.log(name + ' isstudying...')
  }

  return stu
}

var stu1 = createStudent('cj')
var stu2 = createStudent('yaya')

console.log(stu2) // { age: 18, studying: [Function (anonymous)] }

这种方式的弊端和原型方式是一样的, 就不能区分类型, 方法被重复创建等. 虽然这种方式并不可取, 但是它依然提供了一个思路就是我们最终要搞一个工厂函数的方式, 并结合原型链方式实现最终方案

继承实现-by 寄生组合是继承函数

寄生组合式继承相当于是结合前面的东西, 最终完美实现继承的标准方案啦.

// 继承最佳方式: 寄生组合式继承

function Person(name, age, friends) {
  this.name = name 
  this.age = age 
  this.friends = friends
}

Person.prototype.running = function() {
  console.log('running~')
}

// 子类
function Student(name, age, friends, sno, score) {
  Person.call(this, name, age, friends)
  // 子类特有的
  this.sno = sno 
  this.score = score
}

// 要继承 Person, 创建一个对象, 让 Student.prototype -> obj -> Person
Student.prototype = Object.create(Person.prototype)

Student.prototype.studying = function() {
  console.log('studying~')
}

// 验证 
var stu = new Student('youge', 18, ['cj'], 123, 80)
console.log(stu)

stu.studying()
stu.running()

关键就是这句: Student.prototype = Object.create(Person.prototype)

这里: newObj = Object.create(obj); 会内置 newObj.__ proto __ === obj.prototype

Person {
  name: 'youge',
  age: 18,
  friends: [ 'cj' ],
  sno: 123,
  score: 80
}
studying~
running~

这里还有一个问题是类型不对, 这里是 Person, 但其实应该是 Student, 它是打印的时候拼接的, 自动找

// Person 
stu.constructor.name

// Student 原型对象改变了, 且这个新的对象, 没有 constructor, 
// 就会沿着原型链继续找, 然后从 Person 中找到了 constructor,  
// 但它的值是 Person

要解决这个问题也简单, 即将这个 construcotr 属性添加到 Student 新的原型对象中即可

Student.prototype.constructor = Student

当然可以用 Object.defineProperty() 添加得更精准控制一些哈:

Object.defineProperty(Student.prototype, 'constructor', {
  configurable: true,
  enumerable: false, // 不让被遍历
  writable: true, // 可以改
  value: Student  // 值设置为 Student, 作为类型
})

至于就是最佳继承方案啦, 寄生组合式继承. 但还是有能优化的地方, 可以将下面这这坨核心逻辑封装为工具函数:

// 核心逻辑
Student.prototype = Object.create(Person.prototype)
Object.defineProperty(Student.prototype, 'constructor', {
  configurable: true,
  enumerable: false, // 不让被遍历
  writable: true, // 可以改
  value: Student  // 值设置为 Student, 作为类型
})

封装这个继承的工具函数的思路:

  • 参数要传入一个子类, 一个父类
  • 无返回值
  • 内部将子类的原型对象, 去 指向 父类的原型对象
  • 子类添加上 constructor 属性, 指向之类, 修正类型
function inherit(SubType, SuperType) {
  // 修改子类原型对象 -> 父类显示原型对象
  SubType.prototype = Object.create(SuperType.prototype)
  // 子类添加 constructor 属性
  Object.defineProperty(SubType.prototype, "constructor", {
    configurable: true,
    enumerable: false,
    writable: true,
    value: SubType
  })
}

当然这里的 Object.create() 方法也是可以自己实现的, 上面也提到很多次啦!

function createChildObj(parentObj) {
  function Foo() {}
  Foo.prototype = parentObj
  return new Foo()
}

兜兜转转, 终于算是最终推演了这个 js 实现继承的最佳方案啦:

// 继承最佳方式: 寄生组合式继承

// 继承的工具函数 
function inherit(SubType, SuperType) {
  // 修改子类原型对象 -> 父类显示原型对象
  SubType.prototype = Object.create(SuperType.prototype)
  // 子类添加 constructor 属性
  Object.defineProperty(SubType.prototype, "constructor", {
    configurable: true,
    enumerable: false,
    writable: true,
    value: SubType
  })
}

// 案例-父类
function Person(name, age, friends) {
  this.name = name 
  this.age = age 
  this.friends = friends
}

Person.prototype.running = function() {
  console.log('running~')
}

// 案例-子类
function Student(name, age, friends, sno, score) {
  Person.call(this, name, age, friends)
  // 子类特有的
  this.sno = sno 
  this.score = score
}

// 让 Student 继承 Person
inherit(Student, Person)

Student.prototype.studying = function() {
  console.log('studying~')
}

// 验证 
var stu = new Student('youge', 18, ['cj'], 123, 80)
console.log(stu)

stu.studying()
stu.running()
 name: 'youge',
  age: 18,
  friends: [ 'cj' ],
  sno: 123,
  score: 80
}
studying~
running~

补充-等效的 class 语法糖

而这个实现的逻辑, 就是 ES6 中的 class 语法糖的等效实现:

// 等效 es6 的 class 语法糖

class Person {
  constructor(name, age, friends) {
    this.name = name 
    this.age = age 
    this.friends = friends
  }

  running() {
    console.log('running...')
  }
}

// 继承
class Student extends Person {
  constructor(name, age, friends, sno, score) {
    super(name, age, friends) // 对应 Person.call(this, name...)
    this.sno = sno
    this.score = score
  }

  studying() {
    console.log('studying...')
  }
}


// 验证 
var stu = new Student('youge', 18, ['cj'], 123, 80)
console.log(stu) // Student {...}

stu.studying() // studying...
stu.running() // running...

至此, 关于 js 实现继承的内容就差不多啦, 得出最佳的方式就是使用这个寄生组合式继承. 即将子类的显示原型对象, 去指向父类的显示原型对象, 然后再补充上 constructor 属性即可. 要理解这些实现方式, 背后要对 js 函数和对象的原型, 原型链深度理解, 这样就能轻松掌握啦.

posted @ 2025-08-16 13:44  致于数据科学家的小陈  阅读(7)  评论(0)    收藏  举报