本文编写于 173 天前,最后修改于 173 天前,其中某些信息可能已经过时。

说明

  前面三篇博客介绍了js中基本的知识点,包括变量类型及其转换、表达式、运算符等小知识点,这篇博客主要讲的是对象。如果你学过java等语言,你也许在下文的阅读中发现在js中的对象与java中的对象存在一定的区别。

相关术语说明

一、对象中成员变量的”属性特性“

  ①可写:表明该属性可以设置其值。

  ②可枚举:表明是否可以通过for/in循环返回该属性。

  ③可配置:表明是否可以删除或者修改该属性。

注:关于for/in等语句的说明,第一篇博文就说过,本系列教程是建立在读者对基本的编程有一定了解的基础上,所以一些基本的if、for等语句的语法在本系列教程中并不涉及,这样不仅能节约我的时间,也能节约大家阅读的时间。但是对于一些其他语言中少见的语法,在本系列教程中还是会谈到,这里虽然没有谈到for/in语句,但是已给出站外链接,供大家参考。总之鄙人认为有必要让读者了解的东西,要么会出现的自己的博文中,要么会给出站外链接。同样为了节约大家的时间,这里给出的链接一般是内容较好且行文精简的教程。

二、每个对象拥有的“对象特性”

  ①原型(prototype):指向另一个对象,js里面的继承就靠这个prototype实现。与一般的静态编程语言不同,js中的继承出现在对象上,而不是类上。

  ②类(class):标识对象类型的字符串。

  ③扩展标记(extensible flag):指明了该对象是否可以添加新属性。在js中对象的成员属性是可以动态添加的,而不像静态的面向对象的编程语言在编译前就确定了。

三、对象的分类

  ①内置对象:由JavaScript标准提供的对象或类,例如数组、正则等。

  ②宿主对象:由浏览器提供的对象,比如进行DOM操作时常用的document对象,还有进行BOM操作常用的window对象等。

  ③自定义对象:我们自己写的对象。

注:这里对一些属于的解释可能较为抽象,如果实在不明白也没关系,下面会用例子进行实例的讲解。

对象的创建

  js中对象的创建有三种方式,可以通过对象直接量、关键字new和Object.create()函数来创建。

原型
  在介绍对象的创建之前,有必要谈一下对象的原型对象。原型对象原本是属于一个函数的,但我们经常看到对象的原型对象这种说法。一般地,如果某一个函数是构造函数,由这个构造函数创建出来的对象的原型对象就是这个构造函数的原型对象。

  原型对象的出现实现了类似与继承的功能。一般来说每个对象的原型对象是不可访问的,但是在某些浏览器(比如chrome)中提供了对原型对象的访问,其属性名为__proto__。这么说起来你可能觉得比较抽象,难以理解,结合下面的图再讲你可能会更清楚。


 这张图中,Person是一个构造函数,p1和p2都是由这个构造函数创建出来的对象,它们三都指向同一个原型对象(Person prototype由解释器创建)。这里就相当于p1和p2继承于这个原型对象,它俩都共享原型对象的name和age属性,如果这个原型对象还有原型对象,它俩也可以共享,这样就能构造出一条原型链,处于原型链低端的对象可以访问原型链高处的成员属性。上图中p1有自己的name属性,所以它不会再向上访问原型对象的name。

  原型是js中理解对象编程的一个很重要的知识点,如果你觉得还是比较模糊,或者说想更清楚地了解有关原型的内容,强烈推荐你阅读这篇博客。另外上面这张图也是从这篇博客中复制过来的。

对象直接量
  这种创建对象的方式在前几篇的博客中也出现过,就是直接用一个大括号将属性/值括起来,其中属性名可以是任意的字符串,如果属性名是正常的标识符,则可以不用引号括起来,否则必须用引号括起来。属性名和其值之间用冒号分割开。不同的属性之间用逗号分开,具体看下面的例子。

var empty = {};    //一个没有任何属性的对象。
var point = {x:0, y:0};    //拥有两个属性的对象。
var point2 = {x:point.x, y:point.y};
var book = {
    "main title":"javaScript",
    'sub-title':"the definitive guide",
    "for":"all audiences",
    author:{
        firstname:"David",
        lastname:"Flanagan"
    }
};

  这里需要说明的就是point2和book引用的对象,其中point2引用的对象的属性值是一个表达式,js允许属性值是任意有值的表达式。在book中的main title和sub-title不是标识符,所以必须用引号括起来。for虽然是标识符但同时也是保留字,在ECMAScript3中必须用引号括起来,但是ECMAScript5中可以不括起来。值得注意的是对象直接量是一个表达式,这种表达式如果放在循环里面,每次被执行都会创建一个新的对象,且对象的属性值也有可能不同。

关键字new
  new运算符可以后跟一个构造函数来创建一个新对象。如果后面的构造函数没有参数传入,可以省略其后面的括号。

var o = new Object();    //没有参数传入,等价于new Object;
var a = new Array();    //和空数组[]一样。 
var d = new Date();
var r = new RegExp("js");

 除了内置的构造函数,我们也可以自己定义构造函数来初始化新对象,这些内容将在后面的类和模块的博客中进行详细介绍。

Object.create()
  学过java的同学可能知道所有的类都有父类,除了Object类自己。在js中也差不多,所有的对象都有原型对象,除了null。利用Object.create这个静态函数创建对象可以手动指定该对象所继承的原型对象以及其属性。看下面的例子:

 var o1 = Object.create({x:1, y:2});    //o1继承了x和y属性
 var o2 = Object.create(null);    //不继承任何属性
 var o3 = Object.create(Object.prototype);    //等价于{}或new Object()

 这里要注意的是o2这个对象,它的原型对象为null,所以它没有继承到任何属性,这和o3这个对象不同,o3这个对象虽然为{}空对象,但是它还是从它的原型对象那里继承了一些属性的,比如toString()方法等。上面说到的Object.create()还可以用第二个参数给新对象设置属性,这将在本博客的后面讲解。

属性的查询和设置

  js中对对象属性的访问有两种方式,分别利用点(.)和方括号([])进行。其中点运算符只支持对象属性访问,且属性名必须是标识符,而方括号不仅能访问对象的属性,还可以访问数组中的元素,且方括号里面的属性名可以是任意的字符串或者是能转换为字符串的表达式。

var author = book.author;    //等价于book['author']
var title = book['main title'];    //只能用方括号访问。因为'main title'不是标识符,标识符不能含有空格。

 我们平时写的函数名、变量名等就是标识符,关于js中标识符的要求基本上和其它语言差不多,具体请参考本系列教程的第一篇中的”js的词法“部分。

  上面两个例子都是对对象中属性的查询访问,如果需要设置其属性只需将其放在赋值号的左边即可。如果是查询访问,当属性不存(也不存在于原型链中)在会返回undefined,如果是设置其属性,当属性不存在,会在当前对象中创建一个同名属性并设置其值。

  由于在属性访问时不存在的属性会返回undefined,所以这里涉及到一个错误处理的问题,见下面的代码:

//存在问题的代码
var len = book.subtitle.length;

//冗余但易懂的处理方法
var len = undefined;
if(book){
    if(book.subtitle)
        len = book.subtitle.length;
    }
}

//精简的常用方法
var len = book && book.subtitle && book.subtitle.length;

删除属性

  js中有一个delete运算符,用于删除对象的属性,但是它只能删除自有属性,不能删除继承而来的属性,要删除原型链上的属性必须找到那个原型。值得注意的是delete对属性的删除只是解除其指向这个属性值的引用,而不会去操作其属性的属性,因为js中所有内存的释放都是垃圾回收器完成的。见下面的代码:

var a={p:{x:1}};
 var b=a.p;
 delete a.p;    //解除了a.p指向{x:1}这个属性值的引用
 b.x    //1:虽然a.p对{x:1}的引用没了,但是b对{x:1}的引用还在。

  一般情况下使用delete删除属性都将返回一个true,即使是以下这些情况也将返回true:

 var o={x:1};
 delete o.x;    //删除x返回true
 delete o.x;    //什么也没做(x已经不存在了),返回true
 delete o.toString;    //什么也没做(toString是继承来的),返回true。
 delete 1;    //无意义,返回true

  上面说的一般情况,不过存在以下情况的delete语句会返回false。(注意以下情况是在非严格模式下,在严格模式下删除不可配置属性会报错。)

 delete Object.prototype;    //不能删除,属性不可配置
 var x=1;    //声明一个全局变量
 delete this.x;    //全局变量的是不可配置的
 function f() {}    //声明一个全局函数
 delete this.f;    //也不能删除全局函数

检测属性

  属性的检测有三种方法,①通过in运算符、②通过hasOwnProperty()方法、③通过propertyIsEnumerable()方法。

in运算符
  in运算符左侧写属性名(字符串),右侧写对象名,如果这个对象含有或者继承了这个属性,则返回true。

 var o = {x:1}
 "x" in o;    //true
 "y" in o;    //false
 "toString" in o    //true

hasOwnProperty()方法
  只有这个属性是自有属性才返回true,对于继承而来的属性返回false。

 var o = {x:1}
 o.hasOwnProperty("x");    //true
 o.hasOwnProperty("y");    //false
 o.hasOwnProperty("toString");    //false

propertyIsEnumerable()方法
  与hasOwnProperty()方法相比更严格,要求该属性不仅是自有属性,还必须是可枚举的。(下面将讲解什么是可枚举)

其它
  除了以上三种方法用于判断某一对象是否包含某一属性外,还有一种方法如下:

 var o={x:1}
 o.x !== undefined;    //true
 o.y !== undefined;    //false
 o.toString !== undefined    //true

  不过这种方式存在问题,当这个x的值本身就是undefined时,上面o.x !=== undefined这条语句就会返回false。

枚举属性

  所谓可枚举的属性就是使用for/in循环可以遍历到的属性。一般来说我们自己写的属性(包括函数)都是可枚举的,除非用下文提到的一个方法将它转换为不可枚举。注意,可枚举属性不仅可以是自有属性,还可以是继承而来的属性。

  在ECMAScript5的标准中提供了两个用来获取可枚举属性名的函数,第一个是Object.keys()返回对象中所有的自有可枚举属性名组成的组数。第二个是Object.getOwnPropertyNames()返回对象中所有的自有属性名组成的数组。

  先不管definedProperties(下文将会讲),总之上面先创建了一个对象拥有一个x属性值为1可枚举,和一个y属性值为hello不可枚举。然后执行Object.keys()方法和Object.getOwnPropertyNames()方法分别得到了上面这两个数组。

属性setter和getter

  如果你对JavaBean或者说POJO有一定的了解,那么学这两个东西也许还没开始就能猜出十有八九。通常来说对象里面的属性都是键值对(属性名:属性值)的形式存在的,但是js允许某些属性不以这种形式存在,取而代之的是setter和getter。或许这样说很难解释,看下面这个例子你也许会很快了解。

var p = {
//记录笛卡尔坐标系的位置
    x:1.0,
    y:1.0,
   
//r是由x和y转为极坐标后的极径 
//r的setter和getter
    get r() {
        return Math.sqrt(this.x*this.x + this.y*this.y);
    },
    set r(_new) {
        var _old = Math.sqrt(this.x*this.x + this.y*this.y);
        var ratio = _new/_old;
        this.x *= ratio;
        this.y *= ratio;
    },
    
//θ为极角
    get theta() {
        return Math.atan2(this.y, this.x);
    }
};

  上面这个对象我们在外部看来它拥有四个属性,事实上你用Object.keys()函数也能得到["x", "y", "r", "theta"]这个值。然而对象内部真实地却只有两个变量组成的属性x和y。其中属性r和属性theta都是用setter或(和)getter函数模拟出来的。由于theta只有getter,所以这是一个只读属性。

  注意这个例子中setter和getter的书写格式,get或set替代原本用来声明函数的关键字function,并且get和set与函数体之间没有通常键值对用于分割的冒号,这个函数名就是我们模拟出来的对象的属性。

属性的特性
  对象的属性有四个特性:①值、②可写、③可枚举、④可配置。对于由getter和setter模拟出来的属性也有四个特性:①读(get)、②写(set)、③可枚举、④可配置。

获取属性描述符
在ECMAScript5中可以利用Object.getOwnPropertyDescriptor()来获取某一个特定属性的属性描述符,看下面的例子:

  这里使用Object.getOwnPropertyDescriptor()方法对上面定义的“笛卡尔坐标系与极坐标系互转的对象p”进行了属性描述符的获取(在chrome中)。可以看出默认情况下我们定义的属性是可写、可枚举和可配置的。而模拟出来的属性默认也都是可枚举和可配置的。如果它不存在getter或setter,那么在相应的属性位置显示为undefined。如果这个对象(p)不包含该属性(nothing),那么直接返回undefined。

  注意Object.getOwnPropertyDescriptor()该方法只返回自有属性,也就是说要想获得继承属性的特性需要遍历原型链。

设置属性特性
  上面通过Object.getOwnPropertyDescriptor()这个方法可以获取某个对象的属性描述符,如果我们想设置某个对象的某个属性的特性,可以使用Object.defineProperty()这个方法。看下面的例子:

var a = Object.defineProperty({}, "x", {
    value:1,
    writable:true,
    enumerable:true,
    configurable:true});
//上面这段代码等价于 var a = {x:1};

对象的三个属性

  每个对象(null除外)都有与之相关联的三个属性,分别是原型、类和可扩展性。

原型属性
  每一个对象(null除外)都有原型,它在js中是如此的重要,以至于只要你想学好js的对象和函数部分这是你一道跨不过的坎,上文曾稍微谈了一下原型对象,还强烈建议读者阅读这篇博客,如果你对原型对象还不是特别了解,还是建议你去看一下这篇博客,或者其它的讲原型对象的博客,这真的很重要。

  在ECMAScript5中,你可以通过Object.getPrototypeOf()这个方法来获取某个对象的原型对象。也可以使用每个对象的isPrototypeOf()方法来检测继承关系。

1 var a = {x:1};
2 var b = Object.create(a);
3 b.y = 2;
4 Object.getPrototypeOf(b);    //a
5 a.isPrototypeOf(b)    //true

类属性
  类属性是一个字符串,在ECMAScript3和ECMAScript5中并没有提供对这个字符串的修改。对这个字符串的获取就是我们常说的toString()方法。但是某些时候toString()这个函数经常被重写,为了能调用正确版本的toString()方法必须间接调用Function.call()方法。Function()调用方式与Function.call()的调用方式的区别就是函数执行Function这个函数执行时的this指向不同。关于Function.call和Function.apply这两个方法的具体使用将在后面的博客中讲解。如果你有兴趣也可以先参考一下这篇博客。

var a=[1,2];    //定义一个数组
a.toString();    //得到字符串"1,2"
Object.prototype.toString.call(a);    //得到字符串"[object Array]"

可扩展性
  js中的对象的可扩展性直接决定了这个对象是否能添加新属性,如果这个对象不具有可扩展性,那么他将和java等静态语言中的对象一样,无法动态添加属性了。在对象属性的扩展性这方面,ECMAScript5提供了三组常用的函数:

1 Object.isExtensible(a);    //判断对象a是否可扩展。
2 Object.preventExtensions(a);    //设置a为不可扩展
3 Object.isSealed(b);    //判断对象b是否被封闭
4 Object.seal(b);    //封闭对象b
5 Object.isFrozen(c);    //判断对象c是否被冻结
6 Object.freeze(c);    //冻结对象c

  注意上面的设置不可扩展、封闭和冻结等动作都是不可逆的,其中由不可扩展到冻结对属性的限制越来越严。不可扩展只是不允许添加新的属性,封闭不仅不可添加新的属性还将所有的属性都设置为了不可配置,也就说已有的属性也不让删除,而冻结就更严格了,在封闭的基础上还将所有属性设置为了只读(模拟属性中的setter除外)。

序列化对象

  对象序列化是指将对象的状态转化为字符串,也可以将字符串还原为对象。ECMAScript5提供了JSON.stringify()和JSON.parse()来序列化和还原JavaScript对象。JSON的全称是JavaScript Object Notation的缩写,意思是”JavaScript对象表示法“。JSON的语法是JavaScript语法的子集,它并不能表示JavaScript中的所有值。由于序列化这个功能不太常用,所以这里就不展开讲解了,有兴趣的同学可以参考这篇博客。

对象方法

  上文已经谈到过js中的所有对象都从Object.prototype中继承了属性,且这些属性主要是方法。我们前面已经涉及过hasOwnProperty()、propertyIsEnumerable()和isPrototypeOf()这三个方法。静态方法方面涉及了Object.create()和Object.getPrototypeOf()等方法。

toString()方法
  一般将对象转换为字符串时js会自动调用这个方法。比如:"now is " + new Date();这句话中js会自动调用日期这个对象的toString()方法将其转换为字符串再与前面的字符串相连接。该方法经常被重写,在后面的博客中将介绍怎么重写这个方法。

valueOf()方法
  这个方法和上面的toString()方法差不多,一般也是被js自动调用在期待数字的时候,比如:23==new String("23");这个比较中js会自动调用new String("23")这个对象的valueOf方法,将其转换为字符串再进行判等比较。

链接:
JavaScript 基础入门教程(一) http://blog.zh66.club/index.php/archives/81/
JavaScript 基础入门教程(二) http://blog.zh66.club/index.php/archives/83/
JavaScript 基础入门教程(三) http://blog.zh66.club/index.php/archives/87/