对象-你不知道的JS

内置对象从表现形式来说很像其他语言中的类型(type)或者类(class),比如Java中的String 类。

内置“对象”

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

这些内置对象从表现形式来说,很像Java中的String类。
然而,在JS中,它们实际上只是内置函数
这些内置函数可以当作构造函数,由new操作符使用,用来创建对应的基本数据类型的新对象。

一段代码:

var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
var strObject = new String( "I am a string" );
typeof strObject; // "object"
strObject instanceof String; // true

原始值"I am a string" 并不是一个对象,它只是一个字面量,并且是一个不可变的值。
如果要在这个字面量上执行一些操作,比如获取长度、访问其中某个字符等,那需要将其转换为String 对象。
幸好,在必要时语言会自动把字符串字面量转换成一个String 对象,也就是说你并不需要显式创建一个对象。比如:

var strPrimitive = "I am a string";
console.log( strPrimitive.length ); // 13
console.log( strPrimitive.charAt( 3 ) ); // "m"

数值类型和布尔类型也是如此

2..toFixed(2) => "2.00"
3.1315926.toFixed(4) => "3.1416"
  • Date只有构造形式,没有字面量形式。
  • Object,Array,Function,RegExp无论是字面量形式还是构造形式,都是对象。
  • Error对象一般是抛出异常时被自动创建

对象的属性

当我们说对象存储着“属性”时,似乎在暗示这些属性被存储在对象内部,但是这只是它的表现形式。
在引擎内部,这些属性的值的存储方式是多种多样的,一般并不会直接存在对象容器内部。
存储在对象容器内部的是这些属性的名称,它们就像指针(从技术角度来说就是引用)一样,指向这些值真正的存储位置。

属性与函数

由于函数很容易被认为是属于某个对象,在其他语言比如Java中,属于对象(也被称为“类”)的函数通常
被称为“方法”,因此把“属性访问”说成是“方法访问”也就不奇怪了。

从技术角度,JavaScript的函数永远不会“属于”一个对象。有些函数具有this 引用,有时候这些this 确实会指向调用位置的对象引用。但是这种用法从本质上来说并没有把一个函数变成一个“方法”,因为this 是在运行时根据调用位置动态绑定的,所以函数和对象的关系最多也只能说是间接关系。

通过对象的属性访问方式而返回的函数和其他函数没有任何区别(除了可能发生的隐式绑定this,就像我们刚才提到的)。

数组的属性

如果你试图向数组添加一个属性,但是属性名“看起来”像一个数字,那它会变成
一个数值下标(因此会修改数组的元素而不是添加一个属性):

let a = [1,3];
a["1"] = 666;
a => [1,666]

属性描述符

ES5开始,所有的属性具备属性描述符。

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

在创建普通属性时属性描述符会使用默认值,我们也可以使用Object.defineProperty(..)来添加一个新属性或者修改一个已有属性(如果它是configurable)并对特性进行设置

writable

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

如图,我们对于属性值的修改失败了,并且是静默的。(严格模式才会报错)

简单来说,你可以把writable:false 看作是属性不可改变,相当于你定义了一个空操作setter

configurable

属性是可配置的,才可以使用defineProperty(..) 方法来修改属性描述符。
细节,一旦可配置设为false,由于你不能再使用defineProperty(..) ,所以这是个单向操作。delete无法删除这个属性。

let obj = {
   a: 2
}

Object.defineProperty(obj, 'a', {
   configurable: false
})

console.log(obj.a);
delete obj.a  // delete 无法删除
console.log(obj.a);

// 报错:TypeError: Cannot redefine property: a
Object.defineProperty(obj, 'a', {
   configurable: true
})

enumerable

这个描述符控制的是属性是否会出现在对象的属性枚举时。for in 就是属性枚举。

维持对象现状

禁止扩展

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

let obj = {
    a: 9
}
Object.preventExtensions(obj);
obj.b = 23;
obj.b // undefined

非严格模式,静默失败,严格模式,TypeError

更进一步-密封

Object.seal(obj)

密封后,无法删除属性,无法重新设置属性描述符

let obj2 = {
    a: 123,
}

Object.seal(obj2)

obj2.b = 456;
console.log('obj2.b: ', obj2.b);
delete obj2.a;  // 密封后,不能删除已有属性
console.log('obj2.a: ', obj2.a);
Object.defineProperty(obj2, "a", {
    writable: false
})
// 密封后,不能重新配置
console.log(Object.getOwnPropertyDescriptor(obj2, "a"));

更更进一步

Object.freeze(obj)

在密封的基础上,对所有对象的属性标记为writable:false
这个方法是应用在对象上的级别最高的不可变性,它会进制对于对象本身及其任意直接属性的修改(当然,这个对象引用的其他对象不受影响)。
所以,这个冻结是“浅冻结”。