JavaScript继承

方式一:原型链的继承

js
复制代码
// 父类 function Person() {} // 子类 function Student(){} // 继承 Student.prototype = new Person()

我们只要把子类的prototype设置为父类的实例,就完成了继承,怎么样? 是不是超级简单? 有没有比Java,C#的清晰? 事实上,以上就是js里面的原型链继承

当然,通过以上代码,我们的Student只是继承了一个空壳的Person,这样视乎是毫无意义的,我们使用继承的目的, 就是要通过继承获取父类的内容,那我们先给父类加上一点点简单的内容(新增的地方标记 '// 新增的代码'):

js
复制代码
// 父类 function Person(name,age) { this.name = name || 'unknow' // 新增的代码 this.age = age || 0 // 新增的代码 } // 子类 function Student(name){ this.name = name // 新增的代码 this.score = 80 // 新增的代码 } // 继承 Student.prototype = new Person()

使用

js
复制代码
var stu = new Student('lucy') console.log(stu.name) // lucy --子类覆盖父类的属性 console.log(stu.age) // 0 --父类的属性 console.log(stu.score) // 80 --子类自己的属性

这里为了降低复杂度,我们只演示了普通属性的继承,没有演示方法的继承,事实上,方法的继承也很简单, 我们再来稍微修改一下代码,基于上面的代码,给父类和子类分别加一个方法

js
复制代码
// 父类 function Person(name, age) { this.name = name || "unknow"; this.age = age || 0; } // 为父类新曾一个方法 Person.prototype.say = function () { console.log("I am a person"); }; // 子类 function Student(name) { this.name = name; this.score = 80; } // 使用父类的实例对象来重写子类的原型对象 // 继承 注意,继承必须要写在子类方法定义的前面 Student.prototype = new Person("zgc", 18); // 修复子类原型对象上constructor 的指向 Student.prototype.constructor = Student; // 为子类新增一个方法(在继承之后,否则会被重写的原型覆盖) Student.prototype.study = function () { console.log("I am studing"); };

使用

js
复制代码
var stu = new Student("lucy"); console.log(stu.name); // lucy --子类覆盖父类的属性 console.log(stu.age); // 18 --父类的属性 console.log(stu.score); // 80 --子类自己的属性 stu.say(); // I am a person --继承自父类的方法 stu.study(); // I am studing --子类自己的方法

这样,看起来我们好像已经完成了一个完整的继承了,这个就是原型链继承,怎么样,是不是很好理解? 但是,原型链继承有一个缺点,来自原型对象引用属性会被所有实例共享,请看以下代码

js
复制代码
// 父类 function Person(name, age) { this.name = name || 'unknow' this.age = age || 0 this.hobbies = ['music', 'reading'] } // 子类 function Student(name) { this.name = name this.score = 80 } // 继承 Student.prototype = new Person() var stu1 = new Student('zgc') //修改stu1的name属性,stu2的name不会改变 var stu2 = new Student('wf') stu1.hobbies.push('basketball') console.log(stu1.hobbies) // music,reading,basketball console.log(stu2.hobbies) // music,reading,basketball console.log(stu1.name === stu2.name); //true or false 不传参相等,传参根据参数判断

我们可以看到,当我们改变stu1的引用类型的属性时,stu2对应的属性,也会跟着更改,这就是原型链继承缺点 --来自原型链引用属性会被所有实例共享

特点

  • 共享了父类构造函数的方法和属性
  • 简单,易于实现

缺点

  • 继承的属性会被多个实例共享, 如果某个属性是引用类型, 那么这个属性在多个实例中互相影响
  • 创建子类实例时,无法向父类构造函数传参
  • 要想为子类新增属性和方法,必须要在Student.prototype = new Person() 之后执行,不能放到构造器中
  • 不可以实现多继承

例题

js
复制代码
function Parent(name) { this.name = name || '⽗亲'; // 实例基本属性 (该属性,强调私有,不共享) this.arr = [1]; // (该属性,强调私有) } Parent.prototype.say = function () { // -- 将需要复⽤、共享的⽅法定义在⽗类原型上 console.log('hello') } function Child(like) { this.like = like; } Child.prototype = new Parent() // 核⼼,但此时Child.prototype.constructor==Parent Child.prototype.constructor = Child // 修正constructor指向 let boy1 = new Child() let boy2 = new Child() // 优点:共享了⽗类构造函数的say⽅法 console.log(boy1.say(), boy2.say(), boy1.say === boy2.say); // hello , hello , true // 缺点1:不能向⽗类构造函数传参 console.log(boy1.name, boy2.name, boy1.name === boy2.name); // ⽗亲,⽗亲,true // 缺点2: ⼦类实例共享了⽗类构造函数的引⽤属性,⽐如arr属性 boy1.arr.push(2); // 修改了boy1的arr属性,boy2的arr属性,也会变化,因为两个实例的原型上(Child.prototype)有了⽗类构造函数的实例属性arr; 所以只要修改了boy1.arr,boy2.arr的属性也会变化。 console.log(boy2.arr); // [1,2] // 注意1:修改boy1的name属性,是不会影响到boy2.name。因为设置boy1.name相当于在⼦类实例新增了name属性。 // 注意2: console.log(boy1.constructor); // Parent 你会发现实例的构造函数居然是Parent。 // ⽽实际上,我们希望⼦类实例的构造函数是Child, 所以要记得修复构造函数指向。 // 修复方法如下:Child.prototype.constructor = Child;

方式二: 借用构造函数继承

这种方式关键在于:在子类型构造函数中通用call()调用父类型构造函数

js
复制代码
function Person(name, age) { this.name = name; this.age = age; this.setName = function () {}; } Person.prototype.setAge = function () {}; function Student(name, age, price) { Person.call(this, name, age); /* 等于下面两行代码 this.name = name this.age = age */ this.price = price; } var s1 = new Student("Tom", 20, 15000);

image.png

这种方式只是实现部分的继承,如果父类的原型还有方法和属性,子类是拿不到这些方法和属性的。

js
复制代码
console.log(s1.setAge())//Uncaught TypeError: s1.setAge is not a function

特点

  • 解决了原型链继承中子类实例共享父类引用属性的问题
  • 创建子类实例时,可以向父类传递参数
  • 可以实现多继承(call多个父类对象)

缺点

  • 实例并不是父类的实例,只是子类的实例
  • 只能继承父类的实例属性和方法,不能继承父类原型属性和方法
  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

例题

js
复制代码
function Parent(name) { this.name = name; // 实例基本属性 (该属性,强调私有,不共享) this.arr = [1]; // (该属性,强调私有) this.say = function () { // 实例引⽤属性 (该属性,强调复⽤,需要共享) console.log('hello') } } function Child(name, like) { Parent.call(this, name); // 核⼼ 拷⻉了⽗类的实例属性和⽅法 this.like = like; } let boy1 = new Child('⼩红', 'apple'); let boy2 = new Child('⼩明', 'orange '); // 优点1:可向⽗类构造函数传参 console.log(boy1.name, boy2.name); // ⼩红, ⼩明 // 优点2:不共享⽗类构造函数的引⽤属性 boy1.arr.push(2); console.log(boy1.arr, boy2.arr);// [1,2] [1] // 缺点1:⽅法不能复⽤ console.log(boy1.say === boy2.say) // false (说明,boy1和boy2的say⽅法是独⽴,不是共享的) // 缺点2:不能继承⽗类原型上的⽅法 Parent.prototype.walk = function () { // 在⽗类的原型对象上定义⼀个walk⽅法。 console.log('我会⾛路') } boy1.walk; // undefined (说明实例,不能获得⽗类原型上的⽅法)

方式三: 原型链+借用构造函数的组合继承

这种方式关键在于:

  • 普通属性 使用 借用构造函数继承(继承的属性和方法都在实例自身),
  • 方法函数 使用 原型链继承(继承的属性和方法都在实例的原型里面),
  • 通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。
js
复制代码
function Person(name, age) { this.name = name; this.age = age; this.setAge = function () {}; } Person.prototype.sayBy = function () { console.log("111"); }; function Student(name, age, price) { Person.call(this, name, age); //核心代码 this.price = price; this.setScore = function () {}; } Student.prototype = new Person(); // 核心代码 Student.prototype.constructor = Student; //组合继承也是需要修复构造函数指向的 Student.prototype.sayHello = function () {}; var s1 = new Student("Tom", 20, 15000); var s2 = new Student("Jack", 22, 14000); console.log(s1); console.log(s1.constructor); //Student

image.png

这种方式融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。不过也存在缺点

  • 就是无论在什么情况下,都会调用两次父类构造函数:
    • 一次是在创建子类型原型的时候调用,
    • 一次是在子类型构造函数的内部调用,

优点

  • 可以继承实例属性/方法,也可以继承原型属性/方法
  • 不存在引用属性共享问题
  • 可传参
  • 函数可复用

缺点

  • 调用了两次父类构造函数,子类型实例上会有两份父类型的属性和方法
  • 一份在子类实例自身, 一份在子类实例的原型对象中

例题

js
复制代码
function Parent(name) { this.name = name; // 实例基本属性 (该属性,强调私有,不共享) this.arr = [1]; // (该属性,强调私有) } Parent.prototype.say = function () { // --- 将需要复⽤、共享的⽅法定义在⽗类原型上 console.log('hello') } function Child(name, like) { Parent.call(this, name) // 核⼼ 第⼆次 this.like = like; } Child.prototype = new Parent() // 核⼼ 第⼀次 Child.prototype.constructor = Child // 修正constructor指向 let boy1 = new Child('⼩红', 'apple') let boy2 = new Child('⼩明', 'orange') // 优点1:可以向⽗类构造函数传参数 console.log(boy1.name, boy1.like); // ⼩红,apple // 优点2:可复⽤⽗类原型上的⽅法 console.log(boy1.say === boy2.say) // true // 优点3:不共享⽗类的引⽤属性,如arr属性 boy1.arr.push(2) console.log(boy1.arr, boy2.arr); // [1,2] [1] 可以看出没有共享arr属性。 // 缺点1:由于调⽤了2次⽗类的构造⽅法,会存在⼀份多余的⽗类实例属性

方式四: 原型式继承

核心:原型式继承的Object.create方法本质上是对参数对象的一个浅复制。

js
复制代码
var person = { name: "Nicholas", age: 18, friends: ["Shelby", "Court", "Van"], }; person.__proto__.sayHello = function () { console.log("Hello"); }; var anotherPerson = Object.create(person); anotherPerson.name = "Greg"; anotherPerson.age = 22; anotherPerson.friends.push("Rob"); var yetAnotherPerson = Object.create(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); console.log(person.friends); // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie'] console.log(person); // {name: 'Nicholas', age: 18, friends: Array(5)} anotherPerson.sayHello(); // Hello console.log(yetAnotherPerson);

优点

  • 子类可以继承父类和父类原型上的属性和方法

缺点

  • 父类的引用属性会被所有子类实例共享
  • 子类构建实例时不能向父类传递参数

拓展: 创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

js
复制代码
var obj = { name: "zgc", age: 18, }; // 1. var info1 = {}; info1.__proto__ = obj; // 2. function createObject(o) { const F = function () {}; F.prototype = o; return new F(); } var info2 = createObject(obj); // 3. Object.create(obj);

方式五: 寄生式继承

核心:使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。

js
复制代码
function createAnother(original, name) { var clone = Object.create(original); //通过调用函数创建一个新对象 clone.name = name clone.sayHi = function () { //以某种方式来增强这个对象 alert("hi"); }; return clone; //返回这个对象 } var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"], }; var anotherPerson = createAnother(person, 'zgc); anotherPerson.sayHi(); //"hi"

函数的主要作用是为构造函数新增属性和方法,以增强函数

缺点(同原型式继承):

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数

方式六: 寄生组合式继承

结合借用构造函数传递参数和寄生模式实现继承

js
复制代码
function Person(name, age) { this.name = name; this.age = age; this.say = function () {}; } Person.prototype.setAge = function () { console.log("111"); }; // Person.prototype.msg = "Hello"; function Student(name, age, price) { Person.call(this, name, age); // 子类继承父类自身的属性和方法 this.price = price; this.setScore = function () {}; } function inheritPrototype(subType, superType) { subType.prototype = Object.create(superType.prototype); //核心代码 subType.prototype.constructor = subType; //核心代码 } // Object.create(obj); 创建一个新对象,使用现有的对象来提供新创建的对象的__proto__ // subType.prototype.constructor = subType; 将子类原型对象的构造器指向自身构造函数 inheritPrototype(Student, Person); var s1 = new Student("Tom", 20, 15000); console.log(s1 instanceof Student, s1 instanceof Person); // true true console.log(s1.constructor); //Student console.log(s1); // 完美写法: inheritPrototype函数封装 // function createObject(o) { // const F = function () {}; // F.prototype = o; // return new F(); // } // function inheritPrototype(subType, superType) { // subType.prototype = createObject(superType.prototype); //核心代码 // Object.defineProperty(subType.prototype, "constructor", { // value: subType, // enumerable: false, // writable: true, // configurable: true, // }); // }

同样的,Student继承了所有的Person原型对象的属性和方法。目前来说,最完美的继承方法!

image.png

方式七:ES6中class 的继承

ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的。

js
复制代码
class Person { //调用类的构造方法 constructor(name, age) { this.name = name this.age = age } //定义一般的方法 showName() { console.log("调用父类的方法") console.log(this.name, this.age); } } let p1 = new Person('kobe', 39) console.log(p1) //定义一个子类 class Student extends Person { constructor(name, age, salary) { super(name, age)//通过super调用父类的构造方法 this.salary = salary } showName() {//在子类自身定义方法 console.log("调用子类的方法") console.log(this.name, this.age, this.salary); } } let s1 = new Student('wade', 38, 1000000000) console.log(s1) s1.showName()

优点

  • 语法简单易懂,操作更方便

缺点

  • 并不是所有的浏览器都支持class关键字
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP