# JS继承系列介绍

看过文章的小伙伴应该都感觉的出,我比较喜欢针对每个知识点出一些比较细节的题目,然后将这些细节连串起来最后组合成大家最爱的综合题😄。

该系列主要为了让我们彻底理解JavaScript面向对象的三大特性:封装继承多态

"咦~这三大特性我知道啊,大清都完了你还在这谈"

啊~ 看到这里你先别着溜,开始的我也是和你一样觉得背背概念,写点小例子就懂了,直到自己给自己出了几道魔鬼题,我才发现之前对它们的理解还是不太全面...因此才有了本系列。

系列总目录:

  • 封装

    • ES6之前的封装-构造函数
    • ES6之后的封装-class
  • 继承

    • 原型链继承
    • 构造继承
    • 组合继承
    • 寄生组合继承
    • 原型式继承
    • class中的extends继承
  • 多态

(在开始写之前本想要一篇文章全部搞定的,但是发现字真太多了,所以才分开来写,而且我终于又可以用我最爱的绯红主题了 😄)

这一章节主要是想向大家介绍一下JS面向对象的第一大特性-封装,也是为了给后面最重要的继承打好基础。

题目也不太多,总共17道,算是牛刀小试吧。

通过阅读本章节你可以学习到:

  • ES6之前的封装-构造函数
  • ES6之后的封装-class

# 前期准备

先来理解一些最最最基本的概念:

(一)

// 1. 构造函数
function Cat (name) {
    this.name
}
// 2. 构造函数原型对象
Cat.prototype
// 3. 使用Cat构造函数创建的实例'乖乖'
var guaiguai = new Cat('guaiguai')
// 4. 构造函数的静态方法,名为fn
Cat.fn = function () {}
// 5. 原型对象上的方法,名为fn
Cat.prototype.fn = function () {}

(二)

语法糖的意思是现有技术本可以实现,但是采用某种写法会更加简洁优雅。

比如class就是语法糖。

(三)

原型链继承的思维导图

(这个暂时看不懂没关系,在继承那一章节中会讲到)

# 封装

把客观事物封装成抽象的类,隐藏属性和方法,仅对外公开接口。

# 1. ES6之前的封装

(虽然以下内容是概念部分,但是对你解题很有帮助哦,请务必仔细阅读它 😁)

都知道ES6class实际就是一个语法糖,那么在ES6之前,是没有类这个概念的,因此是借助于原型对象构造函数来实现。

  • 私有属性和方法:只能在构造函数内访问不能被外部所访问(在构造函数内使用var声明的属性)
  • 公有属性和方法(或实例方法):对象外可以访问到对象内的属性和方法(在构造函数内使用this设置,或者设置在构造函数原型对象上比如Cat.prototype.xxx)
  • 静态属性和方法:定义在构造函数上的方法(比如Cat.xxx),不需要实例就可以调用(例如Object.assign())

# 1.1 题目一

(理解私有属性方法公有属性方法)

比如我现在想要封装一个生产出猫,名为Cat的构造函数。

  • 由于猫的都是我们肉眼看不见的,所以我把它们设置为私有属性(隐藏起来)
  • 并且猫的心跳我们也是看不到的,所以我把它设置为私有方法(隐藏起来)
  • 然后猫的毛色是可以看见的,所以我把它设置为公有属性
  • 并且猫跳起来这个动作我们是看的到的,所以我把它设置为公有方法
function Cat (name, color) {
  var heart = '❤️'
  var stomach = '胃'
  var heartbeat = function () {
    console.log(heart + '跳')
  }

  this.name = name
  this.color = color

  this.jump = function () {
    heartbeat() // 能跳起来表明这只猫是活的,心也就能跳
    console.log('我跳起来了~来追我啊')
  }
}
var guaiguai = new Cat('guaiguai', 'white')
console.log(guaiguai)
guaiguai.jump()

上述代码打印出来的应该是:

Cat{ name: 'guaiguai', color: 'white', jump: function(){} }
❤️跳
我跳起来了~来追我啊

可以看到,我们生产出名字叫做乖乖的小猫咪只有这几个属性能访问到(也就是能被肉眼看到),为公有属性:

  • name
  • color
  • jump

而私有属性,是我们看不到的:

  • heart
  • somach
  • heartbeat

所以如果你想要直接使用它是不能够的:

// 私有
console.log(guaiguai.heart) // undefined
console.log(guaiguai.stomach) // undefined
guaiguai.heartbeat() // 报错

小结:

很好区分:

  • 在函数内用var定义的就是私有的
  • 在函数内用this承接的就是公有

# 1.2 题目二

(理解静态属性方法公有属性方法)

我们现在往刚刚的Cat构造函数中加些东西。

  • 我们需要对Cat这个构造函数加一个描述,表明它是用来生产猫的,所以我把descript设置为它的静态属性

  • 由于一听到猫这种动物就觉得它会卖萌,所以我把卖萌这个动作设置为它的静态方法

  • 由于猫都会用唾液清洁身体,所以我把清洁身体这个动作设置为它的公有方法

// 这段是旧代码
function Cat (name, color) {
  var heart = '❤️'
  var stomach = '胃'
  var heartbeat = function () {
    console.log(heart + '跳')
  }

  this.name = name
  this.color = color

  this.jump = function () {
    heartbeat() // 能跳起来表明这只猫是活的,心也就能跳
    console.log('我跳起来了~来追我啊')
  }
}
// 这段是新增的代码
Cat.descript = '我这个构造函数是用来生产出一只猫的'
Cat.actingCute = function () {
  console.log('一听到猫我就想到了它会卖萌')
}
Cat.prototype.cleanTheBody = function () {
  console.log('我会用唾液清洁身体')
}
var guaiguai = new Cat('guaiguai', 'white')

console.log(Cat.descript)
Cat.actingCute()
console.log(guaiguai.descript)
guaiguai.cleanTheBody()

上述代码打印出来的应该是:

'我这个构造函数是用来生产出一只猫的'
'一听到猫我就想到了它会卖萌'
undefined
'我会用唾液清洁身体'

可以看到,我们定义的descriptactingCute是定义在构造函数Cat上的,所以可以直接被Cat调用,为静态属性和方法。

但是descriptactingCute并不能存在于乖乖这个实例上,descript只是对构造函数Cat的描述,并不是对乖乖的描述,所以打印出undefined

不过清洁身体是定义在原型对象prototype中的,属于公有方法(实例方法),也就是乖乖这个实例可以用它来调用。

静态属性和方法:

  • descript
  • actingCute

实例(公有)属性和方法:

  • name
  • color
  • jump
  • cleanTheBody

小结:

也很好区分:

  • 在构造函数上也就是使用Cat.xxx定义的是静态属性和方法
  • 在构造函数内使用this设置,或者设置在构造函数原型对象上比如Cat.prototype.xxx,就是公有属性和方法(实例方法)

(也有小伙伴可能会有疑问,这个静态属性和方法是有什么用的啊,感觉我们编码的时候并没有用到过啊。Really? 哈哈 😄,Promise.all()、Promise.race()、Object.assign()、Array.form()这些不就是吗?)

(至于实例方法,想想push、shift,它们实际上不是存在于原型对象上的吗?Array.prototype.push

# 1.3 题目三

(理解实例自身的属性定义在构造函数原型对象中的属性的区别)

OK👌你刚刚既然已经说了使用this.xxx = xxx的方式和使用Cat.prototype.xxx = xxx都是属于实例对象guaiguai上的公有属性,那它们是有什么区别吗?

来看这道题我们就能理解它们的区别了:

function Cat (name) {
  this.name = name
}
Cat.prototype.prototypeProp = '我是构造函数原型对象上的属性'
Cat.prototype.cleanTheBody = function () {
  console.log('我会用唾液清洁身体')
}
var guaiguai = new Cat('guaiguai')
console.log(guaiguai)
console.log(guaiguai.name)
console.log(guaiguai.prototypeProp)
guaiguai.cleanTheBody()

这里输出的结果 🤔️?

Cat {name: "guaiguai"}
'guaiguai'
'我是构造函数原型对象上的属性'
'我会用唾液清洁身体'

看到没,name是使用this.xxx = xxx的形式定义的,它能直接让实例guaiguai就拥有这个属性。

prototypeProp 、cleanTheBody毕竟是定义在构造函数原型上的,所以并不能出现在实例guaiguai上,但是guaiguai却能访问和调用它们。

因此我们得出结论:

定义在构造函数原型对象上的属性和方法虽然不能直接表现在实例对象上,但是实例对象却可以访问或者调用它们

# 1.4 题目四

既然我们已经知道了实例自身的属性定义在构造函数原型对象中的属性的区别,那么我们一般是如何区分它们的呢?

来看看这里:

function Cat (name) {
  this.name = name
}
Cat.prototype.prototypeProp = '我是构造函数原型对象上的属性'
Cat.prototype.cleanTheBody = function () {
  console.log('我会用唾液清洁身体')
}
var guaiguai = new Cat('guaiguai')

for (key in guaiguai) {
  if (guaiguai.hasOwnProperty(key)) {
    console.log('我是自身属性', key)
  } else {
    console.log('我不是自身属性', key)
  }
}
console.log('-分隔符-')
console.log(Object.keys(guaiguai))
console.log(Object.getOwnPropertyNames(guaiguai))

这道题中,我分别用了三种方式来获取实例对象guaiguai上的属性名:

  • for...in...
  • Object.keys()
  • Object.getOwnPropertyNames()

输出的结果为:

'我是自身属性 name'
'我不是自身属性 prototypeProp'
'我不是自身属性 cleanTheBody'
'-分隔符-'
["name"]
["name"]

由此可以得出:

  • 使用for...in...能获取到实例对象自身的属性和原型链上的属性
  • 使用Object.keys()Object.getOwnPropertyNames()只能获取实例对象自身的属性
  • 可以通过.hasOwnProperty()方法传入属性名来判断一个属性是不是实例自身的属性

(上面👆的说法其实并不太严谨,因为要建立在可枚举属性的前提下(属性的enumerabletrue),不过这边我不发散下去了...)

# 1.5 题目五

下面让我们来做道题,看看你到底有没有掌握上面的知识点呢 😁。

function Person (name, sex) {
  this.name = name
  this.sex = sex
  var evil = '我很邪恶'
  var pickNose = function () {
    console.log('我会扣鼻子但不让你看见')
  }
  this.drawing = function (type) {
    console.log('我要画一幅' + type)
  }
}
Person.fight = function () {
  console.log('打架')
}
Person.prototype.wc = function () {
  console.log('我是个人我会wc')
}
var p1 = new Person('lindaidai', 'boy')
console.log(p1.name)
console.log(p1.evil)
p1.drawing('国画')
p1.pickNose()
p1.fight()
p1.wc()
Person.fight()
Person.wc()
console.log(Person.sex)

答案:

'lindaidai'
undefined
'我要画一幅国画'
Uncaught TypeError: p1.pickNose is not a function
Uncaught TypeError: p1.fight is not a function
'我是个人我会wc'
'打架'
Uncaught TypeError: Person.wc is not a function
undefined

解析:

  • name为公有属性,实例访问它打印出'lindaidai'
  • evil为私有属性,实例访问它打印出'undefined'
  • drawing是共有(实例)方法,实例调用它打印出'我要画一幅国画'
  • pickNose是私有方法,实例调用它会报错,因为它并不存在于实例上
  • fight是静态方法,实例调用它报错,因为它并不存在于实例上
  • wc存在于构造函数的原型对象中,使用实例调用它打印出'我是个人我会wc'
  • fight存在于构造函数上,使用构造函数调用它打印出'打架'
  • wc存在于构造函数的原型对象中,并不存在于构造函数中,所以报错
  • sex为公有(实例)属性,并不存在于构造函数上,使用构造函数访问它为undefined

这里大家可能会有一个疑惑点了,为什么最后一个Person.sex也会是undefined呢?

我明明已经这样写了:

function Person (sex) {
	this.sex = sex
}

看起来sex是定义在Person里的呀。

注意了,this.sex表示的是给使用构造函数创建的实例上增加属性sex,而不是给构造函数本身增加(只有Person.sex才是给构造函数上增加属性)。

# 1.6 题目六

如果我的构造函数和构造函数原型对象上存在相同名称的属性咋办呢 🤔️ ?

function Cat () {
  this.color = 'white'
  this.getColor = function () {
    console.log(this.color)
  }
}
Cat.prototype.color = 'black'
var cat = new Cat()
cat.getColor()

这里的执行结果为:

'white'

其实这个很好理解,你原型对象上虽然有一个名叫color的属性,但是我实例对象自己就也有一个啊,那我为什么要用你的呢?只有我自己没有,我才会到你那里去拿。

所以这也就引出了另一个经常听到的概念:

当访问一个对象的属性 / 方法时,它不仅仅在该对象上查找,还会查找该对象的原型,以及该对象的原型的原型,一层一层向上查找,直到找到一个名字匹配的属性 / 方法或到达原型链的末尾(null)。

也就是大名鼎鼎的原型链查找

咱要是没理解没关系哈,一起来看下面一个例子。

# 1.7 题目七

现在我在Cat的原型对象上,还有它原型对象的原型对象上都定义一个叫做color的属性。

(原型对象本质也是个对象,所以它的__proto__也就是Object.prototype)

function Cat () {
  this.color = 'white'
  this.getColor = function () {
    console.log(this.color)
  }
}
Cat.prototype.color = 'black'
Object.prototype.color = 'yellow'
Object.prototype.feature = 'cute'
var cat = new Cat()

cat.getColor()
console.log(cat)
console.log(cat.feature)

然后让我们来看看结果:

'white'
Cat {color: "white", getColor: ƒ}
'cute'

看到了不。

color这个属性还是以它自身的white为主,但是feature这个属性没在实例cat上吧,所以它就会向上层一层查找,结果在Object.prototype中找到了,因此打印出cute

整个过程就是这样:

# 1.8 题目八

"等会等会,让我缓一下"

"wc,我突然就想明白了很多事情!"

比如下面这种写法:

var obj = { name: 'obj' }
console.log(obj.toString())
console.log(obj.hasOwnProperty('name'))
console.log(Object.prototype)

为什么我的obj中明明就没有toString()、hasOwnProperty()方法,但是我却可以调用它。

现在我知道了,原来obj本质是个Object类型。

使用var obj = { name: 'obj' }就相当于是调用了new Object

var obj = new Object({ 'name': 'obj' })

这样的话,我当然就可以使用Object.prototype上的方法啦!

执行结果为:

(obj.toString()这里的结果为[object Object]应该都知道是什么原因吧?)

# 总结-构造函数

现在在来回头看看那句话:

把客观事物封装成抽象的类,隐藏属性和方法,仅对外公开接口

是不是好理解多了呢?

然后让我们对构造函数配合原型对象封装来做一个总结吧:

(一) 私有属性、公有属性、静态属性概念:

  • 私有属性和方法:只能在构造函数内访问不能被外部所访问(在构造函数内使用var声明的属性),见题1.1
  • 公有属性和方法(或实例方法):对象外可以访问到对象内的属性和方法(在构造函数内使用this设置,或者设置在构造函数原型对象上比如Cat.prototype.xxx),见题1.2
  • 静态属性和方法:定义在构造函数上的方法(比如Cat.xxx),不需要实例就可以调用(例如Object.assign())

(二) 实例对象上的属性和构造函数原型上的属性:

  • 定义在构造函数原型对象上的属性和方法虽然不能直接表现在实例对象上,但是实例对象却可以访问或者调用它们。(见题1.3)
  • 当访问一个对象的属性 / 方法时,它不仅仅在该对象上查找,还会查找该对象的原型,以及该对象的原型的原型,一层一层向上查找,直到找到一个名字匹配的属性 / 方法或到达原型链的末尾(null)。(见题1.7)

(三) 遍历实例对象属性的三种方法:

  • 使用for...in...能获取到实例对象自身的属性和原型链上的属性
  • 使用Object.keys()Object.getOwnPropertyNames()只能获取实例对象自身的属性
  • 可以通过.hasOwnProperty()方法传入属性名来判断一个属性是不是实例自身的属性

# 2. ES6之后的封装

ES6之后,新增了class 这个关键字。

它可以用来代替构造函数,达到创建“一类实例”的效果。

并且类的数据类型就是函数,所以用法上和构造函数很像,直接用new命令来配合它创建一个实例。

还有一件事你可能不知道吧,那就是,类的所有方法都定义在类的prototype属性上面

例如:

class Cat {
    constructor() {}
    toString () {}
    toValue () {}
}
// 等同于
function Cat () {}
Cat.prototype = {
    constructor() {}
    toString () {}
    toValue () {}
}

这个可以看下面👇的题目2.2来理解它。

# 2.1 题目一

现在我们将1.1的题目换成class版本的来看看。

class Cat {
  constructor (name, color) {
    var heart = '❤️'
    var stomach = '胃'
    var heartbeat = function () {
      console.log(heart + '跳')
    }

    this.name = name
    this.color = color
    this.jump = function () {
      heartbeat()
     	console.log('我跳起来了~来追我啊')
    }
  }
}
var guaiguai = new Cat('guaiguai', 'white')
console.log(guaiguai)
guaiguai.jump()

其实你会发现,当你使用class的时候,它会默认调用constructor这个函数,来接收一些参数,并构造出一个新的实例对象(this)并将它返回,因此它被称为constructor构造方法(函数)。

(另外,其实如果你的class没有定义constructor,也会隐式生成一个constructor方法)

可以看到,经过用class改造后的Cat

公有(实例)属性和方法:

  • name
  • color
  • jump

而对于私有属性,个人感觉上述的heart不应该叫做私有属性,它只不过被局限于constructor这个构造函数中,是这个作用域下的变量而已。

执行结果:

Cat{ name: 'guaiguai', color: 'white', jump: function () {} }
❤️跳
'我跳起来了~来追我啊'

# 2.2 题目二

(弄懂在类中定义属性或方法的几种方式)

class Cat {
  constructor () {
    var heart = '❤️'
    this.name = 'guaiguai'
    this.jump = function () {}
  }
  color = 'white'
  cleanTheBody = function () {
    console.log('我会用唾液清洁身体')
  }
  hideTheShit () {
    console.log('我在臭臭完之后会把它藏起来')
  }
}
var guaiguai = new Cat()
console.log(guaiguai)
console.log(Object.keys(guaiguai))
guaiguai.cleanTheBody()
guaiguai.hideTheShit()

请仔细看看这道题,在这里面我用了四种不同的方式来定义一些属性。

  1. constructorvar一个变量,它只存在于constructor这个构造函数中
  2. constructor中使用this定义的属性和方法会被定义到实例上
  3. class中使用=来定义一个属性和方法,效果与第二点相同,会被定义到实例上
  4. class中直接定义一个方法,会被添加到原型对象prototype

至此,这道题的答案为:

Cat {color: "white", name: "guaiguai", cleanTheBody: ƒ, jump: ƒ}
["color", "cleanTheBody", "name", "jump"]
'我会用唾液清洁身体'
'我在臭臭完之后会把它藏起来'

解析:

  • heart只能在constructor函数中使用,因此不会出现在实例上。
  • name、jump、color、cleanTheBody满足于上面👆说到的第二点和第三点
  • hideTheShit是在类里直接定义的,满足于上面👆说的第四点,因此它不会被Object.keys()获取到。
  • hideTheShit虽然是在原型对象中,但是也还是能被实例对象所调用,因此最后一段代码也会被执行'我在臭臭完之后会把它藏起来'

这四种定义的方式已经介绍完了 😁,相信大家比较迷惑的一点就是以下这两种方式的定义吧:

class Cat {
    cleanTheBody = function () {}
    hideTheShit () {}
}

看起来都是定义一个函数呀,为什么第一个就可以在实例对象中,而第二个是在原型对象中呢 🤔️ ?

其实不需要特意的去记住它,你只需要知道:在类的所有方法都定义在类的prototype属性上面

这里的cleanTheBody你可以理解为它和color一样只是一个普通的变量,只不过这个变量是个函数,所以它并不算是定义在类上的函数,因此不会存在于原型对象上。

hideTheShit是实实在在的定义在类上的方法,所以它和constructor方法一样,都是在类的原型对象上。

转化为伪代码就是:

class Cat {
    constructor() {}
    hideTheShit () {}
}
// 等同于
function Cat () {}
Cat.prototype = {
    constructor() {}
    hideTheShit () {}
}

# 2.3 题目三

(在class定义静态属性和方法)

前面我们给Cat定义静态属性和方法是采用这种方式,Cat.xxx

function Cat () {...}
Cat.descript = '我这个构造函数是用来生产出一只猫的'
Cat.actingCute = function () {
  console.log('一听到猫我就想到了它会卖萌')
}

class中你也可以使用Cat.xxx这种方式定义,因为前面说过了,class本质也是个对象。

但除此之外,你还可以使用static标识符表示它是一个静态的属性或者方法:

class Cat {
  static descript = '我这个类是用来生产出一只猫的'
  static actingCute () {
    console.log('一听到猫我就想到了它会卖萌')
  }
	// static actingCute = function () {} // 这种写法也是设置静态的方法
}

OK👌,现在让我们来做做下面这道题吧 😊:

class Cat {
  constructor (name, color) {
    var heart = '❤️'
    var stomach = '胃'
    var heartbeat = function () {
      console.log(heart + '跳')
    }
    this.name = name
    this.color = color
    heartbeat()
    this.jump = function () {
      console.log(this)
      console.log('我跳起来了~来追我啊')
    }
  }
  cleanTheBody = function () {
    console.log('我会用唾液清洁身体')
  }
  static descript = '我这个类是用来生产出一只猫的'
  static actingCute () {
    console.log(this)
    console.log('一听到猫我就想到了它会卖萌')
  }
}
Cat.staticName = 'staticName'
var guaiguai = new Cat('guaiguai', 'white')

console.log(guaiguai)
guaiguai.jump()
guaiguai.cleanTheBody()
console.log(guaiguai.descript)
guaiguai.actingCute()

Cat.actingCute()
console.log(Cat.descript)
console.log(Cat.staticName)

结果:

❤️跳
Cat{ name: 'guaiguai', color: 'white', jump: function(){}, cleanTheBody: function(){} }
Cat{ name: 'guaiguai', color: 'white', jump: function(){}, cleanTheBody: function(){} }
'我跳起来了~来追我啊'
'我会用唾液清洁身体'
undefined
Uncaught TypeError: guaiguai.actingCute is not a function

class Cat{...}
'一听到猫我就想到了它会卖萌'
'我这个类是用来生产出一只猫的'
'staticName'

结果分析:

  • 首先在构造guaiguai这个对象的时候会执行heartbeat方法,打印出❤️跳

  • 其次打印出的guaiguai它只会拥有class中定义的实例属性和方法,所以并不会有descriptactingCute

  • jump中的this指向的是实例对象guaiguai,并且执行了'我跳起来了~来追我啊'

  • 直接定义在class中的属性或者方法就相当于是定义在Cat.prototype上,所以也属于实例方法,cleanThebody会执行打印出'我会用唾液清洁身体'

  • 使用了static定义的属性和方法为静态属性和方法,并不存在于实例上,所以打印出undefined和报错

  • actingCute使用了static修饰符,所以它是静态方法,存在于Cat这个类上,因此它里面的this指向这个类,并且执行了'一听到猫我就想到了它会卖萌'

  • descript使用了static修饰符,所以它是静态属性,打印出'我这个类是用来生产出一只猫的'

  • Cat.staticName = 'staticName'就相当于定义了一个静态属性,所以打印出staticName

# 2.4 题目四

我们再来看看这道题,友情提示,这是个坑 🤮...

var a = new A()
function A () {}
console.log(a)

var b = new B()
class B {}
console.log(a)

你开始的预想是不是:

A{}
B{}

😁,结果却发现报错了:

A {}
Uncaught ReferenceError: Cannot access 'B' before initialization

那是因为,函数A是会被提升至作用域的最顶层,所以可以在定义函数A之前使用new A()

但是类却不存在这种提升机制,所以当你执行new B()的时候它就会告诉你在B没有初始化之前不能使用它。

尽管我们知道,class它的本质也是一个函数:

console.log(typeof B) // function

# 2.5 题目五

坑二 🤮...

class Cat {
  constructor () {
    this.name = 'guaiguai'
    var type = 'constructor'
  }
  type = 'class'
  getType = function () {
    console.log(this.type)
    console.log(type)
  }
}
var type = 'window'
var guaiguai = new Cat()
guaiguai.getType()

这里的执行结果是什么呢?

主要是考察了你对作用域以及class的理解。

答案为:

'class'
'window'

解析:

  • 调用getType函数的是guaiguai,所以里面的this指向了guaiguai,而guaiguai上的typeclass
  • 当要打印出type的时候,发现getType函数中并没有这个变量,所以就向外层查找,找到了window中存在这个变量,因此打印出window。(var type = 'constructor'是函数constructor中的变量)

# 2.6 题目六

既然做到了函数类型的题目,那怎么能不想到箭头函数呢?嘿嘿 。

阴笑~

让我们将2.5中的getType函数换成箭头函数看看?

class Cat {
  constructor () {
    this.name = 'guaiguai'
    var type = 'constructor'
  }
  type = 'class'
  getType = () => {
    console.log(this.type)
    console.log(type)
  }
}
var type = 'window'
var guaiguai = new Cat()
guaiguai.getType()
console.log(guaiguai)

现在调用guaiguai.getType()你觉得会是啥?

"既然箭头函数内的this是由外层作用域决定的,那这里外层作用域是window,当然this.type就是'window'咯"

咦~

还记得我之前说过的,class的本质是个函数吗?所以你碰到class内有箭头函数的题目,把它当成构造函数创建对象来处理就可以了。

在构造函数中如果使用了箭头函数的话,this指向的就是这个实例对象。

因此将class转化为构造函数的话,伪代码为:

function Cat () {
  this.type = 'class'
  this.getType = () => {
    console.log(this.type)
    console.log(type)
  }
}
Cat.prototype.constructor = function () {
  this.name = 'guaiguai'
  var type = 'constructor'
}
var type = 'window'
var guaiguai = new Cat()
guaiguai.constructor()
guaiguai.getType()
console.log(guaiguai)

别的都好理解,这里为啥,constructor要放在原型对象中,并且要在var guaiguai = new Cat()下面再调用它呢?

嘻嘻,还记得在2.2中我们就说过了吗,任何放在类上的方法都相当于写在原型对象上,并且在使用类的时候,会隐式执行constructor函数。这两段代码就是为了模拟这个操作。

这样的话,上面👆两个题目的结果都是:

'class'
'window'
Cat {type: "class", name: "guaiguai", getType: ƒ}

哇~

有点意思哈~

class还能这样玩?😁

不过上面对于箭头函数还有不理解的小伙伴可以查看这篇

《【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理)》

文章中的7.4题,里面介绍了构造函数对象中普通函数和箭头函数的区别

# 2.7 题目七

如果在class中存在两个相同的属性或者方法会怎么样呢 🤔️?

class Cat {
  constructor () {
    this.name = 'cat1'
  }
  name = 'cat2'
  getName = function  () {
    console.log(this.name)
  }
}
var cat = new Cat()
cat.getName()

这道题中,我们调用getName方法,打印出的会是:

'cat1'

所以可以看出constructor中定义的相同名称的属性和方法会覆盖在class里定义的。

# 2.8 题目八

那么,原型对象中相同名称的属性和方法呢?

class Cat {
  constructor () {
    this.name = 'cat1'
  }
  name = 'cat2'
  getName = function  () {
    console.log(this.name)
  }
}
Cat.prototype.name = 'cat3'
var cat = new Cat()
cat.getName()

答案:

'cat1'

没错,还是以constructor中的为准。这里和构造函数中同名属性的处理方式是一样的,可以看上面👆的1.7题。

# 2.9 题目九

好吧 😅,现在可以加大难度了:

class Cat {
  constructor () {
    this.name = 'guaiguai'
    var type = 'constructor'
    this.getType = () => {
      console.log(this.type)
      console.log(type)
    }
  }
  type = 'class'
  getType = () => {
    console.log(this.type)
    console.log(type)
  }
}
var type = 'window'
var guaiguai = new Cat()
guaiguai.getType()
console.log(guaiguai)

首先我们很清楚,如果type打印出的是window那就表示使用的是第二个getType,否则表示用的是第一个getType

那么根据题2.7,我们可以看出,第一个getType是会覆盖第二个的,所以执行结果为:

'class'
'constructor'
Cat {type: "class", name: "guaiguai", getType: ƒ}

# 总结-class

来吧,对class实现封装也来做个总结呗:

(一) class的基本概念:

  • 当你使用class的时候,它会默认调用constructor这个函数,来接收一些参数,并构造出一个新的实例对象(this)并将它返回。
  • 如果你的class没有定义constructor,也会隐式生成一个constructor方法

(二) class中几种定义属性的区别:

  • constructorvar一个变量,它只存在于constructor这个构造函数中

  • constructor中使用this定义的属性和方法会被定义到实例上

  • class中使用=来定义一个属性和方法,效果与第二点相同,会被定义到实例上

  • class中直接定义一个方法,会被添加到原型对象prototype

  • class中使用了static修饰符定义的属性和方法被认为是静态的,被添加到类本身,不会添加到实例上

(三) other:

  • class本质虽然是个函数,但是并不会像函数一样提升至作用域最顶层
  • 如遇class中箭头函数等题目请参照构造函数来处理
  • 使用class生成的实例对象,也会有沿着原型链查找的功能
阅读全文