在JavaScript中, 一切皆对象, 函数也是特殊的对象.

对象

在JavaScript中, 对象 是名值对的集合, 值可以由多个名值对组成. 名值对是互相关联的一对名和值 Name/Value Pair. 名可以被定义多次,但在特定的 执行环境 中只能匹配一个值.

1
2
3
4
5
6
7
8
9
10
Address:
{
Street: 'Main',
Number: 100,
Apartment:
{
Floor: 3,
Number: 301
}
}

对象的属性

对象及其属性(将属性和方法统称为属性, 下同)都存在于内存空间, 对象可以获取到与其相关联的属性(have a reference to them, 即获取到的是指向属性所在内存空间的地址).

Object in memory

有两种操作符可以获取属性值:

  1. object["property"]: 括号(Computed Member Access Operator)
  2. object.property: 点号(Member Access Operator)

多用点号操作符, 因为更简洁也更利于调试.

对象字面量(Object Literals)

var person = {}: 用对象字面量方法创建对象.

1
2
3
4
5
6
7
8
9
var person = {
firstname: 'John',
lastname: 'Doe',
address: {
street: '111 Main St.',
city: 'New York',
state: 'NY'
}
};
1
2
3
var person = new Object();
person.firstname = 'John';
person.lastname = 'Doe';

两种方法中, 对象字面量创建对象的方法更好, 比new更简洁, 用处更大. 比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var john = {
firstname: 'John',
lastname: 'Doe',
address: {
street: '111 Main St.',
city: 'New York',
state: 'NY'
}
};

function greet(person){
console.log('Hi ' + person.firstname);
}

greet(john);

greet({
firstname: 'Jane',
lastname: 'Doe'
});

// 可以直接传入对象作为参数

尽管语法不一致, 实际的原理是一样的, 都是在内存空间中创建对象及属性.

JSON与对象字面量

过去传输数据常用XML格式:

1
2
3
4
<object>
<firstname>Mary</firstname>
<isAProgrammer>true</isAProgrammer>
</object>

但因为XML格式包含太多额外的标记, 在传输过程中会浪费很多带宽, JSON的出现解决了这样的问题.
JSON实际上是对象字面量语法的一个子集. 并不属于JavaScript, 但因为格式和JS对象十分相似, 所以JavaScript解析JSON格式的数据十分容易. JavaScript中内置了JSON.stringify()将JS对象转化为JSON, JSON.parse()将JSON解析为JS对象.

1
2
3
4
{
"firstname": "Mary",
"isAProgrammer": true
}

函数也是对象

JavaScript中, 函数是一等公民

任何其他数据类型可以做的事, 函数也能做到, 如:

  • 将函数赋给变量
  • 作为参数传递给其他函数
  • 可以被即时创建
1
2
3
4
5
6
7
function log(a) {
a();
};

log(function(){
console.log('hi');
});

函数也是对象, 因此可以为函数关联属性. 其中两个比较特别的属性是函数名和代码. 而函数名并不是必要的, 不存在函数名的函数是匿名函数. 我们所写的那段代码实际上是函数作为对象的其中一个属性, 并不是函数本身, 同时这个属性是可以被调用的.

functions-are-objects

1
2
3
4
5
6
function greet(){
console.log('hi');
}
// 给函数(对象)greet关联language属性
greet.language = 'English';
console.log(greet.language);

function-example

当函数greet被创建时, 该函数被置于内存空间, 在上述情况下, 该函数是全局对象的属性.

函数声明与函数表达式

表达式(expression): 会返回值, 可以将其储存在一个变量中, 但并非必要.

1
2
3
4
5
6
a = 3;
// 3
1 + 2;
// 3
a = {greeting: 'hi'};
// Object {greeting: "hi"};

以上三者都为表达式, 因为都返回了值, 表达式返回的可以是任何类型的值.

声明(statement): 不会返回值.

1
2
3
4
5
var a;

if(a === 3){

}

以上代码中a === 3是表达式, 返回布尔值, if语句是声明.

函数声明:

1
2
3
4
5
greet();

function greet(){
console.log('hi');
};

函数声明直到执行时才返回值, 在执行环境的创建阶段(creation phase)被置于内存空间, 即被提升(hoisting), 因此在声明之前的位置可以调用函数.

函数表达式:

1
2
3
4
5
6
7
anonymousGreet(); // 不可以在这里调用
var anonymousGreet = function(){
console.log('hi');
};
// anonymousGreet();
//在这里调用才能够执行
//

函数表达式不会被提升(no hoisting), 因为事先置于内存空间的是var anonymousGreet;, 因此上述代码会抛出错误.

值(by value)与引用(by reference)

变量(原始类型)通过值传递

1
2
3
4
5
6
7
8
9
// by value
var a = 3;
var b;

b = a;
a = 2;

console.log(a); // 2
console.log(b); // 3

pass by value

  1. 将某个原始类型值(primitive value)赋值给变量a, 此时a就指向了那个值在内存空间所在的地址;
  2. b = a: 变量b指向一个新的内存空间地址, 那个地址所存储的值是a变量所指向值的拷贝, 因此b的值为3;
  3. 因为存储在不同的地址, 因此之后再改变a的值不会影响到b的值.

变量(对象)通过引用传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var c = {greeting: 'hi'};
var d;

d = c;
c.greeting = 'hello'; // mutate

console.log(c); // Object {greeting: 'hello'};
console.log(d); // Object {greeting: 'hello'};

function changeGreeting(obj) {
obj.greeting = 'Hola'; // mutate
};

changeGreeting(d);

console.log(c); // Object {greeting: 'Hola'};
console.log(d); // Object {greeting: 'Hola'};

c = { greeting: 'howdy'};

console.log(c); // Object {greeting: 'howdy'};
console.log(d); // Object {greeting: 'Hola'};

pass by ref

  1. 将对象(包括函数)赋值给变量a时, a指向对象在内存空间所在地址;
  2. b = a: 变量b不会指向新的内存空间地址, 而是指向a所指向的地址, 没有新的对象被创建;
  3. 因此改变a和b的值都会影响到对方.
  4. changeGreeting(d);通过传入函数的方式改变d的行为同样也会影响到c.
  5. 所有对象都是通过引用传递.
  6. c = { greeting: 'howdy'}; 赋值操作符会在内存空间创建一个新地址, 此时c就不再指向原地址, 而d的指向不变, 因此出现c和d值不同的情况.

参数(arguments)

arguments

当函数的执行环境被创建时, JS引擎除了创建变量环境(container for variables), this, 外部环境(for reference), 还创建了另一个特殊的变量arguments.
arguments是所有参数值的集合, 是类数组元素, 并非数组.

设置参数默认值:

1
2
3
4
5
6
7
8
9
10
function greet(firstname, lastname, language){
console.log(firstname);
console.log(lastname);
console.log(language);
}

greet();
// undefined
// undefined
// undefined

尽管greet函数在定义时有三个参数, 但还是可以不传递任何参数调用greet函数, 这是JavaScript的特性, 在大多数编程语言中不可以这样调用, 由于变量声明提升(Hoisting), 结果出现三个undefined, 但这不是我们想要的结果, 可是实际过程中不传递参数直接调用函数的情况很可能出现, 可以通过以下方法避免出现undefined的情况.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function greet(firstname, lastname, language){
// 1. 为参数设定默认值
language = language || 'en';

// 2. 通过arguments变量获取参数数量
// 如果没有参数, 在控制台中提示
// 并直接返回, 中断函数的执行
if(arguments.length === 0){
console.log('Missing parameters');
return;
}
console.log(firstname);
console.log(lastname);
console.log(language);
console.log(arguments);

}

greet(); // "Missing parameters"
greet('John', 'Doe');
// John
// Doe
// en
// ["John", "Doe"]
greet('John', 'Doe', 'es');
// John
// Doe
// es
// ["John", "Doe", "es"]

在ES6中可以这样设定默认值:

1
2
3
4
5
6
7
8
9
10
11
12
function greet(firstname='John', lastname='Doe', language='en'){
console.log(firstname);
console.log(lastname);
console.log(language);
console.log(arguments);
}

greet();
// John
// Doe
// en
// []

‘this’

this的值取决于函数是如何被调用的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var c = {
name: 'The c object',
log: function(){
this.name = 'Updated c object';// 这里的this执行对象c, 因此this.name改变的是对象c的name属性
console.log(this); // Object {name: "Updated c object"}
var setname = function(newname){
this.name = newname;
// console.log(this); // Window
}
setname('Updated again');
console.log(this); // Object {name: "Updated c object"}
}
}
// c.log();
console.log(name); // "Updated again"

对于上述代码的第二个console.log(this);, 因为在对象c内定义了函数表达式setname, 并在内改变了name的值, 因此预期输出的是Object {name: "Updated again"}, 但实际上并非如此, 通过在setname函数中输出this值发现, 这个this指向全局对象(浏览器中是Window), 因此setname实际上是在全局环境中定义了name变量, 在全局环境输出name可以发现这里的name值才是”Updated again”.

为了避免发生这样的错误, 可以预先定义this的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var c = {
name: 'The c object',
log: function(){
var self = this;
self.name = 'Updated c object';
console.log(self); // Object {name: "Updated c object"}
var setname = function(newname){
self.name = newname;
}
setname('Updated again');
console.log(self); // Object {name: "Updated again"}
}
}
c.log();

因为对象通过引用传递, self与this一样指向对象c, 可以通过self改变对象的属性. 在setname函数中, 没有声明self的值, 通过作用域链到外部寻找self变量, 于是找到了在log方法内定义的指向对象c的self.


参考