首页 > 代码库 > 深入浅出 JavaScript 对象 v0.5

深入浅出 JavaScript 对象 v0.5

JavaScript 没有类的概念,因此它的对象与基于类的语言中的对象有所不同。笔者主要参考《JS 高级程序设计》、《JS 权威指南》和《JS 精粹》

本文由浅入深的讲解了对象的概念,特性,和使用,由于笔者水平的确有限,有些观点也是边理解,边查证,边分享。

希望大家都能感受到分享的乐趣,祝我们共同进步,请大家不吝交流。



目录

  • 对象是什么?
  • 对象有什么特性?
  • 对象有什么用?
  • 如何创建对象?
  1. 对象直接量
  2. 工厂方法创建对象
  3. 通过 new 创建对象
  • 对象属性的查询与设置(检索与更新)
  • 删除对象的属性
  • 反射
  • 枚举属性
  • 对象的三个属性
  • 序列化对象
  • 写在后面的话

对象是什么?

ECMA-262(JS的标准)把对象定义为“无序属性的集合,其属性包含基本值、对象和函数”。

JavaScript中的对象是可变的键控集合(keyed collections)(注意,是可变的哟)。

对象是属性和方法的容器(貌似在任何一门支持对象的语言中都是),每个属性都拥有名字和值。

属性的名字(key)可以包括空字符串在内的任意字符串,属性值可以是除undefined值之外的任何值。

对象有什么特性?

  • 动态性:可以新增也可以删除属性,但常用来模拟静态对象和静态类型语言中的结构体,有时还用做字符串的集合(忽略名值对的值)。
  • 无序性:在枚举对象的属性时,属性是没有顺序的。
  • 无类型:对象适合用于汇集和管理数据。对象可以包含其他对象,所以它们很容易表示树形结构或图形结构。
  • 原型链特征:允许对象继承另一个对象的属性。正确地使用它能减少对象初始化时消耗的时间和内存。

对象有什么用?

对象最常用的用法就是创建、设置、查找、删除、检测和枚举它的属性。另外还有一些高级操作。

如何创建对象?

可以通过对象直接量、工厂方法、new 关键字来创建对象。(另外《权威指南》 上说Object.create() 函数也可以创建自定义对象)

对象直接量

对象直接量是由若干名值对组成的映射表,名值对中间用冒号隔开,名值对之间用逗号分隔,整个映射表用花括号括起来。

属性名可以是JavaScript 标识符或者字符串直接量(包括空字符串),属性的值可以是任意类型的JavaScript 表达式,表达式的值就是这个属性的值。

对象直接量是一个表达式,该表达式每次运算时都创建并初始化一个新的对象,也都会计算它每个属性的值(注意在循环体中使用对象直接量)。

var tony = {
    name: ‘tony‘,
    age: 23,
    contacts: {
        tel: ‘12345566‘,
        QQ: ‘55555‘ 
    }
};

其中 ECMAScript 5 中 字面量最后一个名值对后面的逗号将被忽略,但是IE中会报错。

工厂方法创建对象

用函数来封装以特定接口创建对象的细节。

function createPerson (name, age, job) {
    var o = new Object ();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayHello = function () {
        alert("hello, I‘m "+this.name);
    };
    return o;
}

var tony = createPerson(‘tony‘, 23, ‘engneer‘);

工厂方法虽然解决了创建多个相似对象的问题,但是却没有解决对象识别的问题,不知道一个对象属于那种类型,没有对象的类型信息。

通过 new 创建对象

new 运算符 创建并初始化一个对象,关键字 new 后面跟一个函数调用(该函数被称为构造函数,构造函数用来初始化一个新对象)。
JavaScript语言核心中的原始类型都包含内置构造函数。

var o = new Object();    //  创建一个空对象 {} 还有 Array() Date() RegExp()

除了内置构造函数外,还可以自定义构造函数,例如:

function Person /*首字母大写主要是区别普通函数*/ (name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayHello = function () {
        alert("Hello  I‘m "+ this.name);
    };
}
var tony = new Person (‘tony‘, 23, ‘engneer‘);
var googny = new Person (‘googny‘, 23, ‘manager‘);
googny.sayHello == tony.sayHello; // false

要创建 Person 的新实例,必须使用 new 操作符,这种方式调用构造函数实际上经历四个步骤:

  • 创建一个新对象;
  • 将构造函数的作用域赋给新对象(this 就指向这个新对象)
  • 执行构造函数中的代码
  • 返回新对象

OK,上面的例子的确可以创建并初始化新的对象,有心人可能会注意到我最后三行代码,googny.sayHello 和 tony.sayHello 不相等。

也就是说该函数在每个对象中都有一份拷贝,熟悉类C语言的童鞋都知道,类中的方法方法只有一份拷贝,各个对象共享这份拷贝,调用过程中将 this 隐式的传递给该方法。

这种情况可以在 JavaScript 中实现吗? 是的,可以

var sayHello = function () {
        alert("Hello  I‘m "+ this.name);
 };

function Person /*首字母大写主要是区别普通函数*/ (name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayHello = sayHello;
}
var tony = new Person (‘tony‘, 23, ‘engneer‘);
var googny = new Person (‘googny‘, 23, ‘manager‘);
googny.sayHello == tony.sayHello; // true

从返回结果可以看出,两个对象共享一段代码,但是这样看起来封装性不是特别的好,用起来也憋手蹩脚的,《悟透 JavaScript》 中用“不优雅”来形容。

JavaScript 的设计者也觉得上面的两种做法是浪费“粮食”(内存),而且丑陋,于是“原型” 应运而生。

什么是原型?

我们创建的每个函数都有一个 prototype (原型) 属性, 这个属性是一个指针,指向一个对象,而这个对象的用途就是用来包含可以有特定类型的所有实例共享的属性和方法。

使用原型对象的好处就是可以让所有实例共享它所包含的属性和方法。

function Person /*首字母大写主要是区别普通函数*/ (name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
}

Person.prototype.sayHello = function () {
    alert("Hello  I‘m "+ this.name);
};
var tony = new Person (‘tony‘, 23, ‘engneer‘);
var googny = new Person (‘googny‘, 23, ‘manager‘);
googny.sayHello == tony.sayHello; // true

在默认情况下,所有原型对象都会自动获取一个constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。

很绕是吧,看一张清爽无比的高清大图

//    为了说明Prototype 中 constructor 指向Person函数
var some = new Person.prototype.constructor(‘somebody‘,25,‘boss‘);
console.log(some);  

当然,上述小代码只是为了说明问题,该属性主要是用来判断对象的类型。

tony.constructor == Person;    //constructor先从对象的属性中找,没有的话,再从tony 的原型中找。
tony instanceof Person;

组合使用构造函数和原型模式来创建新对象

function Person (name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
}

Person.prototype = {
    constructor : Person,  //以对象形式给prototype 赋值,要想有类型判断,则必须指定constructor
    sayHello : function () {
        alert("hello,  my name is "+this.name);
    }
};

var tony = new Person (‘tony‘, 23, ‘student‘);

tony instanceof Person;

该方式是目前使用最广泛,认同度最高的一种创建自定义类型的方法。

对象属性的查询与设置(检索与更新)

要检索对象里包含的值,可以采用在[]后缀中括住一个字符串表达式的方法。如果字符串表达式是一个字符串字面量,而且它是一个合法的JavaScript 标识符而不是保留字,那么也可以用 . 表示法代替。 

var tony = {
    "first-name" : ‘tony tong‘,
    departure : ‘xidian university‘,
    contacts : {
        tel : ‘123456767‘,
        QQ : ‘55555‘
    }
};

tony [‘first-name‘] ;    // ‘-‘标识符中不合法哟
tony.contacts.tel ; 

如果你尝试查询一个并不存在的成员属性的值,将返回 undefined

|| 运算符可以用来填充默认值:

var middle = tony["middle-name"] || "(none)"

尝试从undefined 的成员属性中取值会导致TypeError 异常,这时可以通过 && 运算符来避免错误。

tony.age;     // undefined 
tony.age.birth    // TypeError 异常
tony.age && tony.age.birth    //undefined

对象里的值可以通过赋值语句来更新,如果属性名已经存在于对象里,那么这几个属性的值就会被替换。

如果之前对象中没有那个属性,那么该属性就被扩充到对象中。

删除对象的属性

delete 运算符可以用来删除对象的属性。如果对象包含该属性,那么该属性就会被移除,它不会触及原型链中的任何对象。

删除对象的属性可能会让来自原型链中的属性显现出来。

function Person (name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayHello = function () {
        alert("hello, I‘m a person!");
    };
}

Person.prototype = {
    constructor : Person,  
    sayHello : function () {
        alert("hello,  my name is "+this.name);
    }
};

var tony = new Person (‘tony‘, 23, ‘student‘);

tony.sayHello();    // hello, I‘m a person!
delete tony.sayHello;
tony.sayHello();    // hello, my name is tony

反射

反射指的是程序在运行时能够获取自身的信息。例如一个对象能够在运行时知道自己有哪些方法和属性。

检查对象并确定对象有什么属性是很容易的事儿,只要试着去检索该属性并验证取得的值。

typeof 操作符对确定属性的类型很有帮助;

typeof tony.sayHello    // ‘function‘
typeof tony.contacts    // ‘object‘

使用 hasOwnProperty 方法来检查对象是否拥有该属性, 如果对象拥有独有的属性, 它将返回true, 该方法不会检查原型链

枚举属性

for in 语句用来遍历一个对象中的所有属性名。该枚举过程将会列出所有的属性—— 包括函数和可能不关心的原型中的属性——所以有必要过滤掉那些不想要的属性,

最为常用的过滤器是 hasOwnProperty 方法,以及使用typeof 来排除值为函数的属性

var property;
for (property in tony) {
    if ( tony.hasOwnProperty(property) && typeof tony[property] != ‘function‘) {
        console.log(property);
    }
}

属性名出现的顺序是不确定的。

对象的三个属性

每一个对象都有与之相关的原型(prototype)、类(class)、和可扩展性(extensible attribute)

原型属性是一个指向原型对象的指针,主要用来在同一类对象中共享属性和方法,还可以用来继承属性。

对象的类属性是一个字符串,用以表示对象的类型信息,只有一种间接的方法来查询它。

默认的toString()方法,返回如下格式的字符串:

[object class]

因此要想获得对象的类,调用对象的toString 方法,然后提取字符串 从第8个字符到倒数第二个字符之间的字符串。

不过让人棘手的是,很多对象的toString() 方法都被重写了,为了能调用正确的toString() 版本,必须使用函数的间接调用模式(参考深入浅出 JavaScript 函数 中的间接调用模式)

例如:

function classOf (o) {
    if (o == null)  return ‘Null‘;
    if (o == undefined) return ‘Undefined‘;
    return Object.prototype.toString.call(o).slice(8,-1);
}

该函数可以接受任何类型的参数。

对象的可扩展性用以表示对象是否可以给对象添加新属性。所有内置对象和自定义对象都是显式可扩展性的。

宿主对象的可扩展性是由JavaScript 引擎定义的,在ECMAScript 5 中,所有的内置对象和自定义对象默认都是可扩展的,除非将它们转换为不可扩展的。

通过将对象传入 Object.esExtensible(),来判断该对象是否是可扩展的。

如果想将对象转换成不可扩展的,则需要调用Object.preventExtensions(),将待转换的对象作为参数传进去。(此过程是不可逆的

如果想将对象所有的自有属性设置为不可配置的,也就是说不能增加新属性,删除、设置它的已有属性,则需要调用Object.seal()。不过它已有的可写属性依然可以设置。

Object.freeze() 将对象所有属性设置为只读的。 Object.isSealed() 检测是否封闭,Object.isFrozen() 来检测对象是否被冻结。

序列化对象

对象序列化,是指将对象的状态转换为字符串,也可将字符串还原成对象。

ECMAScript 5 提供了内置函数JSON.stringify() 和 JSON.prase()用来序列化和反序列化JavaScript 对象。

var o = {
    x: 1,
    y: {
        z: [false, null, ""]
    }
};    //定义一个测试对象

var s = JSON.stringify (o);    //s 是一个字符串
console.log(s);
var p = JSON.parse(s);    //p是o的深拷贝
console.log(p);

写在后面的话

其实关于JavaScript 对象的话题远远不止这些,比如一些比较高级的话题:对象属性的特性对象属性getter和setter

有兴趣的读者可以找相关资料查阅。

谢谢大家,看着觉的说得过去的话,点个推荐吧。