面试官:请你说说面向对象有什么特点

530

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

面试官一问,面向对象有什么特点?
面试者:封装、继承、多态
面试官:没了?能不能给我讲讲原理,他们能干什么?
面试者:额...

本文将结合自绘的图形和浏览器运行时截图,带你理解透面向对象的三大特点,并且还会有作用域(对作用域不是特别理解的强烈建议先看作用域这篇!!!)与部分this指向的知识。

封装

封装就是创建一个对象,集中保存现实中一个事物的属性和功能。将零散的数据封装进对象结构中,极其便于大量数据的管理维护。只要使用面向对象思想开发,第一步都是先封装各种各样的对象结构备用。

封装对象的三种方式

1. 直接使用大括号

直接使用 {} 这种形式其实是 new Object() 的简写

const xiaoming = {
  name: '小明',
  getName: function () {
    console.log(this.name)
  }
}

xiaoming.age = 18

这里有一个疑问,为什么在xiaoming这个对象内,访问自己的属性要用this.name而不是直接使用name?凭什么这个 this 就是指向xiaoming,而不是 window,关于这个我们先留一下,我们放到后面聊

2. 使用 new Object()

const xiaoming = new Object({
  name: '小明',
  getName: function() {
    console.log(this.name)
  }
})
​
xiaoming.age = 18

3. 使用构造函数

使用上面两种方式创建多个相同结构的对象时会写很多重复代码。

const xiaoming = {
  name: '小明',
  getName: function() {
    console.log(this.name)
  }
}

const xiaoguang = {
  name: '小光',
  getName: function() {
    console.log(this.name)
  }
}

 // ....

使用构造函数可以解决这个问题。

function Person(name) {
  this.name = name
  this.getName = function() {
    console.log(this.name)
  }
}
​
const xiaoming = new Person('小明')
const xiaoguang = new Person('小光')
​
xiaoming.age = 18
xiaoming.getName() // 小明
console.log(xiaoming.age) // 18
​
xiaoguang.getName() // 小光
console.log(xiaoguang.age) // undefined

构造函数的好处就是代码复用。这种方式在很多框架中就有应用,比如在 React 中创建的 Fiber

对象内访问内部属性

为什么对象内不能直接访问自己内部的属性

先看我们一开始的第一个疑问——为什么在对象内部访问自己的属性不能直接用xxx,为什么 this 指向的不是 window

我们以下面这段程序为例:

const xiaoming = {
  name: '小明',
  getName: function () {
    console.log(name)
  }
}
​
xiaoming.getName() // undefined

首先是创建对象时,new Object出来一个对象的地址 0x3241 承接着对象内的属性 name='小明' getName的值是一个function所以需要调用new Function创建一个函数对象 0x5693 内部放着执行这个函数时的代码

1.jpg

然后调用 xiaoming.getName() 方法,首先是先创建临时的作用域对象,这个作用域对象是一个空的,根本就找不到name属性。于是顺着作用域链往上找(往上就只有window了),也找不到有name的属性,于是就只能报undefined

2.jpg

因为只有函数的 {} 才会产生作用域对象,那么这里const xiaoming = { ... } ,所以在对象内部无法直接访问到自身的属性。这就解释了为什么不能直接用 name,以及 this 为什么不指向 window。

关于作用域相关的可以看 这篇

通过对象自身引用访问

从上图中可以看到,我们在getName内部可以访问到 window 中的 xiaoming 的引用,所以在对象内部可以 xiaoming.name 来访问自身的属性,但是这种方式紧耦合,属实不是好的解决方式。

通过 this

我们可以通过this关键字来访问到对象本身 ​

那么,什么是 this ?

  • 每个函数内自带的
  • 专门指向正在调用当前函数的 .前的对象的关键字。

3.png

4.jpg

经典面试题:调用构造函数时 new 做了哪些事情?

function Person(name) {
  this.name = name
  this.getName = function() {
    console.log(this.name)
  }
}
const xiaoming = new Person('小明')

表面上来看,是两件事

  1. 使用 new 调用 Person 实参"小明"传给构造函数内的形参name变量,然后再传给 this.name
  2. 最后再交给对象 xiaoming

但其实,new 做了4件事

  1. 创建一个新的空对象等待

5.jpg

  1. 先留着,后面设计到这个知识点,会重新回顾这四步

  2. 调用构造函数:

    a. 将构造函数中的 this 指向刚创建的新对象

6.jpg

b. 在构造函数内通过强行赋值的方式,为新对象添加属性和方法

7.jpg

在上一节中我们说,this 专门指向正在调用当前函数的 .前的对象的关键字。但是这里我们哪儿来的.

这是因为 new 会将 this 连接到新创建的对象上,这就是 this 的第二种情况。

  1. 最后一步自然是返回新对象的地址,保存到 xiaoming 这个变量里

小结 :

new 做了四件事

  • 创建一个新的空对象等待
  • 将构造函数中的 this 指向刚刚创建的新对象,并且为其添加规定的属性和方法
  • 返回新对象的地址,保存到 = 左边到变量里

目前了解到 this 的两种情况

  • obj.fun(): fun 中的 this 指向 . 前的对象
  • new Fun() Fun 中的 this 指向 new 创建的新对象

继承

继承用来解决重复创建的问题

我们通过上面的 new 一个构造函数时的执行图里面有一个问题,就是就是我每次调用 new Person都会执行new Function来创建构造函数里面的getName方法,但是我们getName方法它的功能总是相同的,这就造成了重复创建。 ​

那么我们就要用到继承来解决这个问题 ​

JS 中继承使用原型对象实现

首先我们要知道,JS 中的继承都是通过原型对象来实现的,原型对象是替所有子对象集中保存共有属性值和方法的父对象。 ​

原型对象不需要自己创建,在定义构造函数时程序会自动创建一个原型对象 prototype 挂载到构造函数上

8.png

这样我们的getName可以直接放到这个prototype上就不用重复创建了

function Person(name) {
  this.name = name
}
​
Person.prototype.getName = function() {
  console.log(this.name)
}
​
const xiaoming = new Person('小明')
xiaoming.getName() // "小明"

我们来调用试试看

9.png

我们通过上面的图就能看出,xiaoming.__proto__就等于Person.prototypexiaoming.getName是通过访问 Person.prototype拿到的。 ​

执行到了getName中,这是第三种 this的情况,但是我们发现,这种this与上面说的 this 指向. 前面的对象其实是一摸一样的!那么这个 this 自然也就是xiaoming了 ​

控制台中打印的 xiaoming 的值中有个[[Prototype]]的属性,这玩意儿就是 __proto__

小结:

使用继承能够避免同样的方法被重复创建,占用内存空间。 ​

JS 中的继承是由原型对象实现的。函数定义时原型对象作为属性prototype挂载。在调用构造函数时,将创建的空对象的__proto__指向函数的prototype。 ​

通过 Fun.prototype.xxx = function () {}来添加原型中的方法,然后实例对象可以直接instance.xxx来通过__proto__查找到xxx

那么我们上面说的 new 的四个步骤中的第二步就补上了,我们再回顾一遍这四个步骤

  • (紫)创建一个新的空对象等待
  • (蓝)让这个新对象继承构造函数的原型对象(__proto__ 属性指向构造函数的原型对象)
  • (绿)将构造函数中的 this 指向这个新对象,并给新对象添加规定的属性
  • (黄)返回新对象地址

10.jpg

同时this的三种情况:

  • obj.fun(): fun 中的 this 指向 . 前的对象
  • new Fun() Fun 中的 this 指向 new 创建的新对象
  • 在构造函数的原型中与第一种一样,不看定义,只看调用

多态

同一个方法,在不同情况下表现出不同的状态。在JavaScript中,多态是基于原型链实现的。

原型链

我们先来看这样的一段代码(Person 仍是上面示例的构造函数)

const obj = { a: 1 }, xiaoming = new Person('小明')

obj.toString()      // '[object Object]'
xiaoming.toString() // '[object Object]'

obj上有toString方法我们可以理解,应该是Object构造对象出厂在 __proto__ 带的,但是Person出厂时并没有添加toString呀? ​

答案就是原型链,这里调用的toString是通过原型链查找得到的。 ​

我们Person返回的是一个对象,而这个对象背后也是 new 出来的 object。 我们用画图来描述在xiaoming上查找toString的过程

11.jpg

  1. 首先是在xiaoming的这个对象上查找,没有找到toString方法,但是有__proto__连接的Person.prototype对象,于是去Person.prototype上查找
  2. 到达了Person.prototype上发现也没有找到toString方法,但是有__proto__连接的Object.prototype对象,于是又跑到Object.prototype上查找
  3. 最终,终于在Object.prototype上找到了toString方法来调用。

总结一下什么是原型链: 原型链是由多级父对象逐级继承形成的链式结构。它保存着一个对象可用的所有属性和方法,控制着属性和方法的使用顺序——就近原则(先子后父,层层查找)。

可能你会有疑问,原型链这跟我们要聊的多态有什么关系?别着急,我们再来看一段代码

const obj = { a: 1 }, date = new Date()

typeof obj      // 'object'
typeof date     // 'object'

obj.toString()  // '[object Object]'
date.toString() // 'Fri Jan 28 2022 16:18:23 GMT+0800 (中国标准时间)'

我们发现同样都是对象,都是调用toString方法是不一样的结果,经过上面关于原型链以及原型链的查找规则的理解,我们应该懂了,这两个toString不是同一个。以实际运行结果为证。

obj.toString === xiaoming.toString  // ture
obj.toString === date.toString      // false

这说明在date的原型对象中自己重新定义了toString方法,使得得到的toString方法与obj.toString不同。 ​

所以才有一开始的那句——在JavaScript中,多态是基于原型链实现的。

我们的Person也可以自己在prototype上写一个toString“覆盖”原本的方法

小结:

在JavaScript中,多态是基于原型链的查找规则实现的。因为在JavaScript中查找顺序是从自己本身找查找开始,如果自己本身没有,则去自己的原型上查找,如果在自己的原型上还没有找到,则去自己的原型的原型上查找....

  • 如果查找到了,则返回查找到的结果,给调用对象使用
  • 如果没有查找到,则一直向上查找,一直查找到Object的原型指向 null 为止

这个查找过程与作用域链的查找过程很相似 ​

总结

封装

封装一个对象,将其零散的属性和功能保存到里面能够便于大量数据的管理维护。 ​

封装对象有三种方式:

  • 直接使用 { }
  • 使用 new Object()
  • 使用构造函数

对于需要多次使用同一结构的数据,使用构造函数能够避免代码冗余 ​

继承

继承避免同样的方法被重复创建,占用内存空间。 ​

JS 中的继承是由原型对象实现的。函数定义时原型对象作为属性prototype挂载。在调用构造函数时,将创建的空对象的__proto__指向函数的prototype。 ​

通过 Fun.prototype.xxx = function () {}来添加原型中的方法,然后实例对象可以直接instance.xxx来通过__proto__查找到xxx

多态

多态是基于原型链的查找规则实现的,从实例本身开始查找,然后再向上面的原型上找,如果找到了则终止查找,返回给实例调用。如果找到Object的原型指向 null 则查找失败。 ​

new 做的四件事

  • 创建一个新的空对象等待
  • 让这个新对象继承构造函数的原型对象(__proto__ 属性指向构造函数的原型对象)
  • 将构造函数中的 this 指向这个新对象,并给新对象添加规定的属性
  • 返回新对象地址

OK,面向对象的三大特点基本聊完了,如果觉得对你有帮助,还请点下赞👍 ,如果觉得有什么地方写的不好的,或者有什么建议或疑问的,欢迎在留言区给我留言反馈,谢谢~

最后,2022,加油!