重温JavaScript(lesson13):面向对象(6)

大家好,我们一起重温JavaScript。上一次我们一起重温了原型上定义引用数据类型以及使用字面量形式定义原型属性的相关内容,我们来一张导图回顾一下上次的内容吧:图片

如果你忘记了,请点击链接回顾上一次的内容:重温JavaScript(lesson12):面向对象(5)

这一次我们要一起学习继承啦~是不是有点小激动呢?在JS中学习面向对象编程的第一步是学会创建对象,第二步是理解继承的概念。在像Java,C++这类的语言中,类是从其他类继承属性的。然而在JS中,继承可以发生在没有类的继承关继的对象之间。这种继承机制我们之前已经遇到了,就是通过原型对象。

1.回顾原型链和Object.prototype

JS内建的继承方法被称为原型对象链,又叫原型对象继承。原型对象的属性可以被对象实例访问。对象实例继承了原型对象的属性。原型对象也是一个对象,它也可以有自己的原型对象并继承属性。这就是原型对象链:对象继承其原型对象,而原型对象继承它的原型对象,依此类推。

所有的对象都继承自Object.prototype。以字面量形式定义的的对象,其[[prototype]]的值都被设为Object.prototype。看代码:

var person = {
  name: 'New_Name'
}
var personPrototype = Object.getPrototypeOf(person);
console.log(personPrototype === Object.prototype);
//true

如上代码验证了:以字面量形式定义的的对象,其[[prototype]]的值都被设为Object.prototype。那么Object.prototype上都有哪些优良品质(属性)可以被其他对象继承呢?我们以表格的形式列出:

属性含义
hasOwnProperty()检查是否存在一个给定名字的自有属性
propertyIsEnumerable()检查一个自有属性是否可以枚举
isPropertyOf()检查一个对象是否是另外一个对象的原型对象
valueOf()返回一个对象的值表达
toString()返回一个对象的字符串表达

这几个属性都是比较常用的,我们举几个例子来看看它们的用法:

1.1 toString方法

看代码:

var obj = new Object();
var objInfo = obj.toString();
console.log(objInfo);
//[object Object]

这段代码中,首先创建了一个对象,然后调用toString()方法,返回该对象的描述字符串。

toString()方法会在某些需要用字符串来表示对象的时候被JS内部调用。例如alert()的工作就需要用到这样的字符串。所以,如果我们把对象传递给一个alert()函数,toString()方法就会在后台被调用,看代码:

var obj = new Object();
alert(obj);
alert(obj.toString());

这段代码中,两个alert语句执行的效果是相同的,执行结果如下图所示:

图片

另外,字符串的连接操作也会使用字符串描述文本,如果我们将某个对象与字符串进行连接,那么该对象就先调用自身的toString()方法

var obj = new Object();
var res = obj + "hello";
console.log(res);
//[object Object]hello

1.2 valueOf()方法

当一个操作符被用于一个对象时就会调用valueOf()方法。对于简单对象(以Objec()为构造器的对象)来说,valueOf()方法所返回的就是对象自己。我们看代码:

var obj = new Object();
console.log(obj.valueOf() === obj);
//true

原始封装类型重写了valueOf()方法,所以对于String它返回的是一个字符串,对于Boolean它返回的是一个布尔值,对于Number返回一个数值,对于Array返回数组,对于Date对象返回一个epoch时间。我们通过代码看几个例子:

var arr = [1,'a',2,'b'];
console.log(arr.valueOf());
//[1, "a", 2, "b"]
var now = new Date();
var last = new Date(2019,12,31);
console.log(now>last);
//true

第一段代码验证了valueOf()方法返回的就是对象自己,第二段代码表明当操作符被用于对象时就会调用valueOf()方法。

以上我们学到了toString和valueOf的调用时机,我们在一些书上或者一些面试题上会看到这样的内容:true + 1,true + "1", 1 + "1" , [1] + 1 ,就是给你几个表达式,问你执行结果。那么我们就需要知道toString和valueOf调用的顺序和优先级的问题。

原理是这样的:一旦valueOf()返回的是一个引用而不是原始值的时候,就会回退调用toString方法。另外,JS期望一个字符串,就会对原始值隐式调用toString()。我们看一些例子:

console.log("1"+1);
//11
console.log("1"+true);
//1true
console.log(undefined + "1");
//undefined1
console.log(null + "1");
//null1

这段代码表明:当加号操作符的一边是一个字符串时,另一边会自动转换为一个字符串

再来看例子:

let person = {
  name: "New_Name"
}
let res = "person:" + person;
console.log(res);
//person:[object Object]

这段代码中也是操作符被用作对象的情况,首先调用person对象的valueOf()方法,由于valueOf()方法返回的是对象本身,是一个引用,所以接着调用toString()方法返回[object Object]并和前面的字符串做拼接。所以还是当加号操作符的一边是一个字符串时,另一边会自动转换为一个字符串

另外需要注意的是,我们可以根据需要改变对象的toString()方法。例如:

let person = {
  name: "New_Name",
  toString: function() {
    return "[My name is " + this.name + "]"
  }
}
let res = "person:" + person;
console.log(res);
//person:[My name is New_Name]

1.3 对Object.prototype的修改

由于所有对象都默认继承自Object.prototype,所以改变Object.prototype会影响所有的对象,牵一发而动全身啊。所以这种操作是具有危险性的。添加了某个方法会导致所有对象都具有这个方法,例如:

let person = {
  name: "New_Name",
  toString: function() {
    return "[My name is " + this.name + "]"
  }
}
Object.prototype.younger = function() {
  console.log("我18岁");
}
person.younger();
//我18岁
let book = {
  titile: "JavaScript学习指南"
}
book.younger();
//我18岁

一本书来了一句“我18岁”,是不是有点儿滑稽啊?所以记住:最好不要修改Object.prototype。

在回顾了原型链和Object.ptototype的基础之上,我们再来看继承就是不是那么难了,我们先从ES5中继承说起。

2.ES5实现继承的几种方式

首先我们看一下使用原型链继承,这里有一部分其实是我们之前学习过的内容了,不过在这里我们再一次的强化和补充一下。

2.1原型链继承

我们看一些通过原型链方式继承的例子:

function Shape()  {
  this.color =  ["red", "green" , "blue"];
}
function Circle() {}
Circle.prototype = new Shape();
var circle = new Circle();
circle.color.push("yellow");
console.log(circle.color);
//["red", "green", "blue", "yellow"]
var circle2 = new Circle();
console.log(circle2.color);
//["red", "green", "blue", "yellow"]

这个例子中的 Shape构造函数定义了一个 colors 属性,该属性包含一个数组(引用类型值)。Circle的每个实例都会有各自包含自己数组的 colors 属性。当 Circle通过原型链继承了Shape之后, Circle.prototype 就变成了 Shape的一个实例,因此它也拥有了一个它自己的 colors 属性。就跟专门创建了一个 Circle.prototype.colors 属性一样。结果是 Circle 的所有实例都会共享这一个 colors 属性。而我们对 circle.colors 的修改能够通过circle2.colors 反映出来,就已经充分证实了这一点。

原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。但是需要注意不同的子类拥有不同的原型:

function Shape()  {
  this.color =  ["red", "green" , "blue"];
}
function Circle() {}
Circle.prototype = new Shape();
function Square() {}
Square.prototype = new Shape();
var square = new Square();
var circle = new Circle();
circle.color.push("yellow");
square.color.push("purple");
console.log(circle.color);
//["red", "green", "blue", "yellow"]
console.log(square.color);
//["red", "green", "blue", "purple"]
console.log(Circle.prototype)
console.log(Square.prototype)

如上代码中,最后两句代码的输出如下图:

图片

2.2通过构造函数继承

在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数的技术(也叫做伪造对象或经典继承):在子类型构造函数的内部调用超类型构造函数。因此通过使用 apply()和 call()方法也可以在(将来)新创建的对象上执行构造函数,如下所示:

function Shape(){
  this.colors = ["red", "blue", "green"];
  }
function Circle(){
  Shape.call(this);
}
var circle = new Circle();
circle.colors.push("yellow");
console.log(circle.colors); 
//["red", "blue", "green", "yellow"]
var circle2 = new Circle();
console.log(circle2.colors); 
//["red", "blue", "green"]

在这段代码中新创建的 Circle实例的环境下调用了 Shape构造函数。就会在新 Circle对象上执行 Shape()函数中定义的所有对象初始化代码。结果Circle的每个实例就都会具有自己的 colors 属性的副本了。

相对于原型链而言,借用构造函数可以在子类型构造函数中向超类型构造函数传递参数。看下面这个例子:

function Person(name) {
  this.name = name;
}
function Programmer() {
  Person.call(this, "New_Name");
  this.age = 18;
}
var programmer = new Programmer();
console.log(programmer.name); 
//New_Name
console.log(programmer.age);
//18

以上代码中的 Person只接受一个参数 name,在 Programmer构造函数内部调用 Person构造函数时,实际上是为 Programmer的实例设置了 name 属性。为了确保Person构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性

借用构造函数存在的问题是:如果方法都在构造函数中定义,那么就很难做到函数复用了,如下所示:

function Person(name) {
  this.name = name;
  this.sayName = function() {
    console.log(this.name);
  }
}
function Programmer() {
  Person.call(this, "New_Name");
  this.age = 18;
  this.sayAge = function(){
    console.log(this.age);
  }
}
function Doctor() {
  Person.call(this, "华佗在世");
  this.age = 188;
  this.sayAge = function(){
    console.log(this.age);
  }
}
var programmer = new Programmer();
programmer.sayName();
//New_Name
programmer.sayAge();
//18
var doctor = new Doctor();
doctor.sayName();
//华佗在世
doctor.sayAge();
//118

在这段代码中,在 Programmer构造函数内部调用 Person构造函数后,又定义了age属性,紧接着定义了sayAge()方法。同样地,在Doctor构造函数内部调用 Person构造函数后,又定义了age属性,紧接着定义了sayAge()方法。Programmer和Doctor的sayAge()方法又一样的函数体,可是却进行了重复的定义。

通过构造函数继承在超类型的原型中定义的方法,对子类型而言也是不可见的,如下例所示:

function Person(name) {
  this.name = name;
  this.sayName = function() {
    console.log(this.name);
  }
}
Person.prototype.eatBreakfast = function() {
  console.log("吃早饭啦");
}
function Programmer() {
  Person.call(this, "New_Name");
  this.age = 18;
  this.sayAge = function(){
    console.log(this.age);
  }
}
var programmer = new Programmer();
programmer.eatBreakfast();
//报错:programmer.eatBreakfast is not a function

在这段代码中,在Person的原型上定义了eatBreakfast()方法,可是对于其子类Programmer,eatBreakfast()方法却是不可见的。

2.3组合继承

组合继承也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

function Shape(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
Shape.prototype.sayName = function () {
  console.log(this.name);
};
function Cricle(name, radius) {
  //继承属性
  Shape.call(this, name);
  this.radius = radius;
}
//继承方法
Cricle.prototype = new Shape();
Cricle.prototype.constructor = Cricle;
Cricle.prototype.sayRadius = function () {
  console.log(this.radius);
};
var circle = new Cricle("棒棒糖", 3);
circle.colors.push("black");
console.log(circle.colors); 
// ["red", "blue", "green", "black"]
circle.sayName();
//棒棒糖
circle.sayRadius(); 
//3
var circle2 = new Cricle("甜甜圈", 10);
console.log(circle2.colors); 
// ["red", "blue", "green"]
circle2.sayName(); 
//甜甜圈
circle2.sayRadius(); 
//10

在这个例子中, Shape 构造函数定义了两个属性:name 和 colors。Shape的原型定义了一个方法 sayName()。Cricle构造函数在调用 Shape构造函数时传入了 name 参数,紧接着又定义了它自己的属性 age。然后,将 Shape的实例赋值给 Cricle的原型,然后又在该新原型上定义了方法 sayAge()。这样一来,就可以让两个不同的 Cricle实例既分别拥有自己属性,包括 colors 属性,又可以使用相同的方法了。
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且, instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象。

2.4动态原型模式

动态原型模式把所有信息都封装在了构造函数中,而通过在构造函数 中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。我们看一段代码:

function Person(name, age, work) {
  this.name = name;
  this.age = age;
  this.work = work;
  if (typeof this.sayName != "function") {
    Person.prototype.sayName = function () {
      console.log(this.name);
    };
  }
}
var person1 = new Person("New_Name", 18, "programmer");
person1.sayName();
//New_Name

2.5寄生构造函数模式

这种模式的基本思想是创建一个函数,封装创建对象的代码,然后再返回新创建的对象。我们看代码:

function Person(name, age, work) {
  var obj = new Object();
  obj.name = name;
  obj.age = age;
  obj.work = work;
  obj.sayName = function () {
    console.log(this.name);
  };
  return obj;
}
var person = new Person("New_Name", 18, "programmer");
person.sayName(); 
//New_Name

如上代码中,Person 函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返 回了这个对象。

关于继承的方式,在《JavaScript高级程序设计(第三版)》中有较为详细的介绍,还有工厂模式、稳妥构造函数模式等,我们就不一一介绍了,感兴趣的同学可以查询相关书籍和资料做进一步的研究和学习。我们这次的分享就到这里,在下一次分享中,我们将重点学习ES6实现继承的方式。

如有错误,请不吝指正。温故而知新,欢迎和我一起重温旧知识,攀登新台阶~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

重温新知

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值