this 和对象原型
对象
语法
对象可以通过两种形式定义:声明(文字)形式和构造形式。
文字语法:
var myObj = {
key: value,
//...
};
构造语法:
var myObj = new Object();
myObj.key = value;
构造形式和文字形式的区别就是,文字声明中可以添加多个健/值对,构造语法只能逐个添加。
类型
对象是JavaScript的基础。在JavaScript中一个有六种主要类型(术语是“语言类型”):string、number、boolean、null、undefined、object
注意,简单基本类型本身并不是对象。null有时会被当作一种对象类型,但这是语言本身的一个bug,即对null执行typeof null时会返回字符串“object”。null本身是基本类型。
实际上,JS中有许多特殊的对象子类型,我们称之为复杂基本类型。
函数、数组都是对象的一种类型。
内置对象
JS中还有一些对象子类型,通常被称为内置对象。有些内置对象的名字看起来和基础类型一样,不过它们的关系更加复杂。String、Number、Boolean、Object、Function、Array、Date、RegExp、Error。
这些内置对象从表现形式上和其他语言中的类型(type)或者类(class)很像,比如Java中的String类。
但在JS中,它们实际上只是一些内置函数。这些内置函数可以当作构造函数来使用,从而构造一个对应子类型的新对象。
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
// 检查sub-type对象
Object.prototype.toString.call(strObject); // [object String]
原始值"I am a String"并不是一个对象,它只是一个字面量,并且是一个不可变的值。语言会自动把字符串字面量转换成一个String对象以便对其执行一些操作,比如获取长度、访问其中某个字符等。
var strPrimitive = "I am a String";
console.log(strPrimitive.length); // 13
console.log(strPrimitive.charAt(3)); // "m"
同样的事也会发生在数值字面量和布尔字面量上。
null和undefined没有对应的构造形式,它们只有文字形式。相反,Date只有构造,没有文字形式。
对于Object、Array、Function和RegExp来说,无论使用文字还是构造形式,它们都是对象,不是字面量。
Error对象很少在代码中显示创建,一般是在抛出异常时被自动创建。也可以使用new Error(..)来创建。
内容
对象的内容是由一些存储在特地命名位置的(任意类型的)值组成的,我们称之为属性。
在引擎内部,属性的存储方式是多种多样的,一般并不会存在对象容器内部。存储在对象容器内部的是这些属性的名称,它们就像指针(从技术角度来说就是引用)一样,指向这些值真正的存储位置。
var myObject = {
a: 2,
};
myObject.a; // 2
myObject["a"]; // 2
“.a”语法通常被称为“属性访问”,“["a"]”语法通常被称为“键访问”。它们访问的都是同一个位置。
两种语法的主要区别为“.”操作符要求属性名满足标识符的命名规范,“[".."]”语法可以接受任意UTF-8/Unicode字符串作为属性名。
在对象中,属性名永远都是字符串。如果使用的是string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串。
var myObject = {};
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
可计算属性名
ES6 增加了可计算属性名:
var prefix = "foo";
var myObject = {
[prefix + "bar"]: "hello",
[prefix + "baz"]: "world",
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
数组
数组也支持[]访问形式。数组期望额是数值下标,也就是值存储的位置(通常被称为索引)是非负整数:
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"
复制对象
在JS中复制一个对象是复杂的,需要判断深复制还是浅复制。对于浅拷贝,会和旧对象使用相同的引用;对于深拷贝,存在循环引用的情况就会导致死循环。
许多JS框架都提出了自己的解决办法,但是JS采用的标准又不统一。
对于JSON安全(也就是可以被序列化为一个JSON字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象来说:
var newObj = JSON.parse(JSON.stringify(someObj));
ES6定义了Object.assign(..)方法实现浅复制:
var newObj = Object.assign({}, myObject);
属性描述符
ES5之前按,JS语言本身没有提供可以直接检测属性特性的方法,比如判断属性是否是只读。
ES5开始,所有的属性都具备了属性描述符。
var myObject = {
a: 2,
};
Object.getOwnPropertyDescriptor(myObject, "a");
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
如我们所见,该普通的对象属性对应的属性描述符(也被称为“数描述符”,因为它只能保存一个数据),不仅只是2。还有另外三个特性:writable(可写)、enumerable(可枚举)和configurable(可配置)。
我们可以使用Object.defineProperty(..)来添加一个新的属性或修改已有属性(如果它是configurable)对特性进行设置。
- writable writable决定是否可以修改属性的值
var myObject = {};
Object.defineProperty(myObject, "a", {
value: 2,
writable: fasle, // 不可写
enumerable: true,
configurable: true
});
myObject.a = 3;
myObject.a; // 2
- configurable 只要属性是可配置的,就可以使用defineProperty(..)方法来修改属性描述符
var myObject = {
a: 2
};
myObject.a = 3;
myObject.a; // 3
Object.defineProperty(myObject, "a", {
value: 4,
writable: true,
enumerable: true,
configurable: fasle // 不可配置
});
myObject.a; // 4
myObject.a = 5;
myObject.a; // 5
Object.defineProperty(myObject, "a", {
value: 6,
writable: true,
enumerable: true,wrai
configurable: true
}); // TypeError
可见将configurable修改为false是单向操作,无法撤销。
要注意一个例外,即便属性是configurable: false,还是能够将 writable 的状态由 true 改为 false,但是无法由 fasle 改为 true。
且configurable: false 还会禁止删除这个属性:
var myObject = {
a: 2
};
myObject.a; // 2
delete myObject.a;
myObject.a; // undefined
Object.defineProperty(myObject, "a", {
value: 2,
writable: true,
enumerable: true,
configurable: fasle // 不可配置
});
myObject.a; // 2
delete myObject.a;
myObject.a; // 2
- enumerable
该描述符控制属性是否会出现在对象的属性枚举中,比如for..in循环,如果设置enumerable为false,这个属性就不会出现在枚举中。
不变性
有时我们希望属性或者对象是不可改变的,H5中可以通过很多方法实现,但所有的方法都是浅不变性,它们只会影响目标对象和它的直接属性。
- 对象常量 结合 writable: false 和 configurable: fasle就可以创建一个真正的常量属性(不可修改、重新定义或者删除):
Object.defineProperty(myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: fasle
});
- 禁止拓展 可以使用Object.preventExtensions(..):
var myObject = {
a: 2,
};
Object.preventExtensions(myObject);
myObject.b = 3;
myObject.b; // undefined
-
密封 Object.seal(..) 会创建一个“密封”的对象,这个方法会一个在现有的对象上调用Object.preventExtensions(..),并把所有的属性标记为configurable: false。
-
冻结 Object.freeze(..) 会创建一个“冻结”的对象,这个方法会一个在现有的对象上调用Object.seal(..),并把所有“数据访问”属性标记为writable: false,这样旧无法修改值。
该方法是我们可以应用在对象上的最高的不可变性,它会禁止对象本身及其任意直接属性的修改(不过该对象引用的其他对象是不受影响的)。
[[Get]]
var myObjedt = {
a: 2,
};
myObjedt.a; // 2
myObjedt.a通过[[Get]]操作实现对a属性的访问。对象默认的内置[[Get]]操作会首先查找是否有同名的属性,有就返回该对象的值。没有就会遍历可能存在的[[Prototype]]链,还是没找到的话就会返回undefined。
[[Put]]
[[Put]]被触发时,会取决于许多因素,包括对象中是否存在这个属性。
如果已经存在这个属性,[[Put]]算法大致会检查下面这些内容。
- 属性是否是访问描述符?如何是并且存在setter就调用setter。
- 属性的数据描述符中writable是否是false?如果是,在非严格模式静默失败,在严格模式抛出TypeError异常。
- 如果都不是将值设为属性的值。
如果对象中不存在这个属性,[[Put]]操作会更加复制。后续详细介绍。
Getter和Setter
对象默认的[[Put]]和[[Get]]操作分别可以控制属性值的设置和获取。
在ES5中可以使用getter和setter部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。getter是一个隐藏函数,会在获取属性值时调用。setter也是一个隐藏函数,会在设置属性值时调用。
当你给一个属性定义getter、setter 或者两者都有时,这个属性会被定义为“访问描述符”(和“数据描述符”相对)。对于访问描述符来说,JavaScript 会忽略它们的value和 writable特性,取而代之的是关心set和get(还有configurable和enumerable)特性
var myObject = {
// 给a定义一个getter
get a() {
return 2;
},
};
Object.defineProperty(
myObject, // 目标对象
"b", // 属性名
{
get: function () {
return this.a * 2;
},
enumerable: true,
},
);
myObject.a; // 2
myObject.b; // 4
通常getter和setter是成对出现的(只定义一个的话通常会产生意料之外的行为):
var myObject = {
// 给a定义一个getter
get a() {
return this._a_;
}
// 给a定义一个setter
set a(val) {
this._a_ = val * 2;
}
}
myObject.a = 2;
myObject.a; // 4
存在性
当myObject.a的属性访问返回值为undefined时,这个值可能时属性中存储的undefined,也可能是属性不存在而返回undefined。我们可以在不访问属性值的情况下判断对象中是否存在这个属性:
var myObject = {
a: 2,
};
"a" in myObject; // true
"b" in myObject; // false
myObject.hasOwnProhaperty("a"); // true
myObject.hasOwnProperty("b"); // false
in 操作符会检查属性是否在对象及其[[Prototype]]原型链中。
hasOwnproperty(..)只会检查属性是否在myObject对象中,不会检查[[Prototype]]链。
枚举
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
可以看到myObject.b确实存在并且有访问值,但不会出现在for..in循环中。
也可以通过其他方式来区分属性是否可枚举:
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"]
遍历
for..in 循环可以用来遍历对象的可枚举属性列表(包括[[Prototype]]链)
ES6中增加了 for..of 循环语法来遍历数组(如果对象本身定义了迭代器的话也可以遍历对象):
var myArray = [1, 2, 3];
for (var v of myArray) {
console.log(v);
}
// 1
// 2
// 3
for..of 循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有返回值。
数组内有内置的@@iterator,因此for..of可以直接应用在数组上。我们使用内置的@@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}
和数组不同,普通的对象没有内置的@@iterator,所有无法自动完成for..of遍历。之所以要这样做,有许多复杂的原因,不过简单来说这样是为了避免影响未来的对象类型。
我们可以给想遍历的对象定义@@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