天天看点

对象----《你不知道的JS》

最近在拜读《你不知道的js》,而此篇是对于《你不知道的js》中对象部分的笔记整理,希望能有效的梳理,并且深入理解对象

一、语法

对象两种定义形式:声明(文字)形式、构造形式

声明(文字)形式

var myObj = {
   key: value,
   ...
}
           

构造形式

var myObj = new Object();
myObj.key = value;
           

构造形式与文字形式生成的对象一样

区别:文字声明中可以添加多个键/值对,构造形式中必须逐个添加属性

二、类型

在JavaScript中一共有6中主要类型:

  • string
  • number
  • boolean
  • null
  • undefined
  • object
注意:简单基本类型(string、boolean、number、null、undefined)本身不是对象。null有时会被当做一种对象类型,typeof null返回‘object’。实际上,null是基本类型

内置对象

内置对象:对象子类型

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

三、内容

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

var myObject = {
    a: 2
};
myObject.a;// 2
myObject['a']; // 2
           

若要访问myObject中a位置上的值,需使用.操作符或[]操作符。.a语法称为属性访问(最常见的方式),[‘a’]语法称为键访问

区别:.操作符要求属性名满足标识符的命名规范,而["…"]语法可以接受任意UTF-8/Unicode字符串作为属性名,如名称为“Super-Fun”的属性,就必须使用[“Super-Fun”]语法访问

由于[’…’]语法使用字符串来访问属性,所以可以在程序中构造这个字符串,如:

var myObject = {
    a: 2
};
var idx;
if(wantA) {
    idx = "a";
}
console.log(myObject[idx]); // 2
           

在对象中,属性名永远都是字符串。若使用string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串。

var myObject = {};
myObject[true] = 'foo';
myObject[3] = "bar";
myObject[myObject]="baz";
myObject["true"]; // 'foo'
myObject['3']; //"bar"
myObject["object object"]; // "baz"
           

1、可计算属性名

ES6增加了可计算属性名:

var prefix = "foo";
var myObject = {
    [prefix + "bar"]: "hello",
    [prefix + "baz"]: "world"
};
myObject["foobar"];// hello
myObject["foobaz"];// world
           

2、属性与方法

在其他语言中,属于对象(也称为“类”)的函数通常被称为“方法”,有时属性方位也称为“方法访问”。

无论返回值是什么类型,每次访问对象的属性就是属性访问。若属性访问返回一个函数,那它也并不是一个“方法”。属性访问返回的函数和其他函数没有任何区别

function foo() {
    console.log("foo");
}
var someFoo = foo; // 对foo变量的引用
var myObject = {
    someFoo: foo,
}
foo; // function foo() {}
someFoo; //  function foo() {}
myObject.someFoo; // function foo() {}
           

someFoo与myObject.someFoo只是对于同一个函数的不同引用,并不能说明这个函数是特别的或“属于”某个对象

3、数组

数组也支持[]访问形式,通过数值下标,即存储位置(索引)访问,是非负整数,如:0,42:

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

数组也是对象,也可以给数组添加属性:

var myArray = ["foo", 42, "bar"];
myArray.baz = "baz";
myArray.length; // 3
myArray.baz; // "baz"
           

虽然添加了命名属性,数组的length值并未发生变化。

若你试图向数组添加一个属性,但属性名“看起来”像数字,那它会变成一个数值下标:

var myArray = ["foo", 42, "bar"];
myArray["3"] = "baz";
myArray.length; // 4
myArray[3] = "baz";
           

4、复制对象

function anotherFunction() {/*..*/}
var anotherObject = {
    c: true
}
var anotherArray = [];
var myObject = {
    a: 2,
    b: anotherObject, // 引用,不是复本
    c: anotherArray, // 另一个引用
    d: anotherFunction
}
anotherArray.push(anotherObject, myObject);
           
如何准确地表示myObject的复制呢?

首先判断它是浅复制还是深复制。

1)浅复制

复制出的新对象中a的值会复制就对象中a的值,即2,但新对象中b、c、d三个属性其实只是三个引用,和就对象中b、c、d引用的对象一样

2)深复制

除了复制myObject以外还会复制anotherObject和anotherArray

问题:anotherArray引用了anotherObject和myObject,所以又需要复制,myObject,这样会由于循环引用导致死循环

如何解决?

1)对于json安全的对象来说:

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

这种方法需要保证对象是json安全的,所以只适用于部分情况。

2)ES6定义了Object.assign(…)方法来实现浅复制。Object.assign(…)方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。它会遍历一个或多个源对象的所有可枚举的自有键并把它们复制(使用 = 操作符赋值)到目标对象:

var newObj = Object.assign({}, myObject);
newObj.a;// 2
newObj.b === anotherObject; // true
newObj.c === anotherArray; // true
newObj.d === anotherFunction; // true
           
注:由于Object.assign(…)使用 = 操作符来赋值,所以源对象属性的一些特性不会被复制到目标对象。

5、属性描述符

在ES5之前,JavaScript语言本身并没有提供可直接检测属性特性的方法,如判断属性是否是只读。从ES5开始,所有的属性都具备了属性描述符。

var myObject = {
    a: 2
}
Object.getOwnPropertyDescriptor(myObject, "a");
// {
//   value: 2,
//   writable: true, // 可写
//   enumerable: true, // 可枚举
//   configurable: true // 可配置
// }
           

在创建普通属性时属性描述符会使用默认值,可使用Object.defineProperty(…)来添加一个新属性或修改一个已有属性,并对特性进行设置。

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

1)writable

决定是否可以修改属性的值

var myObject = {};
Object.defineProperty(myObject, "a", {
    value: 2,
    writable: false, // 不可写
    configurable: true,
    enumerable: true
});
myObject.a = 3;
myObject.a; // 2
           

在严格模式下会报错

"use strict"
var myObject = {};
Object.defineProperty(myObject, "a", {
    value: 2,
    writable: false, // 不可写
    configurable: true,
    enumerable: true
});
myObject.a = 3; // TypeError
           

2)Configurable

只要属性可配置,就可以用defineProperty(…)方法修改属性描述符:

var myObject = {
    a: 2
};
myObject.a = 3;
myObject.a; // 3
Object.defineProperty(myObject, "a", {
    value: 4,
    writable: true,
    configurable: false, // 不可配置
    enumerable: true
});
myObject.a; // 4
myObject.a = 5;
myObject.a; // 5
Object.defineProperty(myObject, "a", {
    value: 6,
    writable: true,
    configurable: true,
    enumerable: true
}); // TypeError
           

无论是否处于严格模式,尝试修改一个不可配置的属性描述符都会出错。

注:把Configurable 修改成false是单向操作,无法撤销;

即便属性是configurable:false,我们还是可以把writable的状态由true改为false,但无法由false改为true。

除了无法修改,configurable: false还会禁止删除这个属性:

var myObject = {
    a: 2
};
myObject.a = 2;
delete myObject.a;
myObject.a; // undefined
Object.defineProperty(myObject, "a", {
    value: 2,
    writable: true,
    configurable: false, // 不可配置
    enumerable: true
});
myObject.a; // 2
delete myObject.a;
myObject.a; // 2
           

在本例中,delete只用来直接删除对象的(可删除)属性。若对象的某个属性是某个对象/函数的最后一个引用者,对这个属性执行delete操作后,这个对象/函数就可以被垃圾回收

3)enumerable

控制属性是否出现在对象的属性枚举类中,如for…in循环,若把enumerable设置为false,属性就不会出现在枚举中,虽然仍可以正常访问它

6、不变性

ES5中所有方法创建的都是浅不变性,即它们只会影响目标对象和它的直接属性。如果目标对象引用了其他对象(数组、对象、函数等),其他对象的内容不受影响,仍是可变的:

myImmutableObject.foo; // [1,2,3]
myImmutableObject.foo.push(4);
myImmutableObject.foo; // [1,2,3,4]
           

1)对象常量

结合writable:false和configurable:false就可以创建一个真正的常量属性(不可修改、重新定义或者删除)

var myObject = {};
Object.defineProperty(myObject, "FAVORITE_NUMBER", {
    value: 42,
    writable: false,
    configurable: false, // 不可配置
});
           

2)禁止扩展

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

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

非严格模式下,创建属性b会静默失败,在严格模式下,将抛出TypeError

3)密封

Object.seal(…)会创建一个“密封”对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(…)并把所有现有属性标记为configurable:false。所以密封后不仅不能添加新属性,也不能重新配置或删除任何现有属性(虽然可以修改属性的值)

4)冻结

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

这个方法可应用在对象上的级别最高的不可变性,它会禁止对象本身及其任意直接属性的修改,这个对象的引用的其他对象是不受影响的
深度冻结方法:首先在这个对象上调用Object.freeze(…),然后遍历它引用的所有对象并在这些对象上调用Object.freeze(…),但可能会在无意中冻结其他(共享)对象

7、[[Get]]

var myObject = {
    a: 2
}
myObject.a; // 2
           

myObject.a在myObject上实际是实现了[[Get]]操作。对象默认的内置[[Get]]操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值,若没找到,按照[[Get]]实验法的定义会执行另外一种非常重要的行为,即遍历可能存在的[[Prototype]]链,也就是原型链。

如果无论如何都没有找到名称相同的属性,那[[Get]]操作会返回undefined

var myObject = {
    a: 2
}
myObject.b; // undefined
           
注:这种方法和访问变量时是不一样的,若你引用了一个当前词法作用域中不存在的变量,并不会像对象属性一样返回undefined,而是会抛出ReferenceError异常:
var myObject = {
    a: undefined
}
myObject.a; // undefined
myObject.b; // undefined 由于根据返回值无法判断出到底变量的值为undefined还是变量不存在,所以[[Get]]操作返回了undefined
           

8、[[Put]]

[[Put]]被触发时,实际行为取决于许多因素,包括对象中是否已经存在这个属性(最重要的因素)

如果已经存在这个属性,[[Put]]算法大致会检查下面这些内容:

1)属性是否是访问描述符?如果是并且存在setter就调用setter

2)属性的数据描述符中writable是否是false?如果是,在非严格模式下静默注册失败,在严格模式下抛出TypeError异常

9、Getter和Setter

在ES5中可使用getter和setter部分改写默认操作,但只能应用在单个属性上,无法应用在整个对象上。getter是一个隐藏函数,会在获取属性值时调用。setter也是一个隐藏函数,会在设置属性时调用。

当你给一个属性定义getter和setter或者两者都有时,这个属性会被定义为“访问描述符”,对于访问描述符来说,js会忽略它们的value和writable特性,取而代之的关心set和get(还有configurable和enumerable)

var myObject = {
    get a() {
        return 2;
    }
}
Object.defineProperty(
    myObject, // 目标对象
    "b", // 属性名
    {
        get: function() {
            return this.a * 2
        },
        enumerable: true
    }
);
myOject.a; // 2
myObject.b; // 4
           

两种方式都会在对象中创建一个不包含值的属性,对于这个属性的访问会自动调用一个隐藏函数,它的返回值会被当做属性访问的返回值:

var myObject = {
    get a() {
        return 2;
    }
}
myOject.a = 3;
myObject.a; // 2
           

由于我们只定义了a的getter,所以对a的值进行设置时set操作会忽略赋值操作,不会抛出错误。

通常来说getter和setter是成对出现的

var myObject = {
    get a() {
        return this._a_;
    }
    set a(val) {
        this._a_ = val * 2;
    }
}
myOject.a = 2;
myObject.a; // 4
           

10、存在性

属性访问返回值可能是undefined,如何区分这是属性中存储的undefined,还是属性不存在而返回的undefined ?
var myObject = {
    a: 2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("b"); // false
           

in操作符会检查属性是否在对象及其[[Prototype]]原型链中,hasOwnProperty(…)只会检查属性是否在myObject对象中,不会检查[[Prototype]]链

注:in是检查某个属性名是否存在,如:4 in [2,4,6] = false, 因为这个数组中包含的属性名为0 ,1, 2

所有的普通对象都可以通过对于 Object.prototype 的委托来访问hasOwnProperty(…),但有的对象可能没有连接到 Object.prototype (通过Object.create(null)创建),则myObject.hasOwnProperty就会失败

此时可采用 Object.prototype.hasOwnProperty.call(myObject, “a”) 进行判断,它借用基础的hasOwnProperty(…)方法并把它显示绑定在myObject上

1)枚举

“可枚举”相当于“可以出现在对象属性的遍历中”

var myObject = {};
Object.defineProperty(
    myObject, 
    "a",
    // 让a像普通对象一样可枚举
    { enumerable: true, value: 2 }
);
Object.defineProperty(
    myObject, 
    "B",
    // 让b不可枚举
    { enumerable: false, value: 3 }
);
myObject.b; // 3
("b" in myObject); // true
myObject.hasOwnProperty("b"); // true

for (var k in myObject) {
    console.log(k, myObject[k]);
}
// "a" 2
           
for…in枚举不仅会包含所有索引,还会包含所有可枚举属性,所以最好只在对象上引用for … in 循环,若遍历数组就使用for循环。但它无法直接获取属性值,需手动获取

也可用propertyIsEnumerable(…)来区分属性是否可枚举

var myObject = {};
Object.defineProperty(
    myObject, 
    "a",
    // 让a像普通对象一样可枚举
    { enumerable: true, value: 2 }
);
Object.defineProperty(
    myObject, 
    "B",
    // 让b不可枚举
    { enumerable: false, value: 3 }
);
myObject.propertyIsEnumerable("a"); // true
myObject.propertyIsEnumerable("b"); // false

Object.keys(myObject); // ["a"]
Object.getOwnPropertyNames(myObject); // ["a", "b"]
           

propertyIsEnumerable(…)会检查给定的属性名是否直接存在于对象中(而非原型链上),并且满足enumerable: true

Object.keys(…)会返回数组,包含所有可枚举属性。Object.getOwnPropertyNames(…)只会查找对象直接包含的属性

四、遍历

for…in用来遍历对象的可枚举属性列表,如何遍历属性值呢?

对于数值索引的数组来说,可用for循环。另外ES5也增加了一些数组辅助迭代器:forEach(…)、every(…)、some(…),他们都可以接受一个回调函数并把它应用在数组的每个元素上,区别就是它们对于回调函数返回值的处理方式不同

forEach(…):遍历数组所有值并忽略回调函数的返回值

every(…):会一直运行到回调函数返回false(或“假”值)

some(…):会一直运行直到回调函数返回true(或“真”值)

遍历数组下标时采用的数字顺序,但遍历对象属性时顺序不确定,在不同的js引擎中可能不一样

如何直接遍历值而不是数组下标?

使用for…of (ES6增加的语法)

var myArray = [1, 2, 3];
for(var v of myArray) {
    console.log(v);
}
// 1
// 2
// 3
           

for…of首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有返回值

数组有内置的@@iterator,也可直接应用在数组上。

var myArray = [1, 2, 3];
var it = myArray[Symbol.iterator]();
it.next(); // {value: 1, done: false}
it.next(); // {value: 2, done: false}
it.next(); // {value: 3, done: false}
it.next(); // {done: true}
           

value是遍历值,done是布尔值,表示是否还有可遍历的值

普通对象中没有内置的@@iterator,所以无法自动完成for…of,但可以结合for…of循环与自定义迭代器来操作对象

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)
              }
          }
      }
  }
});
// 手动遍历
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
for (var v of myObject) {
    conosle.log(v);
}
// 2
// 3