《你不知道的JavaScript-上卷》第二部分-this和对象原型-笔记-3-对象

41 阅读7分钟

3.1 语法

定义对象的方式

  1. 声明形式
  2. 构造形式
//对象的文字语法大概是这样:

var myObj = {
    key: value
    // ...
};

//构造形式大概是这样:
var myObj = new Object();
myObj.key = value;

3.2 类型

在 JavaScript 中一共有六种主要类型

  • string
  • number
  • boolean
  • null
  • undefined
  • object

**简单基本类型 **

string、boolean、number、null 和 undefined


复杂基本类型

多特殊的对象子类型,比如函数、数组、内置对象


内置对象

这些内置对象从表现形式来说很像其他语言中的类型(type)或者类(class),在 JavaScript 中,它们实际上只是一些内置函数。

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error
    var strPrimitive = "I am a string";
    console.log(typeof strPrimitive) // "string"
    console.log(strPrimitive instanceof String); // false

    var strObject = new String("I am a string");
    console.log(typeof strObject); // "object"
    console.log(strObject instanceof String); // true
    // 检查 sub-type 对象
    console.log(Object.prototype.toString.call(strObject)); // [object String]
  • 原始值 "I am a string" 并不是一个对象,它只是一个字面量,并且是一个不可变的值。
  • 语言会自动把字符串字面量转换成一个 String 对象,可以访问属性和方法,比如获取长度、访问其中某个字符等

数字字面量也会自动转成Number对象,比如调用3.1415.toFixed(2)


3.3 内容

对象的内容是由一些存储在特定命名位置的(任意类型的)值组成的,我们称之为属性。

var myObject = {
    a: 2
};
myObject.a; // 2
myObject["a"]; // 2
  • 要访问 myObject 中 a 位置上的值,我们需要使用 . 操作符或者 [] 操作符
  • . 操作符 称为属性访问
  • [] 操作符称为键访问
  • 区别在于属性访问要求属性名必须满足标识符的命名规范,而键访问可以访问任意字符串名字的属性
    var myObjet = { a: 2 }
    console.log(myObjet.a);//2
    console.log(myObjet['a']);//2

    myObjet['!SUPER-PROPERTY!'] = "test"
    console.log(myObjet['!SUPER-PROPERTY!']);//test

属性名永远是字符串,即使你使用其他类型的值,也会被转换成字符串。

    var myObjet = {}
    myObjet[true] = 'foo'
    myObjet[3] = 'bar'
    myObjet[myObjet] = 'baz'
    console.log(myObjet[true], myObjet['true']);//foo
    console.log(myObjet[3], myObjet['3']);//bar
    console.log(myObjet[myObjet], myObjet['[object Object]']);//baz
    console.log(myObjet);//{ 'true': 'foo', 3: 'bar', [object Object]: 'baz' }

3.3.1 可计算属性名

ES6 增加了可计算属性名,可以在文字形式中使用[]包裹一个表达式来当作属性名

    var prefix = "foo";
    var myObjet = {
      [prefix + 'bar']: "hello",
      [prefix + 'baz']: "world"
    }
    console.log(myObjet['foobar'], myObjet['foobaz']);//hello world
    console.log(myObjet);//{foobar: 'hello', foobaz: 'world'}

可计算属性名最常用的场景可能是 ES6 的符号(Symbol)

    const mySymbol = Symbol("demokey");
    var myObject = {
      [mySymbol]: "hello world"
    }
    console.log(myObject[mySymbol]);//hello world
    console.log(myObject);//{Symbol(demokey): 'hello world'}

    let newKey = Symbol("demokey");
    console.log(myObject[newKey]);//undefined

3.3.3 数组

数组支持数值下标访问,也是支持键访问

    var myArray = ["foo", 42, "bar"];
    console.log(myArray.length); // 3
    console.log(myArray[0]); // "foo"
    console.log(myArray[2]); // "bar"

    var myArray = ["foo", 42, "bar"];
    myArray.baz = "baz";
    console.log(myArray.length); // 3
    console.log(myArray.baz); // "baz"
  • 数组添加了命名属性(无论是通过 . 语法还是 [] 语法),数组的 length 值并未发生变化。

3.3.4 复制对象

深复制的方式

var newObj = JSON.parse( JSON.stringify( someObj ) );

3.3.5 属性描述符

从 ES5 开始,所有的属性都具备了属性描述符,可以直接检测属性特性

        var myObject = {
            a: 2
        };
        console.log(Object.getOwnPropertyDescriptor(myObject, "a"));
        //{
        // value: 2,
        // writable: true,
        // enumerable: true, 
        // configurable: true
        //}

属性描述符,不仅仅只是保存一个数据值,还包含另外三个特性

  • writable(可写),是否可以修改属性的值
  • enumerable(可枚举),控制的是属性是否会出现在对象的属性枚举中,比如for-in循环,为false时,不会出现在循环中,但是可以访问
  • configurable(可配置),为true是可配置的,就可以使用 defineProperty(..) 方法来修改属性描述符,为false时,不能修改,也不能删除

创建普通属性 使用 Object.defineProperty(..),来添加一个新属性或者修改一个已有属性(如果它是 configurable)并对特性进行设置

        var myObject = {};
        Object.defineProperty(myObject, "a", {
            value: 2,
            writable: true,
            enumerable: true,
            configurable: true
        });
        console.log(myObject.a);

不可写、不可配置、不可枚举示例

        // 非严格模式
        var notWriteable = {};
        Object.defineProperty(notWriteable, "a", {
            value: 2,
            writable: false,
            enumerable: true,
            configurable: true
        });
        notWriteable.a = 3;
        console.log(notWriteable.a);//2
        
        
                "use strict";
        var notWriteable = {};
        Object.defineProperty(notWriteable, "a", {
            value: 2,
            writable: false,
            enumerable: true,
            configurable: true
        });
        notWriteable.a = 3;//TypeError: Cannot assign to read only property 'a' of object '#<Object>'

        var notConfigurable = {
            a: 2
        };
        Object.defineProperty(notConfigurable, "a", {
            value: 3,
            configurable: false,
        });
        console.log(notConfigurable.a);//3
        notConfigurable.a = 4;
        console.log(notConfigurable.a);//4
        Object.defineProperty(notConfigurable, "a", { // Cannot redefine property: a
            configurable: true,
        });

        var notEnumerable = { a: 1 };
        Object.defineProperty(notEnumerable, "b", {
            value: 2,
            enumerable: false,
        });
        console.log(notEnumerable.a, notEnumerable.b)//1,2
        for (let key in notEnumerable) {
            console.log(key);//a
        }

3.3.6 不变性

希望属性或者对象是不可改变

对象常量

  • 结合 writable:false 和 configurable:false 就可以创建一个真正的常量属性(不可修改、重定义或者删除)
        var myObject = {};
        Object.defineProperty(myObject, "FAVORITE_NUMBER", {
            value: 42,
            writable: false,
            configurable: false
        });
        myObject.FAVORITE_NUMBER = 33; //改动不成功,严格模式下会报错
        console.log(myObject.FAVORITE_NUMBER);  //42

禁止扩展

Object.preventExtensions,禁 止 一 个 对 象 添 加 新 属 性 并 且 保 留 已 有 属 性

        var myObject = {
            a: 2
        };
        Object.preventExtensions(myObject);
        myObject.b = 3;
        console.log(myObject.b); // undefined
        console.log(myObject); // {a: 2}

密封

Object.seal(..),密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。

  • 这个方法实际上会在一个现有对象上调用 Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false

冻结 Object.freeze(..) 会创建一个冻结对象

  • 实际上会在一个现有对象上调用Object.seal(..) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值。

3.3.9 Getter和Setter

对象默认的 [[Put]] [[Get]] 操作分别可以控制属性值的设置和获取

  • getter 是一个隐藏函数,会在获取属性值时调用。setter 也是一个隐藏函数,会在设置属性值时调用。
  • 给一个属性定义 getter、setter 或者两者都有时,这个属性会被定义为“访问描述符”(和“数据描述符”相对)。
         var myObject = {
            get a() {
                return 2;
            }
        }
        console.log(myObject.a);//2

        Object.defineProperty(myObject, "b", {
            get: function () {
                return this.a + 1;
            },
            enumerable: true
        });
        console.log(myObject.b);//3

        myObject.a = 100; //赋值失败,因为没有set a方法
        console.log(myObject.a);//2


        var myObject = {
            __c__: 100,

            get c() {
                return this.__c__;
            },
            set c(value) {
                this.__c__ = value;
            }
        }
        console.log(myObject.c);//100
        myObject.c = 999;//Setting c to 999
        console.log(myObject.c);//999

3.3.10 存在性

在不访问属性值的情况下判断对象中是否存在这个属性

  • in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中
  • hasOwnProperty(..) 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 链
        var myObject = {
            a: 2,
        }
        console.log('a' in myObject)//true
        console.log('b' in myObject)//false

        console.log(myObject.hasOwnProperty("a"));//true
        console.log(myObject.hasOwnProperty("b"));//false

正常创建的对象都可以使用hasOwnProperty方法,但是如果是通过 Object.create(null) 来创建的对象,原型并没有委托给Object.prototype,所以不能调用hasOwnProperty方法。

这时候,可以通过显示绑定的方法来调用

Object.prototype.hasOwnProperty.call(myObject,"a")

in 操作符的迷惑性问题 in 操作符实际上检查的是某个属性名是否存在。 所以在数组中,in检查的是索引下标字符串是否在,而不是里面的原始值

const arr = [2, 4, 6]
console.log(4 in arr);//false

// in 遍历的都是属性
for (let a in arr) {
    console.log(a, arr[a]);
    // 0 2
    // 1 4
    // 2 6
}

枚举

  • propertyIsEnumerable(..) 会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足 enumerable:true
  • Object.keys(..) 会返回一个数组,包含所有可枚举属性,只会查找对象直接包含的属性
  • Object.getOwnPropertyNames(..)会返回一个数组,包含所有属性,无论它们是否可枚举,只会查找对象直接包含的属性
  • 获取所有的属性,包括原型链上的,需要自行实现
var myObject = { a: 2 }
Object.defineProperty(myObject, 'b', {
    value: 2,
    writable: true,
    enumerable: false,
    configurable: true
})
console.log(myObject.propertyIsEnumerable('a')); //true
console.log(myObject.propertyIsEnumerable('b')); //false

console.log(Object.keys(myObject))//['a']
console.log(Object.getOwnPropertyNames(myObject))//['a', 'b']

3.4 遍历

  • for..in 循环可以用来遍历对象的可枚举属性列表(包括 [[Prototype]] 链)。
  • 数组的迭代器方法
    • forEach(..)、every(..) 和 some(..)。
  • ES6 增加遍历数组的 for..of 循环语法
    • 内部使用迭代器对象来遍历数组
        const arr = [2, 4, 6]
        for (let a in arr) {
            console.log(a, arr[a]);
            //0 2
            //1 4
            //2 6
        }
        for (let a of arr) {
            console.log(a);//2,4,6
        }
const arr = [2, 4, 6]
var iterator = arr[Symbol.iterator]();
console.log(iterator.next());//{value: 2, done: false}
console.log(iterator.next());//{value: 4, done: false}
console.log(iterator.next());//{value: 5, done: false}
console.log(iterator.next());//{value: undefined, done: true}

自定义迭代器

给对象添加一个 Symbol.iterator属性

var myObject = {
            a: 2,
            b: 3
        };

        Object.defineProperty(myObject, Symbol.iterator, {
            enumerable: false,
            writable: false,
            configurable: true,
            value: function () {
                var o = this;
                var idx = 0;
                var ks = Object.keys(o);
                return {
                    next: function () {
                        return {
                            value: o[ks[idx++]],
                            done: (idx > ks.length)
                        };
                    }
                };
            }
        });
        // 手动遍历 myObject
        var it = myObject[Symbol.iterator]();
        it.next(); // { value:2, done:false } 
        it.next(); // { value:3, done:false } 
        it.next(); // { value:undefined, done:true }
        // 用 for..of 遍历 myObject
        for (var v of myObject) {
            console.log(v);
        }
        // 2
        // 3

无限随机数示例

var randoms = {
    [Symbol.iterator]: function () {
        return {
            next: function () {
                return { value: Math.random() };
            }
        };
    }
};
var randoms_pool = [];
for (var n of randoms) {
    randoms_pool.push(n);
    // 防止无限运行!
    if (randoms_pool.length === 100) break;
}
console.log(randoms_pool.length);//100