深入理解js面向对象——创建对象

来源:互联网 发布:群晖网络唤醒端口 编辑:程序博客网 时间:2024/05/17 00:53
本博客原文在这

由于js这门语言是解析型语言的特性,导致js中的对象更加灵活,更像是一些 属性的集合,或者说类似于散列表或者字典这些数据结构.所以我们可以在使用的时候随着需求对对象中的属性进行增删改查,这完全是动态的不是编译好的.那么 js中的对象是怎么表示的? js对象的内部表示原理是什么? js创建对象有几种方式? 他们的内部执行过程是什么? 通过这篇文章,可以一探究竟.

思维导图如下:

## 一、ES中的对象由于js这门语言是解析型语言的特性,导致js中的对象更加灵活,更像是一些 **属性的集合** ,或者说类似于散列表或者字典这些数据结构.所以我们可以在使用的时候随着需求对对象中的属性进行增删改查,这完全是动态的不是编译好的.在面向对象中,对象的特征就是有一些行为方法,有一些特征属性,对象是分类型的,某些对象由于相似的操作多被归为一类.在ES标准中是怎么践行面向对象的呢? 在js中方法看做属性,所以对象分为了两种属性,一张是命名的用户自定义的属性,分为命名的数据属性和命名的访问器属性;还有一种是为了支持js语法而设置的内部属性,能够满足js语言的某些特性. 任意一个对象的内部属性中有prototype属性,这个属性是一个对象,指向了该对象的类中那些公共的方法.所以在js中使用原型的方式定义了对象的继承性.在js中对象之间的赋值是引用赋值的,也就是使用对象的指针赋值.
var a = {"M":1}, b = a;b.M = 0;console.log(a.M);  /* 0 */
这段代码在内存中创建了一片空间来存放为对象{“M”:1},a变量指向了这个空间,相当于把{“M”:1}对象的地址给了a,然后b也被赋值为这个地址,所以a和b都是指向同一个对象,所以说这两个变量无论哪一个进行修改都是修改的{“M”:1}.#### 1. 命名的数据属性对于数据属性或者下面介绍的访问器属性,都是描述对象属性的一些特性,因为有他们的存在,我们更好的去管理对象的属性,比如定义属性是否可写,是否只读,是否可枚举是否可删除等.命名的数据属性比较常见,对于使用对象字面量的方式定义的对象中,默认情况下属性为数据属性.用伪代码描述命名的数据属性为:
DataPropertyName : {  value: Data types in ES (boolean,number,string,Object,function,......),  writable: boolean,  enumerable: boolean,  configurable: boolean}
命名的数据属性由一个属性名和一组描述属性的特性组成.属性名就是普通的字符串类型,特性由value,writable,enumberable,configurable组成.对于value来说值为ES规范中定义的任意的数据类型,如果是对象的话将会用一个指针指向这个对象;writable定义了这个属性是否可写,从而能够定义只读属性;enumerable定义了这个属性是否能够被枚举到;configurable定义了是否可以去配置这个属性的特性,如果为true,可以改变writable和enumerable值,否则是不能改变的,很重要的是如果为false,则这个属性是不能使用delete删除的.#### 2. 命名的访问器属性用伪代码描述命名的访问器属性:
accessorPropertyName: {  get: function | undefiend,  set: function| undefiend,  enumberable: boolean,  configurable: boolean}
相比起命名的数据属性而言,访问器属性没有value特性取而代之的是get和set方法,enumberable和configurable是相同的,不再做介绍,接下来主要介绍下get和set.get: 当这个属性作为右值 **取值** 操作的时候就会调用该方法,调用这个方法的[Call]内部属性,传入 **运行** 时的该属性所属的对象作为this,参数列表为空,然后执行函数,返回值作为取得的值,如果没有返回值则为undefined.set: 当这个属性作为左值被 **赋值** 的时候调用这个函数,调用这个方法的[Call]内部属性,传入 **运行** 时的该属性所属的对象作为this,参数列表为被赋的值,然后执行函数.
var a = {  _property:null,  set property(newValue){    console.log("set property");    return this._property = newValue;  },  get property(){    console.log("get property");    return this._property;  },  get name(){  }}console.log(a.name);//undefinedconsole.log(a.name = "maotr");//Cannot set property name of #<Object> which has only a getterconsole.log(a.property);//get property nullconsole.log(a.property = 12); //set property 12var b = Object.create(a);console.log(b.property);//get property 12b.property = -12;console.log(b.property);//get property -12console.log(a.property);//get property 12
这是使用对象字面量的形式定义了一个对象,这个对象中有两个访问器属性分别为property和name,property有get和set方法,name只有get方法.其中数据类型属性_property作为property访问器属性操作的对象存在.
console.log(a.name);//undefined
访问a的name属性,是对a.name执行右值操作,因为name为访问器属性,所以调用name的get方法,a作为get函数的this,没有参数,因为没有返回值所以最终返回undefined.
"use strict";console.log(a.name = "maotr");//Cannot set property name of #<Object> which has only a getter
当在严格模式下为a.name赋值操作,相当于访问name的set方法,但是由于没有定义name的set方法此时set默认值不是函数而是undfined,所以在严格模式下会报错.这也给我们一种启发就是,当为一个对象的属性想仅仅 **只读** 的话可以不定义set方法, 相对应的数据类型属性可以将value特性设置为false.
console.log(a.property);//get property nullconsole.log(a.property = 12); //set property 12
这两行代码就是分别用a作为this调用property属性的get和set方法.
var b = Object.create(a);
这行代码是使用create函数创建一个对象,下面会详细说到,在这里简单说一下就是,创建一个对象b,该对象的继承了b的所有属性,所以b可以访问property和name属性.
console.log(b.property);//get property 12
由于上一部中通过a作为this将property设置为12,所以这里访问property的时候也就是12了,也就是所b访问了_property,注意只不过这里访问property中get函数的this为b了不是a.
b.property = -12;console.log(b.property);//get property -12console.log(a.property);//get property 12
那么为什么我通过b设置property为-12,使用b get property为-12这是符合常理的,为什么a get property还是12不是-12?这个问题涉及到数据属性和访问其属性的很重要的区别那就是, **原型链上数据属性只能get不能set,如果可以set,则在自身属性上创建覆盖原型上的值, 访问器属性可以get和set**这个详细解释会在下一篇文章 **深入理解js面向对象——访问设置对象** 中详细说明.在这里只要明白不用对象调用访问器属性时,getset方法调用的this是不一样的是由运行时决定的,就好比虽然在a中定义le访问器属性,但是b访问访问器属性的时候的this为b不是a.至于为什么 将会 **深入理解js函数–this** 敬请期待吧.有一种需求:当用户输入商品的数量时,改变总价,订单shuliang,库存等信息,之前方式就是检测下用户keyDown事件,如果发生了利用观察者模式事件驱动通知各个观察者进行相应的变化.对于这种某个变量值变化通知其他元素变化的需求下使用访问器属性是比较好的,性能比起事件要好的多.代码就不详细说明了,在博文 **设计模式–观察者模式** 中会详细介绍.#### 3. 如何设置命名属性的特性在ES3中所有属性的特性是不能设置的,默认都为true,在ES5规范中引入了几个函数,能够访问属性的特性,下面分别介绍:1. Object.getOwnPropertyDescriptor(O, p) 获得特性描述对象这个方法是对内置内向Object类方法,所以不必通过new一个对象就能使用,传入两个参数分别问,一个要处理的对象,和该对象的属性名.返回一个特性描述的对象
"use strict";var a = {  _property: null,  name: "maotr",  set property(newValue) {    console.log("set property");    return this._property = newValue;  },  get property() {    console.log("get property");    return this._property;  }}console.log(Object.getOwnPropertyDescriptor(a, "name"));console.log(Object.getOwnPropertyDescriptor(a, "property"));console.log(typeof Object.getOwnPropertyDescriptor(a, "name"));//Object/*结果为:{  value: 'maotr',  writable: true,  enumerable: true,  configurable: true}{  get: [Function: get property],  set: [Function: set property],  enumerable: true,  configurable: true}*/
2. Object.defineProperty(O, p, Attributes) 自定义对象属性的特性
var a = {};Object.defineProperty(a, "property", {  set property(newValue) {    console.log("set property");    return this._property = newValue;  },  get property() {    console.log("get property");    return this._property;  },  enumerable: true,  configurable: true});Object.defineProperty(a, "name", {  value: "maotr",  writable: true,  enumerable: true,  configurable: true});
这个对a的定义方式与上面用对象定义方式等价.3. Object.defineProperties(O, Properties);
Object.defineProperties(a, {  "property": {    set property(newValue) {      console.log("set property");      return this._property = newValue;    },    get property() {      console.log("getgsd property");      return this._property;    },    enumerable: true,    configurable: true  },  "name":{    value: "maotr",    writable: true,    enumerable: true,    configurable: true  }});
这种方式对于定义多个属性的特性很有帮助.#### 4. 内部属性对于js中 **任意的一个** 比如 function,array,object 等 类型的对象都会有一些共同的属性,这些属性不会是公开的为我们使用(ES通过使用部分操作可以设置这些属性),但是他对于维护js语言的特性和语言很好的工作是密不可分的.其实js内对象的内部属性有很多这篇博客知识介绍非常重的两个prototype和class , 其他的与属性的查找和设置相关的属性将会在 **深入理解js面向对象——访问设置对象** 中介绍.###### 1. [prototype]js中 **原型** 和 **原型链** 的概念,是基于prototype内置属性的,对于面向对象开发来说原型这个概念是非常重要的.将在这篇博客中介绍原型的本质是什么,在下一片博客 **深入理解js面向对象——访问设置对象** 将会介绍属性的查找也就是所原型链的本质. **原型**:我们之前已经介绍了对象是如何定义方法和属性的,但是还没有介绍对象如何继承公共的类的,没错,在js中一个对象的继承就是使用[prototype]内部属性来实现的,那么他是如何工作的呢?**原型链**:[prototype]属性,是所有的对象都具有的内部属性,这个属性的值是null或者一个对象,这个对象包含了该对象继承的所有的公共的方法或者属性,从而能够实现继承,由于[prototype]也可以是对象,所以能够形成原型链,但是原型链必须有尽头,即最后一个对象的[prototype]为null.

会看到 js内置的一些对象都是有原型的他们的原型是一些对象,比如object的原型就是Object.prototype, array原型为Array.prototype,Array.prototype又有原型为Object.prototype.那么对于原型链的尽头就是Object.prototype.prototype 为 null.

同时我们也可以自己定义一些类new一些对象,让这些对象去继承这个类.可能会涉及函数一些知识或接下来描述的new操作符的一些知识.在这里仅仅是了解[prototype]原型即可.

function People(name){  this.name = name;}People.prototype = {  constructor: People,  eat: function(){    console.log(this.name + "在吃饭");  }};var Tom = new People("Tom");var Jery = new People("Jery");Tom.eat();Jery.eat();/*Tom在吃饭Jery在吃饭*/

这里可以看出Tom对象本身有属性name,他的prototype为People.prototype,People.prototype的prototype为Object.prototype,Object.prototypede prototype为null.原型链结束.

检测:在ES中定义了几个函数能够取得或者检测一个对象的原型是什么,因为一个对象的类型是由继承的类决定的,一个类的特性体现在被继承的公共的方法上,所以想要检测一个对象的类型只要比较他们原型是一个对象,或者原型链上具有这个对象.

  1. Object.getPrototypeOf(O) 返回o对象的[prototype]值
Object.getPrototypeOf(Tom);//People { constructor: [Function: People], eat: [Function: eat] }
  1. Object.prototype.isPrototypeOf(V)
    这个方法要注意两点,第一点就是这个方法是Object.prototype下的,所以要使用就是通过具体的对象来调用(为什么?想想上面说道的原型链).第二注意的就是这个方法的执行过程:

首先检查V是否是一个对象,如果不是直接返回false;
对于O(即调用isPrototypeOf方法的对象)来说,将O尽可能的转化成对象.比如数字会转化成number对象;
在V的原型链上查找有没有O对象,知道原型链的尽头null位置,有返回true,否则返回false;

console.log(People.prototype.isPrototypeOf(Tom));//trueconsole.log(Tom.isPrototypeOf(People.prototype));//false  注意不要写反console.log(Object.prototype.isPrototypeOf(Tom));//trueconsole.log(String.prototype.isPrototypeOf( Object("s") ) );//true  注意如果检测内置基本类型的话要转化成对象,通用是使用Object(value)
2. [class]

class内部属性的值代表着这个对象是什么 内置对象 类型,是function,array,string,number,object,等?注意是内置对象也就是说通过自定义的任何的对象他的class都是Object.

对于这个内置对象只能通过 Object.prototype.toString() 来得到这个class值,不能通过Array中的toString,因为在Array中已经对Object中的toString方法进行了覆盖,同时返回的是 [object Class] 的类型的字符串, 具体来说 对于函数Object.prototype.toString他的执行过程是这样的,如果传入的是null返回[object Null],如果是undefined返回[object Undefined],如果是非对象则转化成对象,如果是对象返回对应的字符串描述.比如[object Object]或则[object Array].可以通过array中的slice来截取获得.

注意这个函数没有参数,所以如果想检测某个对象但是还要使用原生的方法则通过call调用.同事只有使用这种方式才能检测undfined和null.

function getClass(o){  return Object.prototype.toString.call(o).slice(8,-1);}console.log(getClass(null)); //Nullconsole.log(getClass(undefined));//Undefinedconsole.log(getClass(12));//Numberconsole.log(getClass("s"));//Stringconsole.log(getClass([]));//Arrayconsole.log(getClass({}));//Objectconsole.log(getClass(Tom));//Object

注意的是最后一个输出不是People而是object,所以这也验证了class仅仅能表示内置对象.

3. [GetOwnProperty],[GetProperty],[Get],[Put]等内部属性在下一篇博客中介绍.

二、如何创建对象

好把以为写ES中的对象几句话就能说清楚的没想到写了这么多,这个铺垫够长的,不过这些也是最最基本的.知道了js中对象是什么样子之后,看一下js语法提供了那些手段去创建对象?也就是说这些手段是如何定义属性的,是如何定义内部属性prototype,class等的.

1. 对象直接量

通过对象直接量来定义对象是最简单的方式,这种方式将会创建一个Object的对象,也就是说创建的对象的prototype为Object.prototype,class为”Object”,并且定义的自定义属性可以是数据属性或者访问器属性,他们的特性值都是true.

var a = {  _property:null,  set property(newValue){    console.log("set property");    return this._property = newValue;  },  get property(){    console.log("get property");    return this._property;  },  name: "maotr"}console.log(Object.getOwnPropertyDescriptor(a, "property"));console.log(Object.getOwnPropertyDescriptor(a, "name"));/*{  get: [Function: get property],  set: [Function: set property],  enumerable: true,  configurable: true}{  value: 'maotr',  writable: true,  enumerable: true,  configurable: true}*/

值得注意的是,当执行这个对象字面量定义语法每一次都会创建一个Object的对象,然后对定义的属性值或者表达式都会重新执行一遍然后添加为对象的属性.所以说:

function getObject(){  return {    fun:function(){}  }}var a = getObject();var b = getObject();console.log(a === b, a.fun === b.fun);//false false

很显然这种方式创建对象是很消耗内存的,因为他们的方法属性不共享.再说也不会体现出继承性,因为他们的prototype都是Object的原型属性值.

2. new 创建对象

还有一种比较不错的方式就是通过new的方式,那么new这个运算符是如何工作的呢?也就是说他是如何确立对象的原型值和属性的呢?

function People(name){  this.name = name;}People.prototype = {  constructor: People,  eat: function(){    console.log(this.name +"正在吃饭");  }}var Tom = new People("Tom");

以这个例子来说一下new关联自是如何工作的:

首先会取得new操作对象 People的值为一个对象,然后检测下这个对象是否是一个可以调用的函数,发现确实是,那么就会调用函数的内部属性[Construct],在这个属性中详细的去执行n创建对象的操作.

  1. 创建一个Object的对象实例obj,也就是说此时的prototype为Object原型,class为Object,注意这里的class,在接下来步骤中会改变prototype的值,但是clas不会改变,这也就是为什么通过toString 的方式得到的返回值依然是Object了.
  2. 取得该函数的prototype自定义属性的值为F(至于为什么函数有一个这个属性 会在函数博客中介绍)
  3. 如果F为 一个不是null的对象,那么obj的prototype属性值设置为F
  4. 否则不进行处理,也就是默认的Object的原型对象. (这一步就是为对象定义继承)
  5. 开始调用函数的call内部方法执行函数,注意 this为对象obj (这一步就是为对象定义属性)
  6. 如果函数返回值为一个对象那么返回这个对象,否则返回创建的obj

所以说,Tom对象的prototye是People.prototype,class还是Object,属性中存在name.

这种方式比起第一种方式要好很多至少定义了对象的继承性,但是这种方式的属性和第一种一样特性默认都为true.

3. create()函数创建对象

Object.create()函数是ES5中定义的一个函数,这个函数返回一个对象,同事可以显示的定义这个对象的继承原型值和一些具有自定义特性的属性.

var People = {  eat: function(){    console.log(this.name +"正在吃饭");  }}var Tom = Object.create(People, {  "name": {    value:"Tom",    writable: true,    enumerable: true,    configurable true  }});``这种方式可以显示的定义了对象Tom的属性的也行,也很好的继承了People eat方法,是不是感觉这种定义方式更加的符合js语法的习惯, 这种方式也叫作原型继承.这个函数得到第一个参数只能是null或者任意的对象.在早起浏览器不兼容这种方式,只能通过自定义create函数来实现.

Object.create = function(o /properties/){
console.log(Object.prototype.isPrototypeOf(o));
if(!( Object.prototype.isPrototypeOf(o) ) && o !== null) {
throw Error(“object’s prototype may be an object or null”);
}
var _Tmp = function(){};
_Tmp.prototype = o;
var obj = new _Tmp();

if ( arguments[1] !== undefined ){
var key = Object.getOwnPropertyNames(arguments[1]);
for (var i = 0, len = key.length; i < len; i++) {
obj[key[i]] = arguments[i][key];
}
}
return obj;
};
“`

4. ES6中Class创建对象