在《谈谈JavaScript中对象建立(Object)》这一文中,我们曾经简单地介绍过对象及其创建方式。在今天这一篇文章当中呢,我们要更深入地来理解JavaScript的对象与其他编程语言的对象有何差异。
开始介绍之前先来复习一下。
之前说过,所有原始类型(Primitives) 以外的值都是对象,原始类型有以下几种:
string
number
boolean
null
undefined
symbol
(ES6 新增)
除了上述这些以外的类型,都是对象。
JavaScript 真是一门面向对象的编程语言吗?
在过去JS语言的发展中,这个话题已经被讨论过无数次,有人说它是,也有人说它不那么像是。就像一个使用Java或C#或者其他面向对象开发语言的开发者接触JavaScript的时候,他总会抱怨JavaScript太混乱、没有类型、结构也不好,还有很多奇奇怪怪的地方,它的对象支持也是微乎其微,因此他绝对不是一个面向对象编程的语言。
但JavaScript 确实是一门面向对象的编程语言,只是它与其他语言很大不同的地方是,它的继承方法是通过"prototype" 来实现的。其余大多数的面向对象的编程语言(比如Java)是以「类」为基础的(class-based) ,但JavaScript 没有class、没有extends ,却可以通过「原型」(prototype-based) 来建立起对象之间的继承关系。
PS:ES6虽然新增了class
语法,但仍然属于prototype-based
的继承。class
实质上只是通过简洁的语法来建立对象和处理继承的语法糖。
JavaScript 自定义对象
先前曾介绍过,在JavaScript创建对象我们可以通过new
关键字:
var person = new Object();
person.name = 'mike';
或是直接用大括号{ }
,即可创建起一个新的对象:
var person = {
name: 'mike'
};
理解JavaScript构造函数
虽然JavaScript没有class的语法,但如果你希望JavaScript也能像其他面向对象编程语言一样有类似class
的语法时,可以怎么做呢?由于函数也是个对象,所以可以借用来当作「构造函数」来建立其他对象:
function Person( name, age, gender ){
this.name = name;
this.age = age;
this.gender = gender;
this.greeting = function(){
console.log('Hello! My name is ' + this.name + '.');
};
}
var mike = new Person( 'Mike', 28, 'male');
mike.greeting(); // "Hello! My name is Mike."
var jay = new Person( 'Jay', 18, 'male');
jay.greeting(); // "Hello! My name is Jay."
像这样,我们建立了一个Person
构造函数(Constructor) ,然后就可以通过new
关键字来建立各种对应的对象。
为什么JavaScript明明没有class却可以通过new
一个函数来建立对象?这里简单拆解一下流程:
function Person( name, age, gender ){
// 和上面一致,这里省略
}
var mike = new Person( 'Mike', 28, 'male');
/*
===> var mike = {};
===> Person.call(mike, 'Mike', 28, 'male');
*/
通过new Person(...)
这个动作,返回的对象会有name
, age
, gender
以及greeting
属性,而JavaScript会在背景执行Person.call
方法,将mike
作为this
的参考对象,然后把这些属性通通新增到mike
对象中。
但是,即使是通过构造函数(Constructor)建立的对象,这个对象的属性仍然可以通过.
来公开存取:
function Person( name, age, gender ){
// 和上面一致,这里省略
}
var mike = new Person( 'Mike', 28, 'male');
console.log( mike.age ); // 32
// 因为是公开属性,所以可以很无耻地开放修改
mike.age = 18;
console.log(mike.age ); // 18
属性描述符(Property descriptor)
从ES5开始,我们可以通过新的对象模型来控制对象属性的存取、删除、列举等功能。这些特殊的属性,我们将它称为「属性描述符」(Property descriptor)。
属性描述符一共可以分为六种:
value
: 属性的值writable
:定义属性是否可以改变,如果是false
那就是只可读属性。enumerable
:定义对象内的属性是否可以通过for-in
语法来迭代。configurable
:定义属性是否可以被删除、或修改属性内的writable
、enumerable
及configurable
设定。get
: 对象属性的getter function。set
: 对象属性的setter function。
上述除了value
之外的值都可以不设定,writable
、enumerable
及configurable
的默认值是false
,而get
与set
如果没有特别指定则是undefined
。
这些「属性描述符」必须要通过ES5所提供的Object.defineProperty()
来处理。
Object.defineProperty 与Object.getOwnPropertyDescriptor
我们可以通过Object.defineProperty
来定义对象的属性描述,用法:Object.defineProperty(obj, prop, descriptor)
。
其中:obj->要在其上定义属性的对象;prop->要定义或修改的属性的名称;descriptor->将被定义或修改的属性描述符。
下面通过实际范例来解释:
一般来说,要建立一个简单对象,我们可以用这样方式:
var person = {
name: 'mike'
};
当然,我们也可以通过Object.defineProperty
来定义对象person
的属性:
var person = {};
Object.defineProperty(person, 'name', {
value: 'mike'
});
这样的方式与直接指定对象字面属性是一样的结果。
然后,我们可以用Object.getOwnPropertyDescriptor()
来检查对象属性描述器的状态:
var person = {};
Object.defineProperty(person, 'name', {
value: 'mike'
});
Object.getOwnPropertyDescriptor(person, 'name');
可以看到在默认的情况下,writable
、enumerable
及configurable
都是false
。
var person2 = {
name: 'mike'
};
console.log(Object.getOwnPropertyDescriptor(person2, 'name'));
而通过字面式创建对象建立的属性,默认值则会是true
。
defineProperty
可以针对对象一次设定多个属性描述:
Object.defineProperty(person, 'name', {
value: 'mike',
writable: false,
enumerable: false,
configurable: false
});
或是分别设定:
Object.defineProperty(person, 'name', {
enumerable: true
});
这些都是合法的做法。
假设我们已经定义person.name
属性描述configurable
为false
的情况:
var person = {};
Object.defineProperty(person, 'name', {
value: 'mike',
writable: false,
enumerable: false,
configurable: false
});
那么此时,我们再去执行删除属性的行为:
delete person.name; // it will return false
虽然不会出错,但是你会发现执行结果会返回false
,且person
对象的name
属性依然存在。同样地,当writable
为true
时,你去尝试删除属性「值」的时候,你会发现结果是无效的。
要注意的是,上面这些行为,若是在「严格模式」下则会发生TypeError
的错误。
属性的get 与set 方法
在本文的开始,我们介绍了早期在ES5以前通过this.getXXX()
与this.setXXX()
来作为get
与set
的存取方法。
而现在ES5提供了Object.defineProperty()
之后,我们可以更直观地来处理这些方法:
var person = {};
Object.defineProperty(person, 'name', {
get: function(){
console.log('get');
return this._name_;
},
set: function(name){
console.log('set');
this._name_ = name;
}
});
像这样,我们可以分别为name
属性去定义get
与set
方法,而实际上,我们是通过了另一个属性_name_
来作为name
属性的封装。要注意的是,如果你定义了get
与set
方法,表示你要自行控制属性的存取,那么就不能再去定义value
或writable
的属性描述。
理解了ES5的对象属性描述符之后,往后我们在对对象的属性处理就可以更加灵活,像是VueJS也是通过Object.defineProperty
的get
与set
来做到双向数据绑定的:
每个组件实例都对应一个watcher实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
图片来源:Vue.js: 如何追踪变化
如果觉得文章对你有些许帮助,欢迎在我的GitHub博客点赞和关注,感激不尽!