JavaScript中对象的生命周期
一切皆对象
其实,我们已经了解到了,JavaScript中,所有的东西都是对象,也就是所谓的“Object”
类型,但是,在JavaScript中,并没有真正的类的概念,所以,此处的对象并不像是Java或者Python中的那样,是由类实例化而来的,而是由键和值来组成的,对象其实就是以键命名的值的容器。
比如,我们使用typeof
来检查一个数组的类型,会发现,其实数组也是一个Object
1 | let a = [] |
其他的一些类型也可以这么验证,比如说function类型,如果说,创建一个function类型的话,JavaScript引擎会自动为这个function添加一些额外的属性,就像给对象添加属性一样,比如说toString()
方法。
1 | let b = function () { |
我们如果深究的话,可以尝试一下:
1 | dir(b) |
其原型下面拥有constructor
等方法,这一看就是一个对象啊。
同样考察其他基本数据类型:
1 | typeof Object.prototype // 'object' |
可以看得出来,真的是一切皆对象。
那么什么是一个对象的ProtoType
呢?简单的来说,prototype
就是一个父对象(可以参考父类)的镜像或者链接,通过prototype
我们可以访问父对象中的一些方法。
就像,我们本来定义一个function
的时候是没有toString()
这个方法的,这个方法是哪里来的呢?其实就是我们调用了prototype
中的toString()
方法,这个方法来自于其父对象,也就是Object
。
对象的创建和连接
JavaScript中的对象是互相有关系的,就像Python中的Object
一样,所有的对象,都是object
对象的子对象,我们创建一个对象的时候其实是创建了一个Object的副本,然后向这个副本中添加别的一些属性,并且重命名成为我们想要的对象,当然这个过程还是会进行一些别的操作的。
1 | var Person = { |
在上面的例子中,我们定义了一个Person
对象作为父对象,然后,我们通过这个父对象,创建了一个Tom
子对象,这个子对象就继承了Person
对象的所有属性。包括:name
、age
,以及greet
方法。
我们可以继续为Tom
对象添加新的属性:
1 | Tom.sayHi = function () { |
上面的这种方法创建的对象,继承了所有父对象的属性和值,为Object.create()
方法添加额外的参数,就可以为其返回的新对象初始化数据了。但是,我们先做一个实验:
1 | for (const key in Tom) { |
为什么做这个实验我们先不纠结,上面的实验是指我们循环遍历Tom中,目前所拥有的的属性的名称。
然后我们接着来看另外一种初始化对象的方法:
1 | var Tom = Object.create(Person, { |
我们可以看出,我们使用在Object.create()
方法中添加参数的方法来初始化对象的话,初始化的对象都不能被枚举了。
我们在JavaScript引擎的工作原理中提到过这些概念,现在复习一下这几个概念:
- 可枚举(迭代)性(enumerable):
- 可枚举意味着属性会在
for...in
循环中显示,或者会被遍历,但是该属性还是可以被直接访问到,就是俗称的点出来如:Tom.age
- 可配置性(configurable):
- 意味着能修改属性的行为,让该对象的属性都是不可迭代的、不可修改的和不可配置的. 只有可配置的属性才能通过
delete
被删除。
- 可修改(写)性(writable):
- 意味着我能修改该对象的所有属性的值,通过为这些属性赋予一个新值就能修改:
Tom.age = 1000;
.
所以我们可以修改上面的创建方式来对上面的三个属性使能:
1 | var Tom = Object.create(Person, { |
当然,我们还有一种更为方便的创建对象的方法,就是以函数的方式去创建对象,我们将上面的代码修改如下:
1 | var personMethods = { |
上面的方法具体干了些什么事情?
定义了一个
personMethods
对象,对象中包含一个函数元素,命名为greet
定义了一个
Person
函数,该函数返回一个对象,返回的这个对象继承了personMethods
对象。Person
执行的过程中,还对创建的personMethods
的这个子对象添加了一些自己的属性:age
、name
当然,我们也可以不单独定义(理解错误了):personMethods
对象,也就是父对象。而是将这个方法直接挂载到我们新创建的newPerson
对象的原型上,具体如下
当然我们也可以直接使用Person
的原型为模板创建这个newPerson
对象,这样的话,我们就可以直接为原型添加方法,如下:
1 | Person.prototype.greet = function () { |
现在公共方法的来源是Person.prototype
。 使用JS中的new
运算符,可以消除Person
中的所有噪声,并且只需要为this
分配参数。
可以将下面的这部分代码:
1 | function Person(name, age) { |
修改为:
1 | function Person(name, age) { |
然后在其原型上直接添加属性:
1 | function Person(name, age) { |
注意,使用new
关键字,被称为“构造函数调用”
,new
干了三件事情:
创建一个空对象
将空对象的
__proto__
指向构造函数的prototype
使用空对象作为上下文的调用构造函数
1
2
3
4function Person(name, age) {
this.name = name;
this.age = age;
}
根据上面描述的,new Person("Valentino")
做了:
- 创建一个空对象:
var obj = {}
- 将空对象的
__proto__
指向构造函数的 prototype:obj.__proto__ = Person().prototype
- 使用空对象作为上下文调用构造函数:
Person.call(obj)
检查原型链
原型链其实简单地说就是一个对象之间的依赖关系。类似于父类到子类的继承关系。
对于JavaScript
的原型链检查,可以使用Object.getPrototypeOf()
方法来实现,还有一种方法就是判断一个对象的父对象是否为另一个对象,使用Object.isPrototypeOf()
方法来实现。
比如:
1 | var Person = { |
很明显,如果使用Object.create()
方法来创建对象的话,使用Object。getPrototypeOf()
方法获取到的就是其父对象Person
的内容:
1 | console.log(Object.getPrototypeOf(Tom) === Person); |
而如果使用构造函数方法来创建对象的话,要对其原型进行检查的话需要观察其prototype
属性,具体如下:
1 | function Person(name, age) { |
还有一种检查原型链的方法,就是[Object].prototype.isPrototypeOf()
方法,该方法用于测试一个对象是否存在于另一个对象的原型链上,如下所示,检查 me
是否在 Person.prototype
上:
1 | Person.prototype.isPrototypeOf(me) && console.log('Yes I am!') |
如果要测试一个构造函数的prototype
属性是否出现在原型链上,则还有一种方式isinstance()
方法。
1 | isinstance(tom, Person) |
JavaScript
在访问对象的属性时,具体的流程为:JS引擎会检查该方法是否可直接在当前对象上使用。 如果不是,搜索将继续向上链接,直到找到该方法。
保护对象不受操作
大多数情况下,JS 对象“可扩展”是必要的,这样咱们可以向对象添加新属性。 但有些情况下,我们希望对象不受进一步操纵。 考虑一个简单的对象:
1 | var superImportantObject = { |
默认情况下,每个人都可以向该对象添加新属性
1 | var superImportantObject = { |
Object.preventExtensions()
方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。
1 | var superImportantObject = { |
这种技术对于“保护”代码中的关键对象非常方便。JS
中还有许多预先创建的对象,它们都是为扩展而关闭的,从而阻止开发人员在这些对象上添加新属性。这就是“重要”对象的情况,比如XMLHttpRequest
的响应。浏览器供应商禁止在响应对象上添加新属性
1 | var request = new XMLHttpRequest(); |
这是通过在“response”对象上内部调用Object.preventExtensions
来完成的。 您还可以使用Object.isExtensible
方法检查对象是否受到保护。
如果对象是可扩展的,它将返回true
:
1 | var superImportantObject = { |
如果对象不可扩展的,它将返回false
:
1 | var superImportantObject = { |
当然,对象的现有属性可以更改甚至删除
1 | var superImportantObject = { |
现在,为了防止这种操作,可以将每个属性定义为不可写和不可配置。为此,有一个方法叫Object.defineProperties
。
1 | var superImportantObject = {}; |
或者,更方便的是,可以在原始对象上使用Object.freeze
:
1 | var superImportantObject = { |
Object.freeze
工作方式与Object.preventExtensions
相同,并且它使所有对象的属性不可写且不可配置。
唯一的缺点是“Object.freeze
”仅适用于对象的第一级:嵌套对象不受操作的影响。
类
有大量关于ES6 类的文章,所以在这里只讨论几点。JS是一种真正的面向对象语言吗?看起来是这样的,如果咱们看看这段代码
1 | class Person { |
ES6中引入了类。但是在这一点上,咱们应该清楚JS中没有“真正的
”类。 一切都只是一个对象,尽管有关键字class
,“原型系统”仍然存在。
新的JS版本是向后兼容的,这意味着在现有功能的基础上添加了新功能,这些新功能中的大多数都是遗留代码的语法糖。
总结
JS中的几乎所有东西都是一个对象。 从字面上看。 JS对象是键和值的容器,也可能包含函数。 Object
是JS中的基本构建块:因此可以从共同的祖先开始创建其他自定义对象。 然后咱们可以通过语言的内在特征将对象链接在一起:原型系统。
从公共对象开始,可以创建共享原始“父”的相同属性和方法的其他对象。 但是它的工作方式不是通过将方法和属性复制到每个孩子,就像OOP语言那样。
在JS中,每个派生对象都保持与父对象的连接。 使用Object.create
或使用所谓的构造函数创建新的自定义对象。 与new
关键字配对,构造函数类似于模仿传统的OOP类。
JavaScript中对象的生命周期