Jiahonzheng's Blog

阅读笔记(一):《你不知道的JavaScript》

字数统计: 2.3k阅读时长: 9 min
2018/03/21 Share

闭包

书中的章节总结,给出了 闭包 的定义:

当函数可以记住并访问所在的词法作用域时,即使函数时在当前词法作用域之外执行,这时就产生了闭包。

MDN 定义:

闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。

结合书中给出的代码,解释 闭包 的概念。

1
2
3
4
5
6
7
8
9
10
11
function foo() {
var a = 2;
function bar() {
// bar()的词法作用域可访问 foo()的内部作用域
console.log(a);
}
return bar;
}
var baz = foo();
// bar() 在之前的词法作用域之外执行
baz(); // 2 —— 朋友,这就是闭包的效果

从函数声明,我们注意到,函数 bar() 的词法作用域可以访问到 foo() 的内部作用域。

在执行 foo() 后,其返回值(即内部的 bar() )赋值给了 baz 并调用了 baz() ,即 bar() 在自己定义的词法作用域之外成功执行了。

上述过程,产生了 闭包 ,从上述过程,我们得知:

闭包使得函数可以继续访问定义时的词法作用域。

事实上,当我们将函数作为值来进行传递时,就产生了 闭包 。当我们注册异步任务时,如果使用了回调函数,就产生了 闭包 。

由于 闭包 是个非常强大的工具,可以通过 闭包 来实现 模块 。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
var MyModules = (function Manager() {
var modules = {};

// 定义模块
// name 为模块名称
// deps 为该模块的依赖
// impl 为模块的包装函数
function define(name, deps, impl) {
for (var i=0; i<deps.length; i++) {
deps[i] = modules[deps[i]];
}
// 核心
modules[name] = impl.apply(impl, deps);
}

// 导出模块
function get(name) {
return modules[name];
}

// define 与 get 在外部作用域,仍可访问定义时的作用域,即发生了闭包
return {
define: define,
get: get
}
})();

// 定义 bar 模块
MyModules.define("bar", [], function() {
function hello(who) {
return "Let me introduce: " + who;
}

return {
hello: hello
};
});

// 定义 foo 模块
MyModules.define("foo", ["bar"], function(bar) {
var hungry = "hippo";

function awesome() {
console.log(bar.hello(hungry).toUpperCase());
}

return {
awesome: awesome
};
});

var bar = MyModules.get("bar");
var foo = MyModules.get("foo");

console.log(bar.hello("hippo")); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO

上述代码,对我理解模块系统有很大的帮助,故摘录于此。

对象

常见错误说法

JavaScript 中万物皆是对象。

不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判断为 object 类型,null 的二进制表示是全 0 ,自然前三位为 0 ,故对 null 执行 typeof 时会返回 “object” 。

属性名

对象中的属性名永远为字符串。

1
2
3
4
5
6
var obj = {};

obj[true] = "foo";
obj[1] = "bar";
obj[obj] = "baz";
console.log(obj); // {"1": "bar", "true": "foo", "[obejct Object]": "baz"}

从上述代码,可以看到:所有的属性名都被转换为字符串。

数组是一种内置对象,可以添加属性,当属性不能转换为数字时,数组的长度不会发生变化:

1
2
3
4
var arr = [];

arr.baz = "baz";
console.log(arr.length); // 0

当属性可以转换为数字时,即可转换为数值下标,从而修改了数组的长度。

1
2
3
4
5
var arr = [];

arr["5"] = "baz";
console.log(arr); // [0, 0, 0, 0, 0, 1]
console.log(arr.length); // 6

对象复制

1
2
3
4
5
6
7
8
9
10
11
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 引用的对象是一样的。

浅复制

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

1
2
3
4
5
6
var newObj = Object.assign({}, myObject);

console.log(newObj.a); // 2
console.log(newObj.b === anotherObject); // true
console.log(newObj.c === anotherArray); // true
console.log(newObj.d === anotherFunction); // true

深复制

对于可被 JSON 序列化的对象来说,可使用以下方法进行函数深拷贝。

1
var newObj = JSON.parse(JSON.stringify(obj));

属性描述符

从 ES5 开始, JavaScript 开始提供可直接检测属性特性的方法。

1
2
3
4
5
6
7
8
9
var obj = {a: 2};

Object.getOwnPropertyDescriptor(obj, "a");
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }

在上述代码中,我们通过使用 Object.getOwnPropertyDescriptor() 方法,返回了一个指定对象自有属性的属性描述符。

在为一对象创建普通属性时,会采用默认的属性描述符,默认属性即为上述代码注释。当然,我们可通过使用 Object.defineProperty() 方法给 configurable 对象添加或修改属性。

1
2
3
4
5
6
7
8
9
var obj = {};

// 为 obj 添加属性 a
Object.defineProperty(obj, "a", {
value: 2,
configurable: true,
enumerable: true,
writable: true
});

Writable

该描述符决定是否可修改属性的值。默认值为 true 。

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {};

// 添加 writable 为 false 的属性 a
Object.defineProperty(obj, "a", {
value: 2,
configurable: true,
enumerable: true,
writable: false
});

obj.a = 3;
console.log(obj.a); // 2

在严格模式下,尝试修改 writable 为 false 的属性,会导致 TypeError 错误。

Configurable

该描述符决定是否可采用 Object.defineProperty() 方法或 delete 方法对属性进行操作。默认值为 true 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var obj = {};

// 添加 configurable 为 false 的属性 a
Object.defineProperty(obj, "a", {
value: 2,
configurable: false,
enumerable: true,
writable: true
});

obj.a = 5;
console.log(obj.a); // 5

Object.defineProperty(obj, "a", {
value: 2,
configurable: true,
enumerable: true,
writable: true
});
// TypeError

在声明一个属性的 configurable 为 false 后,我们是不能通过 Object.defineProperty() 方法重新配置属性描述符,否则会报出 TypeError 。

同时,在设置 configurable 为 false 后,我们无法对属性进行 delete 操作。

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {};

Object.defineProperty(obj, "a", {
value: 2,
configurable: false,
enumerable: true,
writable: true
});

console.log(obj.a); // 2
delete obj.a;
console.log(obj.a) // 2

Enumerable

该描述符决定该属性是否出现在对象的属性枚举中,默认值为 true 。

当属性设置了 enumerable 为 false 时,其在 for … in 循环 和 Object.keys() 中不会出现。

我们可以使用 obj.propertysIsEnumerable(prop) 检查属性(不在原型链)是否可枚举。

访问描述符

我们可以通过 getter 和 setter 改写某个属性在读写时的默认操作。

getter 是一个隐藏函数,会在获取属性值时调用

setter 是一个隐藏函数,会在设置属性值时调用

当我们给属性定义 getter 和 setter 时,这些属性会被定义为访问描述符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = {
get a(){
return 2;
}
};

Object.defineProperty(obj, "b", {
get: function() {
return this.a * 2;
}
});

console.log(obj.a); // 2
console.log(obj.b); // 4

对于访问描述符,引擎会忽略它们的 value 和 writable 属性。

1
2
obj.a = 3;
console.log(obj.a); //2

由于我们只定义了 a 的 getter ,所以对 a 的值进行设置时,会忽略赋值操作。因此,为了让属性使用更为合理,我们需要定义 setter 。

1
2
3
4
5
6
7
8
9
10
11
var obj = {
get a() {
return this._a;
},
set a(val) {
this._a = val * 2;
}
};

obj.a = 2;
console.log(obj.a); // 4

属性的存在性

  • in 操作符会检查属性是否在对象及其原型链中。
  • hasOwnProperty 则只会检查属性是否在对象中,不会检查原型链。
  • Object.keys() 和 Object.getOwnPropertNames() 都只会查找对象自有的属性。
    • Object.keys() 返回一个数组,包含所有可枚举属性。
    • Object.getOwnPropertyNames() 返回一个数组,包含所有属性。

混合对象“类”

类理论

  • 类是一种设计模式,类意味着复制。
  • 类 / 继承描述了一种代码的组织结构形式——一种在软件中对真实世界中问题领域的建模方法。
  • 类的另一个核心概念是多态,这个概念是说父类的通用行为可以被子类用更特殊的行为重写。

JavaScript 中的类

传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类中。而在 JavaScript 中,由于对象只能复制引用,所以在继承或实例化时, JavaScript 的对象机制并不会自动执行复制行为。一个对象并不会被复制到其他对象,它们会被关联起来。

混入

由于 JavaScript 在继承或实例化时,不能自动执行对象的复制行为,故可通过 混入 的方式模拟类的复制行为。

我们可以通过 显式混入 或 隐式混入 来模拟类的复制行为,但通常会产生丑陋并且脆弱的语法。此外,显式混入 实际上不能完全模拟类的复制行为,因为对象(和函数)只能复制引用,无法复制被引用的对象或者函数本身。

CATALOG
  1. 1. 闭包
  2. 2. 对象
    1. 2.1. 常见错误说法
    2. 2.2. 属性名
    3. 2.3. 对象复制
      1. 2.3.1. 浅复制
      2. 2.3.2. 深复制
    4. 2.4. 属性描述符
      1. 2.4.1. Writable
      2. 2.4.2. Configurable
      3. 2.4.3. Enumerable
    5. 2.5. 访问描述符
    6. 2.6. 属性的存在性
  • 混合对象“类”
    1. 0.1. 类理论
    2. 0.2. JavaScript 中的类
    3. 0.3. 混入