JavaScript对象类型详解

《JavaScript高级程序设计》已经学习到了第四章,不过因为第五章讲的都是各种对象类型,所以在进行第五章的学习之前,先深入了解一下对象是有好处的。

JavaScript Objects in Detail

关于对象类型的方方面面在这篇文章里都写得很清楚了,本着不重复造轮子的原则,我这里也不打算再重新写一篇了,更何况,我这新手写出来的文章肯定也跟人家的没得比。

鉴于很多朋友可能对英文不是很感兴趣,所以这里准备把文章翻译过来。不过提前声明,本人没有什么翻译经验,翻译出来的文章可能水平欠佳。如果英文比较好的话,建议直接看原文。毕竟是新的尝试,各位高手如果有啥建议或者意见可以在评论提出,但请勿无故乱喷。


JavaScript对象类型详解

JavaScrtip有六种数据类型,一种复杂的数据类型(引用类型),即Object对象类型,还有五种简单的数据类型(原始类型):NumberStringBooleanUndefinedNull。其中,最核心的类型就是对象类型了。同时要注意,简单类型都是不可变的,而对象类型是可变的。

什么是对象

一个对象是一组简单数据类型(有时是引用数据类型)的无序列表,被存储为一系列的名-值对(name-value pairs)。这个列表中的每一项被称为 属性(如果是函数则被称为 方法)。

下面是一个简单的对象:

    var myFirstObject = { 
        firstName: "Richard",
        favoriteAuthor: "Conrad"
    };

可以把对象考虑成一个列表,列表中的每一项(属性或方法)都以名-值对的方式存储。上面例子中,对象的属性名就是firstNamefavortieAuthor,相应的,对象的属性值为RichardConrad

属性名可以是字符串或者数字,但是如果以数字作为属性名,则必须以方括号(方括号记法)来获得这个数字属性名对应的属性值。稍后有方括号记法的更详细解释。下面是一个方括号记法的例子:

    var ageGroup = {30: "Children", 100:"Very Old"};
    console.log(ageGroup.30) // 报错
    ​// 访问30这个属性的正确方法
    console.log(ageGroup["30"]); // Children​
    ​
    ​//最好避免使用数字作为属性名

作为一个JavaScript程序员,你会经常使用到对象数据类型。一般用它来储存数据,或者创建自定义的方法或函数。

引用数据类型和原始数据类型

引用类型与原始类型最主要的一个不同点就是引用类型是按引用存储的,它不会像原始类型一样,将值直接存储在变量中。比如:

    // 原始类型数据是按值存储的 
    ​var person = "Kobe";  
    ​var anotherPerson = person; // anotherPerson = the value of person​
    person = "Bryant"; // person的值改变了
    ​
    console.log(anotherPerson); // Kobe​
    console.log(person); // Bryan

可以注意到,即使我们将person的值改为"Bryant",对anthoerPerson也会不有丝毫影响,它仍然保存了原本person赋给它的值。

将原始类型的按值存储跟引用类型的按引用存储进行一下比较:

    var person = {name: "Kobe"};
    ​var anotherPerson = person;
    person.name = "Bryant";
    ​
    console.log(anotherPerson.name); // Bryant​
    console.log(person.name); // Bryant

在这个例子中,我们将person对象复制给了anthoerPerson,但是由于person对象中存储的是引用而不是真正的值。所以当我们将person.name改变为"Bryant"的时候,anotherPerson变量也反应出了这个变化,因为它并没有将person中的所有属性都复制一份保存起来,而是直接保存了对象的引用。

对象属性的特性(Attributes)

注:Attribute一般也是翻译为属性,但是为了跟Propertie(也翻译为属性)进行区分,这里将其翻译为特性,这也是咨询过别人的,应该无伤大雅

每个对象属性不止保存了自身的名-值对,它同时还包含了三个特性,这三个特性默认被设置为true。

  • Configurable Attribute: 指定这个对象属性是否可以被删除或修改。
  • Enumerable:指定这个对象属性在for-in循环中是否可以被取得。
  • Writable:指定这个对象属性是否可以被修改。

在EMACScript 5中有一些新的特性,这里不做详细讲解。

创建对象

创建对象有两种比较常用的方法:

  1. 对象字面量

    这是创建对象最常用,也是最简单的方式,直接使用字面量进行创建:

     // 空对象
     ​var myBooks = {};
     ​
     ​// 使用字面量创建的包含4个属性的对象
     ​var mango = {
         color: "yellow",
         shape: "round",
         sweetness: 8,
     ​
     ​    howSweetAmI: function () {
             console.log("Hmm Hmm Good");
         }
     }
    
  2. 对象构造函数

    第二种常用的方法是使用对象构造函数。构造函数是一种可以用来创建新对象的特殊函数,要使用new关键字来调用构造函数。

     var mango =  new Object ();
     mango.color = "yellow";
     mango.shape= "round";
     mango.sweetness = 8;
     ​
     mango.howSweetAmI = function () {
         console.log("Hmm Hmm Good");
     }
    

虽然可以使用某些保留字或关键字,比如for作为对象属性的名称,不过这可不是一个明智的选择。

对象的属性可以包含任何数据类型,包括NumberArrays,甚至是其它的Object

对象创建的实践模式

对于创建只使用一次的用于存储数据的简单对象,上面的两种方法就可以满足需求。

但是,假设有一个程序用于展示水果和它的详细信息。程序中的每个水果类型都有如下对象属性:color, shape, sweetness, cost 和一个showName函数。要是每次创建一个新的水果对象时,都得敲一遍下面的代码,那将是十分乏味和低效率的。

    var mangoFruit = {
        color: "yellow",
        sweetness: 8,
        fruitName: "Mango",
        nativeToLand: ["South America", "Central America"],
    ​
        ​showName: function () {
            console.log("This is " + this.fruitName);
        },
        ​nativeTo: function () {
            this.nativeToLand.forEach(function (eachCountry)  {
                console.log("Grown in:" + eachCountry);
            });
        }
    }

如果你有10个水果,你就得添加10次相同的代码。并且,如果想修改nativeTo函数,就得在10个不同的地方进行修改。再进一步推想,如果你在开发一个大型网站,你为上面的对象都一个一个添加了属性。但是,你突然发现你创建对象的方式不是很理想,你想要进行修改,这时又该怎么办。

为了解决这些重复性的问题,软件工程师们发明了各种模式(对于重复问题和常见任务的解决方案),使用开发程序更有效率和合理化。

下面是两种创建对象的常用模式:

  1. 构造方法模式

     function Fruit (theColor, theSweetness, theFruitName, theNativeToLand) {
         this.color = theColor;
         this.sweetness = theSweetness;
         this.fruitName = theFruitName;
         this.nativeToLand = theNativeToLand;
    
         this.showName = function () {
             console.log("This is a " + this.fruitName);
         }
    
         this.nativeTo = function () {
             this.nativeToLand.forEach(function (eachCountry)  {
                 console.log("Grown in:" + eachCountry);
             });
         }
     }
    

使用这种模式,很容易就可以创建出各式各样的水果来。像这样:

    var mangoFruit = new Fruit ("Yellow", 8, "Mango", ["South America", "Central America", "West Africa"]);
    mangoFruit.showName(); // This is a Mango.​
    mangoFruit.nativeTo();
    ​//Grown in:South America​
    ​// Grown in:Central America​
    ​// Grown in:West Africa​

    ​var pineappleFruit = new Fruit ("Brown", 5, "Pineapple", ["United States"]);
    pineappleFruit.showName(); // This is a Pineapple.
如果你要改变属性或方法,你只需要在一个地方进行修改就可以了。这个模式通过一个`Fruit`函数的继承,封装了所有水果的功能和特性。

注意:

* 可继承的属性需要定义在对象的`prototype`对象属性上。比如
        
        someObject.prototype.firstName = "rich";

* 属于自身的属性要直接定义在对象的上。比如:

        // 首先,创建一个对象
        var aMango = new Fruit ();
        // 接着,直接在对象上定义mongoSpice方法
        // 因为我们直接在对象身上定义了mangoSpice属性,所以它是aMango自身的属性,不是一个可继承的属性
        aMango.mangoSpice = “some value”;

* 要访问一个对象的属性,使用`object.property`,如:
    
        console.log(aMango.mangoSpice); // "some value"

* 要调用一个对象的方法,使用`object.method()`,如:
    
        // 首先,增加一个方法
        aMango.printStuff = function() { return "Printing"; }
        
        // 现在,可以调用printStuff方法
        aMango.printStuff(); 
  1. 原型模式

     function Fruit () {
     }
    
     Fruit.prototype.color = "Yellow";
     Fruit.prototype.sweetness = 7;
     Fruit.prototype.fruitName = "Generic Fruit";
     Fruit.prototype.nativeToLand = "USA";
    
     Fruit.prototype.showName = function () {
         console.log("This is a " + this.fruitName);
     }
    
     Fruit.prototype.nativeTo = function () {
         console.log("Grown in:" + this.nativeToLand);
     }
    

    下面是在原型模式中调用Fruit()构造函数的方法:

     var mangoFruit = new Fruit ();
     mangoFruit.showName(); //​
     mangoFruit.nativeTo();
     ​// This is a Generic Fruit​
     ​// Grown in:USA
    

扩展阅读

如果需要了解这两种模式的更详细的解释,可以阅读《JavaScript高级程序设计》的第六章,其中详细讨论了这两种方法的优缺点。书中还讨论了除这两个外的其它模式。

如何访问对象中的属性

访问对象属性的两种主要方法是点记法(dot notation)和中括号记法(bracket notation)。

  1. 点记法

     // 这是我们前面例子中一直使用的访问属性的方法
     ​var book = {title: "Ways to Go", pages: 280, bookMark1:"Page 20"};
     ​
     ​// 使用点记法访问book对象的title和pages属性:​
     console.log ( book.title); // Ways to Go​
     console.log ( book.pages); // 280
    
  2. 中括号记法

     // 使用方括号启示访问book对象的属性:
     console.log ( book["title"]); //Ways to Go​
     console.log ( book["pages"]); // 280​
     ​
     ​//如果属性名储存在一个变量当中,也可以这样:​
     ​var bookTitle = "title";
     console.log ( book[bookTitle]); // Ways to Go​
     console.log (book["bookMark" + 1]); // Page 20
    

访问一个对象中不存在的属性会得到一个undefined

自身属性和继承属性

对象拥有自身属性和继承属性。自身属性是直接定义在对象上的属性,而继承属性是从ObjectPrototype继承的属性。

为了确写一个对象是否拥有某个属性(不管是自身属性还是继承属性),可以使用in操作符:

    // 创建一个有schoolName属性的对象
    var school = {schoolName:"MIT"};

    ​// 打印出true,因为对象拥有schoolName这个属性
    console.log("schoolName" in school);  // true​
    ​
    ​// 打印出false,因为我们既没有定义schoolType属性,也没有从Object的Prototype中继承schoolType属性
    console.log("schoolType" in school);  // false​
     
    ​// 打印出true, 因为从Object的Prototype中继承了toString方法
    console.log("toString" in school);  // true

hasOwnProperty

为了确定一个对象是否拥有一个特定的自身属性,可以使用hasOwnPrototype方法。这个方法十分有用,因为我们经常需要枚举一个对象的所有自身属性,而不是继承属性。

    // 创建一个拥有schoolName属性的对象
    ​var school = {schoolName:"MIT"};
    ​
    ​// 打印出true,因为schooName是school的自身属性
    console.log(school.hasOwnProperty ("schoolName"));  // true​
     
    ​// 打印出false,因为toString是从Object的Prototype中继承下来的,并且school的自身属性
    console.log(school.hasOwnProperty ("toString"));  // false 

访问和枚举对象中的属性

为了访问对象中可以枚举的属性(自身或者继承的),可以使用for-in循环或普通的循环方式。

    // 创建拥有3个属性的school对象: schoolName, schoolAccredited, and schoolLocation.​
    ​var school = {schoolName:"MIT", schoolAccredited: true, schoolLocation:"Massachusetts"};
    ​
    ​//使用for-in循环获取对象中的属性
    ​for (var eachItem in school) {
        console.log(eachItem); // Prints schoolName, schoolAccredited, schoolLocation​
    }

访问继承的属性

ObjectPrototype中继承的属性不可枚举的,所以在for-in循环中不会访问到这些属性。然而,如果是可枚举的继承属性,它们也是能够从for-in循环中访问到的。

比如:

    //使用for-in循环访问school对象中的属性
    for (var eachItem in school) {
        console.log(eachItem); // Prints schoolName, schoolAccredited, schoolLocation​
    }



    // 注:以下这段说明是原文的说明
    /* SIDE NOTE: As Wilson (an astute reader) correctly pointed out in the comments below, the educationLevel property is not actually inherited by objects that use the HigherLearning constructor; instead, the educationLevel property is created as a new property on each object that uses the HigherLearning constructor. The reason the property is not inherited is because we use of the "this" keyword to define the property.
    */​

    // Create a new HigherLearning function that the school object will inherit from.​

    function HigherLearning () {
        this.educationLevel = "University";
    }

    // Implement inheritance with the HigherLearning constructor​
    var school = new HigherLearning ();
    school.schoolName = "MIT";
    school.schoolAccredited = true;
    school.schoolLocation = "Massachusetts";

    //Use of the for/in loop to access the properties in the school object​
    for (var eachItem in school) {
        console.log(eachItem); // Prints educationLevel, schoolName, schoolAccredited, and schoolLocation​
    }

删除对象中的属性

可以使用delete操作符来删除对象中的属性。我们不能删除继承的属性,同时也不能删除Configurable特性被设置为false的对象属性。要删除继承的属性,必须从Prototype对象中删除(也就是定义这些属性的地方)。并且,我们也不能删除全局对象中的属性。

删除成功的时候,delete操作符会返回true。令人意外的是,当要删除的属性不存在,或者不能被删除(即不是自身的属性或者Configurable特性被设置为false)时, delete操作符也会返回true

以下是示例:

    var christmasList = {mike:"Book", jason:"sweater" }
    delete christmasList.mike; // deletes the mike property​

    for (var people in christmasList) {
        console.log(people);
    }
   // Prints only jason​
   // The mike property was deleted​

   delete christmasList.toString; // 返回 true, 但是因为toString是继承的属性,所以它不会被删除

    // 因为toString没有被删除,所以这里还能够正常使用
    christmasList.toString(); //"[object Object]"​

    // 如果一个属性是对象实例的自身属性,则我们可以删除它。
    // 比如我们可以从之前例子中定义的school对象中删除educationLevel属性,
    // 因为educationLevel是定义在那个实例中的:我们在HigherLearning函数中定义educationLevel时使用了"this"关键字。
    //我们并没有在HigherLearning函数的prototype对象在定义educationLevel属性。

    console.log(school.hasOwnProperty("educationLevel")); // true​
    // educationLevel是一个school对象的一个自身属性,所以 我们可以删除它​
    delete school.educationLevel; // true 

    // educationLevel属性已经从school实例中删除了
    console.log(school.educationLevel); // undefined

    // 但是educationLevel属性仍然存在于HigherLearning函数中
    var newSchool = new HigherLearning ();
    console.log(newSchool.educationLevel); // University​

    // 如果我们在HigherLearning函数prototype中定义了一个属性, 比如这个educationLevel2属性:​
    HigherLearning.prototype.educationLevel2 = "University 2";

    // 这个educationLevel2属性不属性HigherLearning实例的自身属性

    // educationLevel2属性不是school实例的自身属性​
    console.log(school.hasOwnProperty("educationLevel2")); false​
    console.log(school.educationLevel2); // University 2​

    // 尝试删除继承的educationLevel2属性​
    delete school.educationLevel2; // true (正如前面所提到的,这个表达式会返回true)

    // 继承的educationLevel2属性没有被删除
    console.log(school.educationLevel2); University 2​

序列化和反序列化对象

为了在HTTP中传递对象或者将对象转化成字符串,我们必须将对象序列化(将其转化为字符串)。我们可以使用JSON.stringify来序列化对象。要注意的是,在ECMAScript 5之前的版本,我们要使用json2库来获得JSON.stringify函数。在ECMAScript 5中,这个函数已经成为标准函数。

为了将反序列化对象(即,将字符串转化成对象),可以使用JSON.parse函数来完成。同样,在第5版之前要从json2库中获取这个函数,在第5版中已经加入这个标准函数。

示例代码:

    var christmasList = {mike:"Book", jason:"sweater", chelsea:"iPad" }
    JSON.stringify (christmasList);
    // Prints this string:​
    // "{"mike":"Book","jason":"sweater","chels":"iPad"}"

    // To print a stringified object with formatting, add "null" and "4" as parameters:​
    JSON.stringify (christmasList, null, 4);
    // "{
    //      "mike": "Book",
    //      "jason": "sweater",
    //      "chels": "iPad"​
    //  }"

    // JSON.parse Examples
    // The following is a JSON string, so we cannot access the properties with dot notation (like christmasListStr.mike)​
    var christmasListStr = '{"mike":"Book","jason":"sweater","chels":"iPad"}';

    // Let’s convert it to an object​
    var christmasListObj = JSON.parse (christmasListStr); 

    // Now that it is an object, we use dot notation​
    console.log(christmasListObj.mike); // Book

更多关于JavaScript对象的讨论和解释,以及ECMAScript第5版增加的内容,可以参考《JavaScript权威指南(第6版)》第六章。


后记

第一次翻译文章,真心觉得要把翻译做好也不是那么简单的,很多简单的句子看着很明白,结果真正想翻译出来的时候,却是死活想不出合适的表达方式。通篇文章都是根据我自己的理解,然后通过意译出来的,没有逐句进行翻译。所以,如果有哪些地方理解有偏差,或者翻译不当的地方,请尽量指出,我会尽快改正。毕竟翻译这往篇文章也是想跟大家分享,我不希望因为自己理解的错误,导致对大家产生误导。

就酱,收工。