
大白话系列之JavaScript原型及面向对象
大白话系列之JavaScript原型及面向对象
对象
前言
一开始就和Java一样整个new
1 | // 1.创建一个空的对象 |
后来很多开发者为了方便期间,都是直接通过字面量的形式来创建对象:
1 | var obj2 = { |
对象属性描述
如果使用字面量形式进行声明对象属性,我们就失去了对对象属性的精准控制。
如:
- 这个属性我能不能delete
- 这个属性能不能在for in的时候被遍历
为了实现如此控制我们可以使用Object.defineProperty来对属性进行添加或者修改
而数据属性描述又分为两类
数据属性描述
[[Configurable]]:表示属性是否可以通过delete删除属性,是否可以修改它的特性。
- 当我们在一个对象上定义了某个属性时,这个属性的Configurable为true
- 当我们通过属性描述符(Object.defineProperty)定义一个属性时,这个值为false
[[Enumerable]]:表示属性是否可以通过for-in或者Object.keys()返回该属性;
- 当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true;
- 当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false;
[[Writable]]:表示是否可以修改属性的值;
- 当我们直接在一个对象上定义某个属性时,这个属性的[[Writable]]为true;
- 当我们通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为false;
[[value]]:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改;
- 默认情况下这个值是undefined;
存取属性描述
- [[Configurable]] [[Enumerable]] 这两个似乎可以相互转换,功能是一样的。
- [[get]]:获取属性时会执行的函数。默认为undefined
- [[set]]:设置属性时会执行的函数。默认为undefined
1 | var obj = { |
如果自己定义属性:
1 | // "use strict" |
…… 省略一把
构造函数创建对象
什么是构造函数?
- 就是在我们创建对象时会调用的函数。
- 在其他面向的编程语言里面,构造函数是存在于类中的一个方法,称之为构造方法;
- 但是JavaScript中的构造函数有点不太一样
构造函数在js中就是一个普通函数,和别的函数没有区别
被new关键词调用的函数,就是构造函数
什么是new?
如果一个函数被new操作符调用了就会有如下操作:
- 内存中创建一个新的对象(空对象)
- 这个对象内部的Property属性会被赋值为该函数的Property属性。
- 构造函数内部的this会指向创建出来的新对象。
- 执行函数的内部代码也就是函数体代码
- 如果构造函数没有返回非空对象,那么返回新创造的对象。
1 | function Person() { |
这个构造函数可以确保我们的对象是有Person的类型的(实际是constructor的属性,这个我们后续再探讨);
认识原型
JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]],这个特殊的对象可以指向另外一个对象。
- 当我们通过引用对象的属性key来获取一个value时,它会触发 [[Get]]的操作;
- 这个操作会先检查是否有对应的属性,有就用
- 如果对象中没有改属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性;
- 这个 [[prototype]] 我们通常会将其称之为隐式原型;
那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?
答案是有的
方式一:通过对象的
__proto__属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题);方式二:通过
Object.getPrototypeOf方法可以获取到;
1 | var obj = {} |
那么我们就可以进行如下的测试了:
1 | // 定义一个obj对象 |
结论,我们是可以直接这样子去添加的。
函数的原型
我们前面说了,通过构造函数创建对象时,对象的Property会是构造函数的Property
函数的prototype
这里我们又要引入一个新的概念:所有的函数都有一个prototype的属性:
1 | function foo() { |
不是因为函数是一个对象所以有prototype属性。
而是因为是函数才有prototype属性。
对象没有prototype属性,有的是 __proto__这个属性
我们前面讲过new关键字的步骤如下:
- 1.在内存中创建一个新的对象(空对象);
- 2.这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性;(后面详细讲);
- 3.构造函数内部的this,会指向创建出来的新对象;
- 4.执行函数的内部代码(函数体代码);
- 5.如果构造函数没有返回非空对象,则返回创建出来的新对象;
我们将重心放到步骤一和二中:
- 在内存中创建一个对象;
- 将对象的[[prototype]]属性赋值为该构造函数的prototype属性;
那就意味着:
1 | function Person(){ |
那么也就意味着我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype:
1 | function Person() { |
那么其实就是..Person函数有一个prototype属性指向 Person的原型。
然后对象创建出来以后..对象的 __proto__和指向和Person函数一样。

那么如果在Person的prototype属性中创建属性,那么p的对象能不能访问到?
当然可以
1 | function Person() { |
值得一提的是,如果你中途来一个p1.name = xxx 那么此时会在你p1的对象里面创建一个name,并不会改变Person中的值
constructor属性
事实上原型对象(就是指函数的prototype属性指向的原型对象)上面是有一个属性:constructor。
默认情况下原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象(这里指的是函数的prototype指向的对象中的constructor的值又是该函数本身,也就是Person函数的prototype对象中的constructor的值指向的是Person函数【函数tm的在js的内存中是对象形式存在所以妈的这句话就很拗口】);
1 | console.log(Person.prototype.constructor) // [Function: Person] |
重写原型
如果我们需要在原型上添加很多很多属性,通常就重写原型了。
1 | function Person() { |
我们说过..每创建一个函数就会生成一个函数对应的原型对象。而原型对象的constructor也会自动获取到函数对象(这里的函数对象其实也就是Person Fuction哈)。
但是我们如果重写了函数的prototype,那么这个prototype就会默认指向Object构造函数了(Object Function)。
1 | console.log(Person.prototype.constructor) // [Function: Object] |
如果要原型constructor指向回来,可以如下
1 | Person.prototype = { |
除了constructor属性一些特性被默认开启了如
[[Enumerable]]为true了。
不然就只能用Object.defineProperty啦
1 | Person.prototype = { |
原型链
在真正实现继承之前,我们先来理解一个非常重要的概念:原型链。
我们知道,从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取:
1 | var obj = { |
但是如果obj的原型上也没有对应的address属性呢?必然还是获取不到的。
那么如果我们配置的原型对象上,继续配置原型呢?
1 | var obj = { |

Object的原型
按照上面的案例,第三个proto的proto就是Object对象了
1 | console.log(obj.__proto__.__proto__.__proto__.__proto__) // [Object: null prototype] {} |
看一下默认的对象原型都是啥
1 | var obj = { name: "xiaoyu" } |
我们可以知道,从Object直接创建出来的对象的原型都是 [Object: null prototype] {}。
那么我们可能会问题: [Object: null prototype] {} 原型有什么特殊吗?
- 特殊一:该对象不再继续有原型属性了,也就是已经是顶层原型了;
- 特殊二:该对象上有很多默认的属性和方法;

继承
ES6下Class语法糖
拯救以上你一下子学不会,写啊写不明白的原型继承,虽然底层依旧是我上面讲的。
那么,如何使用class来定义一个类呢?
1 | // 类的声明 |
接着我们就可以使用new操作符调用类:
1 | var p1 = new Person |
这个写法创造的变量,其实和我们刚刚new 函数干出来的其实是一样的
1 | var p = new Person |
类的构造方法(从这开始就和你其他oop学到的差不多了)
如果我们希望在创建对象的时候给类传递一些参数,这个时候应该如何做呢?
- 每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的constructor;
- 当我们通过new操作符,操作一个类的时候会调用这个类的构造函数constructor;
- 每个类只能有一个构造函数,如果包含多个构造函数,那么会抛出异常;
1 | class Person { |
通过new关键字操作的时候..其实和上文学习的内容是一样的
当我们通过new关键字操作类的时候,会调用这个constructor函数,并且执行如下操作:
- 1.在内存中创建一个新的对象(空对象);
- 2.这个对象内部的[[prototype]]属性会被赋值为该类的prototype属性;
- 3.构造函数内部的this,会指向创建出来的新对象;
- 4.执行构造函数的内部代码(函数体代码);
- 5.如果构造函数没有返回非空对象,则返回创建出来的新对象;
相比较于之前直接写this复制那一套,语法糖果然名副其实
类的方法定义
在上面我们定义的属性都是直接放到了this上,也就意味着它是放到了创建出来的新对象中:
- 在前面我们说过对于实例的方法,我们是希望放到原型上的,这样可以被多个实例来共享;
- 这个时候我们可以直接在类中定义;(其实不要觉得怪异,这在oop思想中本就是应该的)
1 | class Person { |
这样子就可以在各个对象中进行使用了,因为是放在原型上的。
我们也可以查看它们的属性描述符:
- 会发现它们的enumerable都是为false的;
访问器方法
我们之前讲对象的属性描述符有时可以添加setter和getter函数的,那么类也是可以的:
1 | class Person { |
ok 就差不多就是这个写法吧。
但是和直接在对象中定义不同的是,类中的setter和getter是放在对象的
proto上面的,不是和对象中定义一样放在对象中。

我们可以发现确实是放在原型上的哦~
ps:我们如果直接在node环境下进行打印prototype是不能显示的,console.log会有显示上限的噢
所以我们可以用getOwnPropertyDescriptors如下方法进行打印
1 | console.log(Object.getOwnPropertyDescriptors(p.__proto__)) |
getOwnPropertyDescriptors 方法返回一个对象,其中包含指定对象的所有自身属性(即直接属性)的描述符。每个属性的描述符包括 value, writable, enumerable, 和 configurable 等属性。
静态方法
静态方法就和你在别的语言里面所了解的到的一样,比如PHP和Java都有静态方法。
也就是可以直接用类去调用方法。
1 | class Person { |
ES6类继承
没错就是extends,ES5的继承太抽象了,要这么繁琐,所以ES6直接一手extends
1 | class Person{ |
我们知道继承可以让我们复用父类的一些代码结构,比如继承属性和方法:
1 | class Person { |
super关键字
如果在子类的构造函数中使用this或者返回默认对象之前,必须调用super调用父类的构造方法。
这一点PHP的面向对象里面如果要调用父类函数有一个parent::写法,或者直接$this起手,如果子类没有就会去找父类而没有super这个函数。
super的使用位置有三个:
- 子类的构造函数
- 实例方法
- 静态方法;
1 | // 调用 父对象/父类 的构造函数 |
下面的代码会报错,因为我们没有调用super:
1 | class Person { |
我们可以在子类的方法中调用父类的方法:
1 | class Person { |
继承内置类
emm 这个概念怎么说呢,如果你想要对Array类做出一些改变加个方法什么的也不是不行,这样子你就可以用到内置类。
比如Array
1 | class xArray extends Array{ |
但是目前这种继承所返回的数据,或者数据类型既是xArray又是Array我们可以做个改动
1 | class xArray extends Array { |
ES6对象增强
ES6中对对象字面量进行了增强
属性简写
就对象属性赋值的时候,如果变量名和属性名一样就可以简略些
1 | var name = "xiaoyu" |
方法简写
另一个就可以方法简写,其实在不知不觉中我自己已经有混用发现了…..
1 | // ES5的方法写法 |
计算属性名
相信你一定有遇到属性名是需要动态构成的情况,比如我一个属性名得是一个变量名对吧~以前es5只能字面量赋值
1 | var name = "three" |
ES6可以
1 | var name = "three" |
解构Destructuring
ES6加了一个结构的方法,咋说呢,要不是去学习我以为就是天然支持的哈哈哈,毕竟是别的
语言过来的,很多东西都是相通的。
数组结构
1 | var names = ["abc", "cba", "nba"] |
但是结构数组必须得按照顺序来,如果你只要第二个和第三个
1 | var [, nameb, namec] = names |
隔壁GO是_来占位,js直接空着就完事了
如果我们希望解构出来一个元素,其他元素继续放到另外一个数组中:
1 | var [namea, ...newNames] = names |
剩下两个就丢进了newNames中了
如果我们解构的数据数量大于数组中原本的数据数量,那么会返回undefined:
1 | var [namex, namey, namez, namem] = names |
我们可以在解构出来的数据为undefined的时候给它一个默认值
1 |
|
对象的解构
对象的解构和数组的解构是相似的,不同之处在于:
- 数组中的元素是按照顺序排列的,并且我们只能根据顺序来确定需要获取的数据;
- 对象中的数据由key和value组成,我们可以通过key来获取想要的value;
1 | var obj = { |
因为对象是可以通过key来解构的,所以它对顺序、个数都没有要求:
如果我们对变量的名称不是很满意,那么我们可以重新命名:
1 | var { name: whyName, age: whyAge } = obj |
我们也可以给变量一个默认值:
1 | var { name, address = "广州市" } = obj |

![Langchain系列[20]与SQL数据库进行聊天](/img/langchain/cover.png)