JS面向对象

96 阅读17分钟

面向对象是现实的抽象方式

对象是JavaScript中一个非常重要的概念,这是因为对象可以将多个相关联的数据封装到一起,更好的描述一个事物:

比如我们可以描述一辆车:Car,具有颜色(color)、速度(speed)、品牌(brand)、价格(price)这些属性,行驶(travel)功能等等

比如我们可以描述一个人:Person,具有姓名(name)、年龄(age)、身高(height)这些属性,吃东西(eat)、跑步(run)功能等等

用对象来描述事物,更有利于我们将现实的事物,抽离成代码中某个数据结构,而有一些编程语言就是纯面向对象的编程语言,比如Java。你在实现任何现实抽象时都需要先创建一个类,再根据类去创建对象

JavaScript的面向对象

JavaScript其实支持多种编程范式的,包括函数式编程和面向对象编程

JavaScript中的对象被设计成一组属性的无序集合,像是一个哈希表,有key和value组成;

key是一个标识符名称,value可以是任意类型,也可以是其他对象或者函数类型;

如果值是一个函数,那么我们可以称之为是对象的方法;

如何创建一个对象呢?

早期使用创建对象的方式最多的是使用Object类,并且使用new关键字来创建一个对象:

这是因为早期很多JavaScript开发者是从Java过来的,它们也更习惯于Java中通过new的方式创建一个对象

后来很多开发者为了方便起见,都是直接通过字面量的形式来创建对象:

这种形式看起来更加的简洁,并且对象和属性之间的内聚性也更强,所以这种方式后来就流行了起来

image.png

对属性操作的控制

在前面我们的属性都是直接定义在对象内部,或者直接添加到对象内部的:

但是这样来做的时候我们就不能对这个属性进行一些限制:比如这个属性是否是可以通过delete删除的?这个属性是否可以在for-in遍历的时候被遍历出来呢?

如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符。 通过属性描述符可以精准的添加或修改对象的属性:

属性描述符需要使用 Object.defineProperty 来对属性进行添加或者修改

Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

image.png 可接收三个参数:

obj:要定义属性的对象;

prop:要定义或修改的属性的名称或 Symbol;

descriptor:要定义或修改的属性描述符;

返回值:被传递给函数的对象。

属性描述符的类型有两种:

数据属性(Data Properties)描述符(Descriptor)

存取属性(Accessor访问器 Properties)描述符(Descriptor)

image.png

数据属性描述符

数据数据描述符有如下四个特性:

configurable: 表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符。当我们直接在一个对象上定义某个属性时,这个属性的configurable为true。当我们通过属性描述符定义一个属性时,这个属性的Configurable默认为false

enumerable: 表示属性是否可以通过for-in或者Object.keys()返回该属性

当我们直接在一个对象上定义某个属性时,这个属性的enumerable为true。当我们通过属性描述符定义一个属性时,这个属性的enumerable默认为false

writable: 表示是否可以修改属性的值。当我们直接在一个对象上定义某个属性时,这个属性的writable为true。当我们通过属性描述符定义一个属性时,这个属性的writable默认为false。

value: 属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改,默认情况下这个值是undefined

image.png

存取属性描述符

数据数据描述符有如下四个特性:

configurable: 表示属性是否可以通过delete删除,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;

和数据属性描述符是一致的,当我们直接在一个对象上定义某个属性时,这个属性的configurable为true。

当我们通过属性描述符定义一个属性时,这个属性的configurable默认为false。

enumerable: 表示属性是否可以通过for-in或者Object.keys()返回该属性。和数据属性描述符是一致的,当我们直接在一个对象上定义某个属性时,这个属性的enumerable为true。

当我们通过属性描述符定义一个属性时,这个属性的[[enumerable]]默认为false;

get: 获取属性时会执行的函数。默认为undefined

set: 设置属性时会执行的函数。默认为undefined

我们什么时候会用到get和set呢?

1.不希望设置的属性直接暴露给使用者,类似于面向对象里面的私有属性:

image.png

2.如果我们希望截获某一个属性的访问和设置值的过程,也会使用存取属性描述符

3.在vue2的响应式系统中有用到存取属性描述符,他是在属性的getter中收集依赖,当某个地方读取这个属性的时候会调用getter,在getter中收集依赖。当我们对该值进行写入操作的时候,在setter中调用notify方法,向这个属性的所有依赖发出通知,从而达到响应式的效果

同时定义多个属性

Object.defineProperties() 方法可以直接在一个对象上定义 多个 新的属性或修改现有属性,并且返回该对象。

image.png

另外,如果configurable和enumerable属性为true的情况下,可以使用以下简便写法:

image.png

对象方法补充

获取对象的属性描述符:

getOwnPropertyDescriptor

getOwnPropertyDescriptors

禁止对象扩展新属性:

preventExtensions:给一个对象添加新的属性会失败(在严格模式下会报错),但是可以删除和修改;

密封对象:

seal: 不允许配置和删除属性(不可添加和删除,可以修改)实际是调用preventExtensions,并且将现有属性的configurable设置成false

冻结对象

freeze:不允许修改现有属性:(添加,删除,修改都会失败)实际上是调用seal,并且将现有属性的writable设置成 false

这里补充一个点,configurable:false(可以修改,不能删除)writable:false(可以删除,不能修改)

image.png

image.png

image.png

image.png

创建多个对象的方案

如果我们现在希望创建一系列的对象:比如Person对象

包括张三、李四、王五等等,他们的信息各不相同;

那么采用什么方式来创建比较好呢? 目前我们已经学习了两种方式:

(1)new Object方式;

(2)字面量创建的方式;

image.png

这种方式有一个很大的弊端:创建同样的对象时,需要编写重复的代码

创建对象的方案 – 工厂模式

我们可以想到的一种创建对象的方式:工厂模式

工厂模式其实是一种常见的设计模式;

通常我们会有一个工厂方法,通过该工厂方法我们可以产生想要的对象;

image.png

认识构造函数

工厂方法创建对象有一个比较大的问题:我们在打印对象时,对象的类型都是Object类型

但是从某些角度来说,这些对象应该有一个他们共同的类型;另外一个问题就是所有对象里都有实例方法,而这些方法在不同的对象中的实现都是一样的。 下面我们来看一下另外一种模式:构造函数的方式;

我们先理解什么是构造函数?

构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数;

在其他面向对象的编程语言里面,构造函数是存在于类中的一个方法,称之为构造方法;

但是JavaScript中的构造函数有点不太一样;

JavaScript中的构造函数是怎么样的?

构造函数也是一个普通的函数,从表现形式来说,和普通的函数没有任何区别;

那么如果这么一个普通的函数被使用new操作符来调用了,那么这个函数就称之为是一个构造函数;

那么被new调用有什么特殊的呢?

new操作符调用的作用

如果一个函数被使用new操作符调用了,那么它会执行如下操作:

  1. 在内存中创建一个新的对象(空对象);

  2. 这个对象内部的prototype属性(隐式原型)会被赋值为该构造函数的prototype属性(显式原型);(后面详细讲);

  3. 构造函数内部的this,会指向创建出来的新对象;

  4. 执行函数的内部代码(函数体代码);

  5. 如果构造函数没有返回非空对象,则返回创建出来的新对象;

创建对象的方案 – 构造函数

我们来通过构造函数实现一下:

image.png

这个构造函数可以确保我们的对象是具有Person的类型的(实际是constructor的属性,这个我们后续再探讨)

但是构造函数就没有缺点了吗?

构造函数也是有缺点的,它在于我们需要为每个对象的函数去创建一个函数对象实例

在开发中有一个约定俗成的规范,如果编写的是一个构造函数,一般函数名首字母大写

image.png

认识对象的原型

JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]],这个特殊的属性可以指向另外一个对象。这个属性是ECMA规定所有的JS引擎必须为每个对象实现的一个标准属性,目前浏览器大多数都是通过 __proto__ 来实现ECMA规定的这个属性的。

那么这个对象有什么用呢?

当我们通过引用对象的属性key来获取一个value时,它会触发 Get的操作;

这个操作会首先检查该对象是否有对应的属性,如果有的话就使用它;

如果对象中没有该属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性;

那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?

答案是有的,只要是对象都会有这样的一个内置属性;(隐式原型) 获取的方式有两种:

方式一:通过对象的 __proto __ 属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题)

方式二:通过 Object.getPrototypeOf 方法可以获取到;这个方法是标准规定的

image.png

image.png

创建对象的方案 – 构造函数

我们来通过构造函数实现一下:

image.png

这个构造函数可以确保我们的对象是具有Person的类型的(实际是constructor的属性,这个我们后续再探讨) 但是构造函数也是有缺点的,它在于我们需要为每个对象去创建一个函数对象实例

认识对象的原型

JavaScript当中每个对象都有一个特殊的内置属性 prototype,这个特殊的属性可以指向另外一个对象。

那么这个对象有什么用呢?

当我们通过引用对象的属性key来获取一个value时,它会触发 Get的操作;

这个操作会首先检查该对象是否有对应的属性,如果有的话就使用它;

如果对象中没有该属性,那么会访问对象prototype内置属性指向的对象上的属性;

那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?

答案是有的,只要是对象都会有这样的一个内置属性;(隐式原型) 获取的方式有两种:

方式一:通过对象的 proto 属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题)

方式二:通过 Object.getPrototypeOf 方法可以获取到;这个方法是标准规定的

image.png

image.png

函数的原型 prototype

那么我们知道上面的东西对于我们的构造函数创建对象来说有什么用呢? 它的意义是非常重大的,接下来我们继续来探讨。

这里我们又要引入一个新的概念:所有的函数都有一个prototype的属性。你可能会问,是不是因为函数是一个对象,所以它有prototype的属性呢?

不是的,因为它是一个函数,才有了这个特殊的属性,而不是因为它是一个对象,所以有这个特殊的属性。

image.png

image.png 以上讲的都是对象的原型,对象的原型其实是隐式原型,但是函数相对于普通对象来说还有一个显式原型

函数的显式原型有什么作用呢?

它的主要作用是在通过new操作符调用函数的时候,会把函数的隐式原型指向函数的显式原型

image.png

再看new操作符

1.在内存中创建一个新的对象(空对象);

2.这个对象内部的prototype属性(隐式原型),也就是__proto__会被赋值为该构造函数的prototype(显式原型)属性;

那么也就意味着我们通过Person构造函数创建出来的所有对象的隐式原型属性都指向Person.prototype(显式原型):

image.png

在这里总结一下这2个原型的作用,

(1)作为对象,有自己的隐式原型__proto__,这个原型的作用是当在对象是查找属性的时候,如果在对象上找不到,则去这个对象的隐式原型上查找。

(2)作为函数,有自己的显式原型,这个显式原型式在通过new关键字创建对象的时候,把这个显式原型赋值给新创建出来的对象的隐式原型用的,所以我们通过把对象的公共方法添加到对象的显式原型obj.prototype属性上,可以实现代码的复用

构造函数原型的内存表现

image.png

image.png

1.当解析函数的时候在堆内存中存在着一个函数对象,这里对应的就是上图的person对象了

2.person函数对象中除了有函数的执行体,函数的父级作用域以外还有一个prototype属性(这里是显式原型属性了)

3.这个prototype会指向Person函数的显式原型对象

4.当我们通过new Person()的方式去调用Person函数的时候,那么执行函数执行体中的代码,在堆内存中会创建一个p1对象,这个对象中默认有一个__proto__,接着在GO对象中会有一个p1的引用,这个引用指向刚刚创建的p1对象

5.把函数对象的prototype赋值给这个p1对象的__proto__,这样子刚刚创建出来的p1对象中的__propto__属性也指向了Person函数的原型对象了,当然我们如果通过new再创建一个函数对象,那么这个函数对象的__proto__属性也同样会指向Person函数的显式原型对象,所以函数的隐式原型都是指向函数的显式原型的。

image.png

我们来简化一下上面的过程:

1.Person函数对象中除了有函数的执行体,函数的父级作用域以外还有一个prototype属性

2.这个prototype属性指向函数的显式原型对象

3.当我们通过new调用函数的时候,执行函数的执行体,在内存中创建一个对象(参考new操作符调用的作用的第二步),在这里是p1对象

4.把函数的prototype属性的值(这里是个内存地址)赋值给第3步创建出来的对象的__propto__(隐式原型)

总结:函数的prototype和__proto__指向的都是函数的显式原型对象,new操作最主要的一步操作是将函数的显式原型赋值给它的隐式原型(上面的第4步)

当我们打印某个对象的属性的时候,首先会在对象的本身的属性里查找,如果找不到,就会在对象的原型中查找

image.png

image.png

当然上述代码更好的做法应该是使用Person.prototype.name="kobe"这种方法,代码中一般修改这个显式原型。

甚至我们在p1中修改了原型对象,然后通过p2读取原型对象的值,也是可以看到修改后的值的。

函数在进行new操作的时候,会将函数的隐式原型指向函数的显式原型

constructor属性

事实上(显式)原型对象上面是有一个属性:constructor

默认情况下(显式)原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象,也就是构造函数本身了;

image.png

image.png

image.png 我们也可以在原型对象上添加自己的属性

image.png

打印属性的时候,先从对象本身中查找属性,找不到则去查找对象的原型。

重写原型对象

如果我们需要在原型上添加过多的属性,通常我们会重写整个原型对象:

image.png

注意这里需要把原型里的construtor属性也重写一下,让它也指向原来的函数对象,但是在真实开发中,不要像上图一样直接添加,应该通过属性描述符添加,把constructor属性设置得跟原来的可读写属性一致:

image.png

这里我们来捋一下对象的原型和函数的原型:

每个对象都有一个隐式的原型,函数因为它也是一个对象,所以函数也有隐式原型,不同之处在于函数还有一个显式原型,在通过new关键字调用函数的时候会将函数的显式原型赋值给函数对象的隐式原型。我们在代码中一般通过修改函数的显式原型实现对函数原型的修改

创建对象 – 构造函数和原型组合

我们在上一个构造函数的方式创建对象时,有一个弊端:会创建出重复的函数,比如running、eating这些函数,如下:

image.png

那么有没有办法让所有的对象去共享这些函数呢?

可以,将这些函数放到Person.prototype的对象上即可;

image.png

这里我们来思考两个问题

Q1:放到原型上的函数,内部的this指向会不会有问题?

不会有问题的,因为函数的this绑定只跟函数的调用位置有关,调用eating和running方法的时候是通过p1.eating()这样的方式调用的,那么采用隐式绑定规则,这里的this就绑定了p1对象,这个绑定是正确的

Q2:对象的属性能不能也定义在原型上?

不可以的,由于每个对象属性的值都是不同的,p1.name跟p2.name是不一样的值,如果把属性定义在原型上,那么当创建p1的时候为name属性赋值是没问题的,当创建p2的时候为name属性赋值由于修改了原型里的属性,那么p1里的name属性就会被修改成p2的name属性的值了,这样显然是不正确的!

这篇先到这里吧,下一篇再写继承相关的