本文是我学习《你所不知道的javaScript上卷》的读书笔记的整理。
更多详细内容,请微信搜索“前端爱好者
“, 戳我 查看 。
1. 对象
对象(object)是 JavaScript 语言的核心概念,也是最重要的数据类型。
什么是对象?简单说,对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合。
var obj = {
foo: 'Hello',
bar: 'World'
};
上面代码中,大括号就定义了一个对象,它被赋值给变量obj,所以变量obj就指向一个对象。
该对象内部包含两个键值对(又称为两个“成员”):
第一个键值对是foo: 'Hello',其中foo是“键名”(成员的名称),字符串Hello是“键值”(成员的值)。
键名与键值之间用冒号分隔。
第二个键值对是bar: 'World',bar是键名,World是键值。
两个键值对之间用逗号分隔。
1.1 创建新对象
JavaScript 拥有一系列预定义的对象。
从 JavaScript 1.2 之后,你可以通过对象初始化器(Object Initializer)创建对象。
你也可以创建一个构造函数并使用该函数和 new 操作符
初始化对象。
1.1.1 使用对象初始化器/对象的文字语法
var myObj = {
key: value
// ...
};
1.1.2 使用构造函数
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
var rand = new Person("Rand McKinnon", 33, "M");
1.1.3 使用Object.create方法
对象也可以用 Object.create()
方法创建。
该方法非常有用,因为它允许你为创建的对象选择其原型对象,而不用定义一个构造函数。
// Animal properties and method encapsulation
var Animal = {
type: "Invertebrates", // Default value of properties
displayType : function() { // Method which will display type of Animal
console.log(this.type);
}
}
// Create new animal type called animal1
var animal1 = Object.create(Animal);
animal1.displayType(); // Output:Invertebrates
// Create new animal type called Fishes
var fish = Object.create(Animal);
fish.type = "Fishes";
fish.displayType(); // Output:Fishes
Object.create() 方法会使用指定的原型对象及其属性去创建一个新的对象。
1Object.create(proto[, propertiesObject])
Object.create()
参数
- proto 新创建对象的原型对象。
- propertiesObject 可选。
如果没有指定为 undefined,则是要添加到新创建对象的可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)对象的属性描述符以及相应的属性名称。
这些属性对应Object.defineProperties()的第二个参数。
返回值 -- 在指定原型对象上添加新属性后的对象。
例外 -- 如果proto
参数不是 null
或一个对象
,则抛出一个 TypeError
异常。
1.2 javaScript 内置对象
对象是 JavaScript 的基础。
JavaScript 中有一些对象子类型,通常被称为内置对象,主要有:
- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
有些内置对象的名字看起来和简单基础类型一样,不过实际上它们的关系更复杂。
基本数据类型
在 JavaScript 中一共有六种主要类型(术语是“语言类型”)
- string
- number
- boolean
- null
- undefined
- object (注: Array是特殊的Object)
- Symbol(es6新定义的)
简单基本类型(string、boolean、number、null 和 undefined)本身并不是对象。
null 有时会被当作一种对象类型,但是这其实只是语言本身的一个 bug,即对 null 执行
typeof null 时会返回字符串 "object"。实际上,null 本身是基本类型。
“JavaScript 中万物皆是对象”,这显然是错误的。
注意区分js中的基本数据类型和内置对象
这些内置对象从表现形式来说很像其他语言中的类型(type)或者类(class),比如 Java 中的 String 类。
但是在 JavaScript 中,它们实际上只是一些内置函数。
这些内置函数可以当作构造函数
(由 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
// 检查 sub-type 对象
Object.prototype.toString.call( strObject ); // [object String]
string 和 number 、 boolean JS引擎会在需要的时候把他们转成相应的对象,调用其中放大,操作完成后对象又转回成简单的字面量。
null 和 undefined 没有对应的构造形式,它们只有文字形式。
Date 只有构造,没有文字形式。
Object、Array、Function 和 RegExp(正则表达式)无论使用文字形式还是构造形式,它们都是对象,不是字面量。
Error 对象很少在代码中显式创建,一般是在抛出异常时被自动创建。也可以使用 new Error(..) 这种构造形式来创建。
1.3 对象取值
如果要访问 对象中的值,我们需要使用 . 操作符或者 [] 操作符。
- . 语法通常被称为“属性访问”,
- [] 语法通常被称为“键访问”。
var myObject = {
a: 2
};
myObject.a; // 2
myObject["a"]; // 2
在对象中,属性名永远都是字符串。
如果你使用 string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串。
即使是数字也不例外,虽然在数组下标中使用的的确是数字,但是在对象属性名中数字会被转换成字符串,所以当心不要搞混对象和数组中数字的用法。
1.4 可计算属性名
如果需要通过表达式来计算属性名,那么myObject[..] 这种属性访问语法就可以派上用场了,如可以使用 myObject[prefix + name]。
ES6 增加了可计算属性名,可以在文字形式中使用 [] 包裹一个表达式来当作属性名:
var prefix = "foo";
var myObject = {
[prefix + "bar"]:"hello",
[prefix + "baz"]: "world"
};
myObject["foobar"]; // hello myObject["foobaz"]; // world
可计算属性名最常用的场景可能是 ES6 的符号(Symbol)
1.5 属性与方法
1.5.1 函数永远不会“属于”一个对象
从技术角度来说,
函数永远不会“属于”一个对象,
所以把对象内部引用的函数称为“方法”似乎有点不妥。
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 只是对于同一个函数的不同引用,并不能说明这个函数是特别的或者“属于”某个对象。
如果 foo() 定义时在内部有一个 this 引用,那这两个函数引用的唯一区别就是 myObject.someFoo 中的 this 会被隐式绑定到一个对象。
无论哪种引用形式都不能称之为“方法”。
1.5.2 在对象的文字形式中声明一个函数表达式
即使你在对象的文字形式中声明一个函数表达式,这个函数也不会“属于”这个对象
它们只是对于相同函数对象的多个引用。
var myObject = {
foo: function() {
console.log( "foo" );
}
};
var someFoo = myObject.foo;
someFoo; // function foo(){..}
myObject.foo; // function foo(){..}
1.6 数组
数组也是对象,所以虽然每个下标都是整数,你仍然可以给数组添加属性
var myArray = [ "foo", 42, "bar" ];
myArray.baz = "baz";
myArray.length; // 3
myArray.baz; // "baz"
但是数组和普通的对象都根据其对应的行为和用途进行了优化,所以最好:
- 只用对象来存储键 / 值对,
- 只用数组来存储数值下标 / 值对。
如果你试图向数组添加一个属性,但是属性名“看起来”像一个数字,那它会变成一个数值下标(因此会修改数组的内容而不是添加一个属性)
var myArray = [ "foo", 42, "bar" ];
myArray["3"] = "baz";
myArray.length; // 4
myArray[3]; // "baz"
2. 对象常用操作
2.1 复制对象
JavaScript 初学者最常见的问题之一就是如何复制一个对象。看起来应该有一个内置的copy()方法?
实际上事情比你想象的更复杂,因为我们无法选择一个默认的复制算法。
function anotherFunction() { /*..*/ }
var anotherObject = { c: true
};
var anotherArray = [];
var myObject = {
a: 2,
b: anotherObject, // 引用,不是复本!
c: anotherArray, // 另一个引用!
d: anotherFunction
};
如何准确地表示 myObject 的复制呢?
首先,我们应该判断它是浅复制还是深复制。
- 对于
浅拷贝
来说,复制出的新对象中a 的值会复制旧对象中a 的值,也就是 2,但是新对象中b、c、d 三个属性其实只是三个引用,它们和旧对象中b、c、d 引用的对象是一样的。 - 对于
深复制
来说,除了复制myObject 以外还会复制anotherObject 和anotherArray。这时问题就来了,anotherArray 引用了anotherObject 和
myObject,所以又需要复制myObject,这样就会由于循环引用导致死循环。
安全的 JSON 对象
对于 JSON 安全(也就是说可以被序列化为一个 JSON 字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象来说,有一种巧妙的复制方法:
var newObj = JSON.parse( JSON.stringify( someObj ) );
当然,这种方法需要保证对象是 JSON 安全的,所以只适用于部分情况。
浅复制 ES6 解决方案 -- Object.assign(…)
Object.assign(..)
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
const object1 = {
a: 1,
b: 2,
c: 3
};
const object2 = Object.assign({}, object1);
console.log(object2.c);
// expected output: 3
语法
Object.assign(target, ...sources)
参数
- target 目标对象。
- sources 源对象。
返回值
目标对象。
2.2 属性描述符 -- Object.defineProperties()
从 ES5 开始,所有的属性都具备了属性描述符。
在 ES5 之前,JavaScript 语言本身并没有提供可以直接检测属性特性的方法,比如判断属性是否是只读。
var myObject = {
a:2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true, (可写)
// enumerable: true, (可枚举)
// configurable: true (可配置)
// }
在创建普通属性时属性描述符会使用默认值。
我们也可以使用 Object.defineProperty(..)
来添加一个新属性或者修改一个已有属性(如果它是 configurable)并对特性进行设置。
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
});
myObject.a; // 2
我们使用 defineProperty(..) 给 myObject 添加了一个普通的属性并显式指定了一些特性。
然而,一般来说不会使用这种方式.
除非你想修改属性描述符。
2.2.1 Object.defineProperties()详解:
Object.defineProperties() 直接在一个对象上定义一个或多个新的属性或修改现有属性,并返回该对象。
语法
Object.defineProperties(obj, props)
参数
2.2.2 defineProperty VS defineProperties
Object.defineProperty() 直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
Object的
defineProperty
defineProperties
主要功能就是用来定义或修改这些内部属性,
与之相对应
getOwnPropertyDescriptor
getOwnPropertyDescriptors
就是获取这些内部属性的描述。
var obj = {};
Object.defineProperties(obj, {
'property1': {
value: true,
writable: true
},
'property2': {
value: 'Hello',
writable: false
}
// etc. etc.
});
区别:
Object.defineProperty(obj, "key", {
enumerable: false,
configurable: false,
writable: false,
value: "static"
});
2.3 对象是不可改变或者对象属性不可改变
在 ES5 中可以通过很多种方法来实现,对象是不可改变或者对象属性不可改变。
所有的方法创建的都是浅不变性
,也就是说,它们只会影响目标对象和它的直接属性。
如果目标对象引用了其他对象(数组、对象、函数,等),其他对象的内容不受影响,仍然是可变的。
2.3.1 对象常量
结合 writable:false
和 configurable:false
就可以创建一个真正的常量属性(不可修改、重定义或者删除)。
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
});
2.3.2 禁止扩展
可以使用 Object.preventExtensions(..)
var myObject = {
a:2
};
Object.preventExtensions( myObject ); myObject.b = 3;
myObject.b; // undefined
在非严格模式下,创建属性 b 会静默失败。在严格模式下,将会抛出 TypeError 错误。
2.3.3 密封
Object.seal(..) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用
Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false。
所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。
2.3.4 冻结
Object.freeze(..) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(..) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值。
这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改(不过就像我们之前说过的,这个对象引用的其他对象是不受影响的)。
2.4 [[Get]]
var myObject = {
a: 2
};
myObject.a; // 2
myObject.a 是一次属性访问,但是这条语句并不仅仅是在 myObjet 中查找名字为 a 的属性。
在语言规范中,myObject.a 在 myObject 上实际上是实现了 [[Get]] 操作(有点像函数调用:[[Get]]())。
对象默认的内置 [[Get]] 操作首先在对象中查找是否有名称相同的属性:
- 如果找到就会返回这个属性的值。
- 如果没有找到名称相同的属性,按照 [[Get]] 算法的定义会执行另外一种非常重要的行为遍历可能存在的[[Prototype]] 链,也就是原型链。
- 如果无论如何都没有找到名称相同的属性,那 [[Get]] 操作会返回值 undefined。
如果你引用了一个当前词法作用域中不存在的变量,并不会像对象属性一样返回 undefined,而是会抛出一个 ReferenceError 异常。
var myObject = {
a: undefined
};
myObject.a; // undefined
myObject.b; // undefined
从返回值的角度来说,这两个引用没有区别——它们都返回了 undefined。
然而,尽管乍看之下没什么区别,
实际上底层的 [[Get]] 操作对 myObject.b 进行了更复杂的处理。
2.5 [[Put]]
你可能会认为给对象的属性赋值会触发 [[Put]] 来设置或者创建这个属性。但是实际情况并不完全是这样
。
[[Put]] 被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性(这是最重要的因素)。
如果已经存在这个属性,[[Put]] 算法大致会检查下面这些内容。
- 属性是否是访问描述符(参见 3.3.9 节)?如果是并且存在 setter 就调用 setter。
- 属性的数据描述符中 writable 是否是 false ?如果是,在非严格模式下静默失败,在严格模式下抛出 TypeError 异常。
- 如果都不是,将该值设置为属性的值。
如果对象中不存在这个属性,[[Put]] 操作会更加复杂。
2.6 Getter和Setter
对象默认的 [[Put]] 和 [[Get]] 操作分别可以控制属性值的设置和获取。
在 ES5 中可以使用 getter 和 setter 部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。
getter 是一个隐藏函数,会在获取属性值时调用。
setter 也是一个隐藏函数,会在设置属性值时调用。
var myObject = {
// 给 a 定义一个 getter
get a() {
return 2;
}
};
Object.defineProperty(
myObject, // 目标对象
"b", // 属性名
{ // 描述符
// 给 b 设置一个 getter
get: function(){
return this.a * 2
},
// 确保 b 会出现在对象的属性列表中
enumerable: true
}
);
myObject.a; // 2
myObject.b; // 4
不管是对象文字语法中的 get a() { .. },还是 defineProperty(..) 中的显式定义,二者都会在对象中创建一个不包含值的属性,
对于这个属性的访问会自动调用一个隐藏函数,它的返回值会被当作属性访问的返回值。
var myObject = {
// 给 a 定义一个 getter
get a() {
return 2;
}
};
myObject.a = 3;
myObject.a; // 2
由于我们只定义了 a 的 getter,所以对 a 的值进行设置时 set 操作会忽略赋值操作,不会抛出错误。
而且即便有合法的 setter,由于我们自定义的 getter 只会返回 2,所以 set 操作是没有意义的。
var myObject = {
// 给 a 定义一个 getter 、
get a() {
return this._a_;
},
// 给 a 定义一个 setter
set a(val) {
this._a_ = val * 2;
}
};
myObject.a = 2;
myObject.a; // 4
为了让属性更合理,还应当定义 setter,和你期望的一样,setter 会覆盖单个属性默认的 [[Put]](也被称为赋值)操作。
本例中,实际上我们把赋值([[Put]])操作中的值 2 存储到了另一个变量 _a_
中。名称 _a_
只是一种惯例,没有任何特殊的行为——和其他普通属性一样
通常来说 getter 和 setter 是成对出现。
2.7 判断对象中是否存在这个属性/方法
如何区分:
- 值有可能是属性中存储的 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]] 链。所有的普通对象都可以通过对于 Object.prototype 的委托来访问
hasOwnProperty(..)
,但是有的对象可能没有连接到 Object.prototype( 通过
Object. create(null)
来创建)。在这种情况下,形如 myObejct.hasOwnProperty(..)
就会失败。这时可以使用一种更加强硬的方法来进行判断:
Object.prototype.hasOwnProperty. call(myObject,"a")
,它借用基础的 hasOwnProperty(..) 方法并把它显式绑定 到 myObject 上。
in操作
in 操作符可以检查容器内是否有某个值,实际上检查的是某个属性名
是否存在。
对于数组来说这个区别非常重要
4 in [2, 4, 6] 的结
果并不是你期待的 True,因为 [2, 4, 6] 这个数组中包含的属性名是 0、1、
2,没有 4。
2.8 枚举 - for..in(枚举属性值)
var myObject = { };
Object.defineProperty(
myObject, "a", {
enumerable: true, // 让 a 像普通属性一样可以枚举
value: 2
});
Object.defineProperty(
myObject, "b", {
enumerable: false, // 让 b 不可枚举
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 循环中(尽管可以通过 in 操作符来判断是否存在)。
原因是: numerable: false。
“可枚举”就相当于“可以出现在对象属性的遍历中”。
对象上遍历属性值应用 for..in 循环,
遍历数组就使用传统的 for 循环来遍历数值索引。
var myObject = { };
Object.defineProperty(
myObject, "a", {
enumerable: true, // 让 a 像普通属性一样可以枚举
value: 2
});
Object.defineProperty(
myObject, "b", {
enumerable: false, // 让 b 不可枚举
value: 3
});
myObject.propertyIsEnumerable( "a" ); // true
myObject.propertyIsEnumerable( "b" ); // false
Object.keys( myObject ); // ["a"]
Object.getOwnPropertyNames( myObject ); // ["a", "b"]
总结
- propertyIsEnumerable(..) 会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足 enumerable: true。
- Object.keys(..) 会返回一个数组,包含所有可枚举属性 enumerable: true。
- Object.getOwnPropertyNames(..) 会返回一个数组,包含所有属性,无论它们是否可枚举。
- in 和 hasOwnProperty(..)区别:是否查找 [[Prototype]] 链。
- Object.keys(..) 和 Object.getOwnPropertyNames(..) 都只会查找对象直接包含的属性。
2.9 遍历 - for..in(枚举属性值)
for..in 循环可以用来遍历对象的可枚举属性列表(包括 [[Prototype]] 链)。但是如何遍历属性的值呢?
数组 -- 可以使用标准的 for 循环来遍历值
var myArray = [1, 2, 3];
for (var i = 0; i < myArray.length; i++) {
console.log( myArray[i] );
}
// 1 2 3
这实际上并不是在遍历值,而是遍历下标来指向值,如 myArray[i]。
ES5 中增加了一些数组的辅助迭代器
- forEach(..)
- every(..)
- some(..)
每种辅助迭代器都可以接受一个回调函数并把它应用到数组的每个元素上
,
唯一的区别就是它们对于回调函数返回值的处理方式不同。
- forEach(..) 会遍历数组中的所有值并忽略回调函数的返回值。
- every(..) 会一直运行直到回调函数返回 false(或者“假”值)
- some(..) 会一直运行直到回调函数返回 true(或者
“真”值)。
every(..) 和 some(..) 中特殊的返回值和普通 for 循环中的 break 语句类似,它们会提前终止遍历。
for..in 遍历对象是无法直接获取属性值
使用 for..in 遍历对象是无法直接获取属性值的,因为它实际上遍历的是对象中的所有可枚举属性
,你需要手动获取属性值
。
for..in 遍历数组下标时采用的是数字顺序(for 循环或者其他迭代器)
但是 for..in 遍历对象属性时的顺序是不确定的,在不同的 JavaScript 引擎中可能不一样。
因此, 在不同的环境中需要保证一致性时,一定不要相信任何观察到的顺序,它们是不可靠的。
如何直接遍历值而不是数组下标(或者对象属性)呢?
ES6 增加了一种用来遍历数组的 for..of 循环语法(如果对象本身定义了迭代器的话也可以遍历对象)
var myArray = [ 1, 2, 3 ];
for (var v of myArray) {
console.log( v );
}
// 1
// 2
// 3
var myArray = [ 1, 2, 3 ];
for (var v in myArray) {
console.log( v );
}
// 0
// 1
// 2
for..of原理
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
我们使用 Object.defineProperty(..) 定义了我们自己的 @@iterator(主要是为了让它不可枚举),不过注意,我们把符号当作可计算属性名(本章之前有介绍)。
此外,也可以直接在定义对象时进行声明,比如 var myObject = { a:2, b:3, [Symbol.iterator]: function() { /* .. */ } }
。
总结
参考文档
- http://javascript.ruanyifeng.com/grammar/object.html
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide...\_with\_Objects
- https://www.cnblogs.com/ihboy/p/6700059.html
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Refer...
- https://segmentfault.com/a/1190000011294519
- 《你所不知道的javaScript上卷》
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。