[关闭]
@duanyubin 2015-12-24T08:48:01.000000Z 字数 13873 阅读 411

Javascript 高级特性

javascript


来自《Node.js开发指南》

1 作用域

作用域(scope)是结构化编程语言中的重要概念,它决定了变量的可见范围和生命周
期,正确使用作用域可以使代码更清晰、易懂。作用域可以减少命名冲突,而且是垃圾回收
的基本单元。和 C、C++、Java 等常见语言不同,JavaScript 的作用域不是以花括号包围的块
级作用域(block scope),这个特性经常被大多数人忽视,因而导致莫名其妙的错误。例如
下面代码,在大多数类 C 的语言中会出现变量未定义的错误,而在 JavaScript 中却完全合法:

  1. if (true) {
  2. var somevar = 'value';
  3. }
  4. console.log(somevar); // 输出 value

这是因为 JavaScript 的作用域完全是由函数来决定的,if、for 语句中的花括号不是独
立的作用域。

1.1函数作用域

不同于大多数类 C 的语言,由一对花括号封闭的代码块就是一个作用域,JavaScript 的
作用域是通过函数来定义的,在一个函数中定义的变量只对这个函数内部可见,我们称为函
数作用域。在函数中引用一个变量时,JavaScript 会先搜索当前函数作用域,或者称为“局
部作用域”,如果没有找到则搜索其上层作用域,一直到全局作用域。我们看一个简单的
例子:

  1. var v1 = 'v1';
  2. var f1 = function() {
  3. console.log(v1); // 输出 v1
  4. };
  5. f1();
  6. var f2 = function() {
  7. var v1 = 'local';
  8. console.log(v1); // 输出 local
  9. };
  10. f2();

以上示例十分明了,JavaScript 的函数定义是可以嵌套的,每一层是一个作用域,变量
搜索顺序是从内到外。下面这个例子可能就有些令人困惑:

  1. var scope = 'global';
  2. var f = function() {
  3. console.log(scope); // 输出 undefined
  4. var scope = 'f';
  5. }
  6. f();

上面代码可能和你预想的不一样,没有输出 global,而是undefined,这是为什么呢?
这是 JavaScript 的一个特性,按照作用域搜索顺序,在 console.log 函数访问 scope 变
量时,JavaScript 会先搜索函数 f 的作用域,恰巧在 f 作用域里面搜索到 scope 变量,
所以上层作用域中定义的 scope 就被屏蔽了,但执行到 console.log 语句时,scope 还
没被定义,或者说初始化,所以得到的就是 undefined 值了。

我们还可以从另一个角度来理解:对于开发者来说,在访问未定义的变量或定义了但没
有初始化的变量时,获得的值都是 undefined。于是我们可以认为,无论在函数内什么地
方定义的变量,在一进入函数时就被定义了,但直到 var 所在的那一行它才被初始化,所
以在这之前引用到的都是 undefined 值。(事实上,JavaScript 的内部实现并不是这样,未
定义变量和值为 undefined 的变量还是有区别的。)

函数作用域的嵌套
接下来看一个稍微复杂的例子:

  1. var f = function() {
  2. var scope = 'f0';
  3. (function() {
  4. var scope = 'f1';
  5. (function() {
  6. console.log(scope); // 输出 f1
  7. })();
  8. })();
  9. };
  10. f();

上面是一个函数作用域嵌套的例子,我们在最内层函数引用了 scope 变量,通过作用
域搜索,找到了其父作用域中定义的 scope 变量。

有一点需要注意:函数作用域的嵌套关系是定义时决定的,而不是调用时决定的,也就
是说,JavaScript 的作用域是静态作用域,又叫词法作用域,这是因为作用域的嵌套关系可
以在语法分析时确定,而不必等到运行时确定。下面的例子说明了这一切:

  1. var scope = 'top';
  2. var f1 = function() {
  3. console.log(scope);
  4. };
  5. f1(); // 输出 top
  6. var f2 = function() {
  7. var scope = 'f2';
  8. f1();
  9. };
  10. f2(); // 输出 top

这个例子中,通过 f2 调用的 f1 在查找 scope 定义时,找到的是父作用域中定义
的 scope 变量,而不是 f2 中定义的 scope 变量。这说明了作用域的嵌套关系不是在调用
时确定的,而是在定义时确定的。

1.2 全局作用域

在 JavaScript 中有一种特殊的对象称为 全局对象。这个对象在Node.js 对应的是 global
对象,在浏览器中对应的是 window 对象。由于全局对象的所有属性在任何地方都是可见的,
所以这个对象又称为 全局作用域。全局作用域中的变量不论在什么函数中都可以被直接引
用,而不必通过全局对象

满足以下条件的变量属于全局作用域:
- 在最外层定义的变量;
- 全局对象的属性;
- 任何地方隐式定义的变量(未定义直接赋值的变量)。

需要格外注意的是第三点,在任何地方隐式定义的变量都会定义在全局作用域中,即不
通过 var 声明直接赋值的变量。这一点经常被人遗忘,而模块化编程的一个重要原则就是
避免使用全局变量,所以我们在任何地方都不应该隐式定义变量。

2 闭包

闭包(closure)是函数式编程中的概念,出现于 20 世纪 60 年代,最早实现闭包的语言
是 Scheme,它是 LISP 的一种方言。之后闭包特性被其他语言广泛吸纳。

闭包的严格定义是“由函数(环境)及其封闭的自由变量组成的集合体。”这个定义对
于大家来说有些晦涩难懂,所以让我们先通过例子和不那么严格的解释来说明什么是闭包,
然后再举例说明一些闭包的经典用途。

2.1 什么是闭包

通俗地讲,JavaScript 中每个的函数都是一个闭包,但通常意义上嵌套的函数更能够体
现出闭包的特性,请看下面这个例子:

  1. var generateClosure = function() {
  2. var count = 0;
  3. var get = function() {
  4. count ++;
  5. return count;
  6. };
  7. return get;
  8. };
  9. var counter = generateClosure();
  10. console.log(counter()); // 输出 1
  11. console.log(counter()); // 输出 2
  12. console.log(counter()); // 输出 3

这段代码中,generateClosure() 函数中有一个局部变量count,初值为 0。还有一
个叫做 get 的函数,get 将其父作用域,也就是 generateClosure() 函数中的 count 变
量增加 1,并返回 count 的值。generateClosure() 的返回值是 get 函数。在外部我们
通过 counter 变量调用了 generateClosure() 函数并获取了它的返回值,也就是 get 函
数,接下来反复调用几次 counter(),我们发现每次返回的值都递增了 1。

让我们看看上面的例子有什么特点,按照通常命令式编程思维的理解,count 是
generateClosure 函数内部的变量,它的生命周期就是 generateClosure 被调用的时
期,当 generateClosure 从调用栈中返回时,count 变量申请的空间也就被释放。问题是,在 generateClosure() 调用结束后,counter() 却引用了“已经释放了的” count
变量,而且非但没有出错,反而每次调用 counter() 时还修改并返回了 count。这是怎
么回事呢?

这正是所谓闭包的特性。当一个函数返回它内部定义的一个函数时,就产生了一个闭包,
闭包不但包括被返回的函数,还包括这个函数的定义环境。上面例子中,当函数
generateClosure() 的内部函数 get 被一个外部变量 counter 引用时,counter 和
generateClosure() 的局部变量就是一个闭包。如果还不够清晰,下面这个例子可以帮助
你理解:

  1. var generateClosure = function() {
  2. var count = 0;
  3. var get = function() {
  4. count ++;
  5. return count;
  6. };
  7. return get;
  8. };
  9. var counter1 = generateClosure();
  10. var counter2 = generateClosure();
  11. console.log(counter1()); // 输出 1
  12. console.log(counter2()); // 输出 1
  13. console.log(counter1()); // 输出 2
  14. console.log(counter1()); // 输出 3
  15. console.log(counter2()); // 输出 2

上面这个例子解释了闭包是如何产生的:counter1 和 counter2 分别调用了 generate-
Closure() 函数,生成了两个闭包的实例,它们内部引用的 count 变量分别属于各自的
运行环境。我们可以理解为,在 generateClosure() 返回 get 函数时,私下将 get 可
能引用到的 generateClosure() 函数的内部变量(也就是 count 变量)也返回了,并
在内存中生成了一个副本,之后 generateClosure() 返回的函数的两个实例 counter1
和 counter2 就是相互独立的了。

2.2 闭包的用途

1. 嵌套的回调函数
闭包有两个主要用途,一是实现嵌套的回调函数,二是隐藏对象的细节。让我们先看下
面这段代码示例,了解嵌套的回调函数。如下代码是在 Node.js 中使用 MongoDB 实现一个
简单的增加用户的功能:

  1. exports.add_user = function(user_info, callback) {
  2. var uid = parseInt(user_info['uid']);
  3. mongodb.open(function(err, db) {
  4. if (err) {callback(err); return;}
  5. db.collection('users', function(err, collection) {
  6. if (err) {callback(err); return;}
  7. collection.ensureIndex("uid", function(err) {
  8. if (err) {callback(err); return;}
  9. collection.ensureIndex("username", function(err) {
  10. if (err) {callback(err); return;}
  11. collection.findOne({uid: uid}, function(err) {
  12. if (err) {callback(err); return;}
  13. if (doc) {
  14. callback('occupied');
  15. } else {
  16. var user = {
  17. uid: uid,
  18. user: user_info,
  19. };
  20. collection.insert(user, function(err) {
  21. callback(err);
  22. });
  23. }
  24. });
  25. });
  26. });
  27. });
  28. });
  29. };

如果你对 Node.js 或 MongoDB 不熟悉,没关系,不需要去理解细节,只要看清楚大概
的逻辑即可。这段代码中用到了闭包的层层嵌套,每一层的嵌套都是一个回调函数。回调函
数不会立即执行,而是等待相应请求处理完后由请求的函数回调。我们可以看到,在嵌套的
每一层中都有对 callback 的引用,而且最里层还用到了外层定义的 uid 变量。由于闭包
机制的存在,即使外层函数已经执行完毕,其作用域内申请的变量也不会释放,因为里层的
函数还有可能引用到这些变量,这样就完美地实现了嵌套的异步回调。

2. 实现私有成员
我们知道,JavaScript 的对象没有私有属性,也就是说对象的每一个属性都是曝露给外部
的。这样可能会有安全隐患,譬如对象的使用者直接修改了某个属性,导致对象内部数据的一
致性受到破坏等。JavaScript通过约定在所有私有属性前加上下划线(例如_myPrivateProp),表示这个属性是私有的,外部对象不应该直接读写它。但这只是个非正式的约定,假设对象
的使用者不这么做,有没有更严格的机制呢?答案是有的,通过闭包可以实现。让我们再看
看前面那个例子:

  1. var generateClosure = function() {
  2. var count = 0;
  3. var get = function() {
  4. count ++;
  5. return count;
  6. };
  7. return get;
  8. };
  9. var counter = generateClosure();
  10. console.log(counter()); // 输出 1
  11. console.log(counter()); // 输出 2
  12. console.log(counter()); // 输出 3

我们可以看到,只有调用 counter() 才能访问到闭包内的 count 变量,并按照规则
对其增加1,除此之外决无可能用其他方式找到 count 变量。受到这个简单例子的启发,
我们可以把一个对象用闭包封装起来,只返回一个“访问器”的对象,即可实现对细节隐藏。
关于实现JavaScript对象私有成员的更多信息,请参考http://javascript.crockford.com/private.html

3 对象

提起面向对象的程序设计语言,立刻让人想起的是 C++、Java 等这类静态强类型语言,
以及 Python、Ruby 等脚本语言,它们共有的特点是基于类的面向对象。而说到 JavaScript,
很少能让人想到它面向对象的特性,甚至有人说它不是面向对象的语言,因为它没有类。没
错,JavaScript 真的没有类,但 JavaScript 是面向对象的语言。JavaScript 只有对象,对象就
是对象,不是类的实例。

因为绝大多数面向对象语言中的对象都是基于类的,所以经常有人混淆类的实例与对象
的概念。对象就是类的实例,这在大多数语言中都没错,但在 JavaScript 中却不适用。
JavaScript 中的对象是基于原型的,因此很多人在初学 JavaScript 对象时感到无比困惑。通过
这一节,我们将重新认识 JavaScript 中对象,充分理解基于原型的面向对象的实质。

3.1 创建和访问

JavaScript 中的对象实际上就是一个由属性组成的关联数组,属性由名称和值组成,值
的类型可以是任何数据类型,或者函数和其他对象。注意 JavaScript 具有函数式编程的特性,
所以函数也是一种变量,大多数时候不用与一般的数据类型区分。

在 JavaScript 中,你可以用以下方法创建一个简单的对象:

  1. var foo = {};
  2. foo.prop_1 = 'bar';
  3. foo.prop_2 = false;
  4. foo.prop_3 = function() {
  5. return 'hello world';
  6. }
  7. console.log(foo.prop_3());

以上代码中,我们通过 var foo = {}; 创建了一个对象,并将其引用赋值给 foo,
通过 foo.prop1 来获取它的成员并赋值,其中 {} 是对象字面量的表示方法,也可以用 var
foo = new Object() 来显式地创建一个对象。

1. 使用关联数组访问对象成员
我们还可以用关联数组的模式来创建对象,以上代码修改为:

  1. var foo = {};
  2. foo['prop1'] = 'bar';
  3. foo['prop2'] = false;
  4. foo['prop3'] = function() {
  5. return 'hello world';
  6. }

在 JavaScript 中,使用句点运算符和关联数组引用是等价的,也就是说任何对象(包括
this 指针)都可以使用这两种模式。使用关联数组的好处是,在我们不知道对象的属性名
称的时候,可以用变量来作为关联数组的索引。例如:

  1. var some_prop = 'prop2';
  2. foo[some_prop] = false;

2. 使用对象初始化器创建对象
上述的方法只是让你对JavaScript对象的定义有个了解,真正在使用的时候,我们会采用
下面这种更加紧凑明了的方法:

  1. var foo = {
  2. 'prop1': 'bar',
  3. prop2: 'false',
  4. prop3: function (){
  5. return 'hello world';
  6. }
  7. };

这种定义的方法称为对象的初始化器。注意,使用初始化器时,对象属性名称是否加引
号是可选的,除非属性名称中有空格或者其他可能造成歧义的字符,否则没有必要使用引号。

3.2 构造函数

前一小节讲述的对象创建方法都有一个弱点,就是创建对象的代码是一次性的。如果我
们想创建多个规划好的对象,有若干个固定的属性、方法,并能够初始化,就像 C++ 语言
中的对象一样,该如何做呢?别担心,JavaScript 提供了构造函数,让我们来看看应该如何
创建复杂的对象。

  1. function User(name, uri) {
  2. this.name = name;
  3. this.uri = uri;
  4. this.display = function() {
  5. console.log(this.name);
  6. }
  7. }

以上是一个简单的构造函数,接下来用 new 语句来创建对象:

  1. var someuser = new User('byvoid', 'http://www.byvoid.com');

然后就可以通过 someuser 来访问这个对象的属性和方法了。

3.3 上下文对象

在 JavaScript 中,上下文对象就是 this 指针,即被调用函数所处的环境。上下文对象
的作用是在一个函数内部引用调用它的对象本身,JavaScript 的任何函数都是被某个对象调
用的,包括全局对象,所以 this 指针是一个非常重要的东西。

在前面使用构造函数的代码中我们已经看到了 this 的使用方法,下面代码可以更佳清
楚地说明上下文对象的使用方式:

  1. var someuser = {
  2. name: 'byvoid',
  3. display: function() {
  4. console.log(this.name);
  5. }
  6. };
  7. someuser.display(); // 输出 byvoid
  8. var foo = {
  9. bar: someuser.display,
  10. name: 'foobar'
  11. };
  12. foo.bar(); // 输出 foobar

JavaScript 的函数式编程特性使得函数可以像一般的变量一样赋值、传递和计算,我们
看到在上面代码中,foo 对象的 bar 属性是 someuser.display 函数,使用 foo.bar()
调用时,bar 和 foo 对象的函数看起来没有区别,其中的 this 指针不属于某个函数,而
是函数调用时所属的对象。

在 JavaScript 中,本质上,函数类型的变量是指向这个函数实体的一个引用,在引用之
间赋值不会对对象产生复制行为。我们可以通过函数的任何一个引用调用这个函数,不同之
处仅仅在于上下文。下面例子可以帮助我们理解:

  1. var someuser = {
  2. name: 'byvoid',
  3. func: function() {
  4. console.log(this.name);
  5. }
  6. };
  7. var foo = {
  8. name: 'foobar'
  9. };
  10. someuser.func(); // 输出 byvoid
  11. foo.func = someuser.func;
  12. foo.func(); // 输出 foobar
  13. name = 'global';
  14. func = someuser.func;
  15. func(); // 输出 global

仔细观察上面的例子,使用不同的引用来调用同一个函数时,this 指针永远是这个引
用所属的对象。在前面的章节中我们提到了 JavaScript 的函数作用域是静态的,也就是说一
个函数的可见范围是在预编译的语法分析中就可以确定的,而上下文对象则可以看作是静态
作用域的补充。

1. call 和 apply
在 JavaScript 中,call 和 apply 是两个神奇的方法,但同时也是容易令人迷惑的两个
方法,乃至许多对 JavaScript 有经验的人也不太清楚它们的用法。call 和 apply 的功能是
以不同的对象作为上下文来调用某个函数。简而言之,就是允许一个对象去调用另一个对象
的成员函数。乍一看似乎很不可思议,而且容易引起混乱,但其实 JavaScript 并没有严格的所谓“成员函数”的概念,函数与对象的所属关系在调用时才展现出来。灵活使用 call 和
apply 可以节省不少时间,在后面我们可以看到,call 可以用于实现对象的继承。

call 和 apply 的功能是一致的,两者细微的差别在于 call 以参数表来接受被调用函
数的参数,而 apply 以数组来接受被调用函数的参数。call 和 apply 的语法分别是:

  1. func.call(thisArg[, arg1[, arg2[, ...]]])
  2. func.apply(thisArg[, argsArray])

其中,func 是函数的引用,thisArg 是 func 被调用时的上下文对象,arg1、arg2 或
argsArray 是传入 func 的参数。我们以下面一段代码为例介绍 call 的工作机制:

  1. var someuser = {
  2. name: 'byvoid',
  3. display: function(words) {
  4. console.log(this.name + ' says ' + words);
  5. }
  6. };
  7. var foo = {
  8. name: 'foobar'
  9. };
  10. someuser.display.call(foo, 'hello'); // 输出 foobar says hello

用 Node.js 运行这段代码,我们可以看到控制台输出了 foobar。someuser.display 是
被调用的函数,它通过 call 将上下文改变为 foo 对象,因此在函数体内访问 this.name
时,实际上访问的是 foo.name,因而输出了foobar。

2. bind
如何改变被调用函数的上下文呢?前面说过,可以用 call 或 apply 方法,但如果重复
使用会不方便,因为每次都要把上下文对象作为参数传递,而且还会使代码变得不直观。针
对这种情况,我们可以使用 bind 方法来永久地绑定函数的上下文,使其无论被谁调用,上
下文都是固定的。bind 语法如下:func.bind(thisArg[, arg1[, arg2[, ...]]])

其中 func 是待绑定函数,thisArg 是改变的上下文对象,arg1、arg2 是绑定的参
数表。bind 方法返回值是上下文为 thisArg 的 func。通过下面例子可以帮你理解 bind
的使用方法:

  1. var someuser = {
  2. name: 'byvoid',
  3. func: function() {
  4. console.log(this.name);
  5. }
  6. };
  7. var foo = {
  8. name: 'foobar'
  9. };
  10. foo.func = someuser.func;
  11. foo.func(); // 输出 foobar
  12. foo.func1 = someuser.func.bind(someuser);
  13. foo.func1(); // 输出 byvoid
  14. func = someuser.func.bind(foo);
  15. func(); // 输出 foobar
  16. func2 = func;
  17. func2(); // 输出 foobar

上面代码直接将 foo.func 赋值为 someuser.func,调用 foo.func() 时,this指
针为 foo,所以输出结果是 foobar。foo.func1 使用了 bind 方法,将 someuser 作
为this指针绑定到 someuser.func,调用 foo.func1() 时,this指针为 someuser,
所以输出结果是 byvoid。全局函数 func 同样使用了 bind 方法,将 foo 作为 this 指
针绑定到 someuser.func,调用 func() 时,this 指针为 foo,所以输出结果是 foobar。
而 func2 直接将绑定过的 func 赋值过来,与 func 行为完全相同。

3. 使用 bind 绑定参数表
bind 方法还有一个重要的功能绑定参数表,如下例所示。

  1. var person = {
  2. name: 'byvoid',
  3. says: function(act, obj) {
  4. console.log(this.name + ' ' + act + ' ' + obj);
  5. }
  6. };
  7. person.says('loves', 'diovyb'); // 输出 byvoid loves diovyb
  8. byvoidLoves = person.says.bind(person, 'loves');
  9. byvoidLoves('you'); // 输出 byvoid loves you

可以看到,byvoidLoves 将 this 指针绑定到了 person,并将第一个参数绑定到
loves,之后在调用 byvoidLoves 的时候,只需传入第三个参数。这个特性可以用于创建
一个函数的“捷径”,之后我们可以通过这个“捷径”调用,以便在代码多处调用时省略重
复输入相同的参数。

3.4 原型

原型是 JavaScript 面向对象特性中重要的概念,也是大家太熟悉的概念。因为在绝大多
数的面向对象语言中,对象是基于类的(例如 Java 和 C++ ),对象是类实例化的结果。而在
JavaScript 语言中,没有类的概念①,对象由对象实例化。打个比方来说,基于类的语言中类
就像一个模具,对象由这个模具浇注产生,而基于原型的语言中,原型就好像是一件艺术品
的原件,我们通过一台 100% 精确的机器把这个原件复制出很多份。

前面小节的例子中都没有涉及原型,仅仅通过构造函数和 new 语句生成类,让我们看
看如何使用原型和构造函数共同生成对象。

  1. function Person() {}
  2. Person.prototype.name = 'BYVoid';
  3. Person.prototype.showName = function () {
  4. console.log(this.name);
  5. };
  6. var person = new Person();
  7. person.showName();

上面这段代码使用了原型而不是构造函数初始化对象。这样做与直接在构造函数内定义
属性有什么不同呢?

下面这段代码可以验证以上问题:

  1. function Foo() {
  2. var innerVar = 'hello';
  3. this.prop1 = 'BYVoid';
  4. this.func1 = function(){
  5. innerVar = '';
  6. };
  7. }
  8. Foo.prototype.prop2 = 'Carbo';
  9. Foo.prototype.func2 = function () {
  10. console.log(this.prop2);
  11. };
  12. var foo1 = new Foo();
  13. var foo2 = new Foo();
  14. console.log(foo1.func1 == foo2.func1); // 输出 false
  15. console.log(foo1.func2 == foo2.func2); // 输出 true

尽管如此,并不是说在构造函数内创建属性不好,而是两者各有适合的范围。那么我们
什么时候使用原型,什么时候使用构造函数内定义来创建属性呢?

原型链
JavaScript 中有两个特殊的对象: Object 与 Function,它们都是构造函数,用于生
成对象。Object.prototype 是所有对象的祖先,Function.prototype 是所有函数的原
型,包括构造函数。我把 JavaScript 中的对象分为三类,一类是用户创建的对象,一类是构
造函数对象,一类是原型对象。用户创建的对象,即一般意义上用 new 语句显式构造的对
象。构造函数对象指的是普通的构造函数,即通过 new 调用生成普通对象的函数。原型对象
特指构造函数 prototype 属性指向的对象。这三类对象中每一类都有一个 proto
性,它指向该对象的原型,从任何对象沿着它开始遍历都可以追溯到 Object.prototype。
构造函数对象有 prototype 属性,指向一个原型对象,通过该构造函数创建对象时,被创
建对象的 proto 属性将会指向构造函数的 prototype 属性。原型对象有 constructor
属性,指向它对应的构造函数。让我们通过下面这个例子来理解原型:

  1. function Foo() {}
  2. Object.prototype.name = 'My Object';
  3. Foo.prototype.name = 'Bar';
  4. var obj = new Object();
  5. var foo = new Foo();
  6. console.log(obj.name); // 输出 My Object
  7. console.log(foo.name); // 输出 Bar
  8. console.log(foo.__proto__.name); // 输出 Bar
  9. console.log(foo.__proto__.__proto__.name); // 输出 My Object
  10. console.log(foo. __proto__.constructor.prototype.name); // 输出 Bar

我们定义了一个叫做 Foo ()的构造函数,生成了对象 foo。同时我们还分别给 Object
和 Foo 生成原型对象。

下图解析了它们之间错综复杂的关系
此处输入图片的描述

在 JavaScript 中,继承是依靠一套叫做原型链(prototype chain)的机制实现的。属性
继承的本质就是一个对象可以访问到它的原型链上任何一个原型对象的属性。例如上例的
foo 对象,它拥有 foo. proto 和 foo. proto.proto 所有属性的浅拷
贝(只复制基本数据类型,不复制对象)。所以可以直接访问foo.constructor(来自foo.
proto,即Foo.prototype),foo.toString(来自foo. proto.proto
即Object.prototype)。

3.5 对象的复制

JavaScript 和 Java 一样都没有像C语言中一样的指针,所有对象类型的变量都是指向对
象的引用,两个变量之间赋值传递一个对象并不会对这个对象进行复制,而只是传递引用。
有些时候我们需要完整地复制一个对象,这该如何做呢? Java 语言中有 clone 方法可以实
现对象复制,但 JavaScript 中没有这样的函数。因此我们需要手动实现这样一个函数,一个
简单的做法是复制对象的所有属性:

  1. Object.prototype.clone = function() {
  2. var newObj = {};
  3. for (var i in this) {
  4. newObj[i] = this[i];
  5. }
  6. return newObj;
  7. }
  8. var obj = {
  9. name: 'byvoid',
  10. likes: ['node']
  11. };
  12. var newObj = obj.clone();
  13. obj.likes.push('python');
  14. console.log(obj.likes); // 输出 [ 'node', 'python' ]
  15. console.log(newObj.likes); // 输出 [ 'node', 'python' ]

上面的代码是一个对象浅拷贝(shallow copy)的实现,即只复制基本类型的属性,而
共享对象类型的属性。浅拷贝的问题是两个对象共享对象类型的属性,例如上例中 likes 属
性指向的是同一个数组。

实现一个完全的复制,或深拷贝(deep copy)并不是一件容易的事,因为除了基本数据
类型,还有多种不同的对象,对象内部还有复杂的结构,因此需要用递归的方式来实现:

  1. Object.prototype.clone = function() {
  2. var newObj = {};
  3. for (var i in this) {
  4. if (typeof(this[i]) == 'object' || typeof(this[i]) == 'function') {
  5. newObj[i] = this[i].clone();
  6. } else {
  7. newObj[i] = this[i];
  8. }
  9. }
  10. return newObj;
  11. };
  12. Array.prototype.clone = function() {
  13. var newArray = [];
  14. for (var i = 0; i < this.length; i++) {
  15. if (typeof(this[i]) == 'object' || typeof(this[i]) == 'function') {
  16. newArray[i] = this[i].clone();
  17. } else {
  18. newArray[i] = this[i];
  19. }
  20. }
  21. return newArray;
  22. };
  23. Function.prototype.clone = function() {
  24. var that = this;
  25. var newFunc = function() {
  26. return that.apply(this, arguments);
  27. };
  28. for (var i in this) {
  29. newFunc[i] = this[i];
  30. }
  31. return newFunc;
  32. };
  33. var obj = {
  34. name: 'byvoid',
  35. likes: ['node'],
  36. display: function() {
  37. console.log(this.name);
  38. },
  39. };
  40. var newObj = obj.clone();
  41. newObj.likes.push('python');
  42. console.log(obj.likes); // 输出 [ 'node' ]
  43. console.log(newObj.likes); // 输出 [ 'node', 'python' ]
  44. console.log(newObj.display == obj.display); // 输出 false

上面这个实现看起来很完美,它不仅递归地复制了对象复杂的结构,还实现了函数的深
拷贝。这个方法在大多数情况下都很好用,但有一种情况它却无能为力,例如下面的代码:

  1. var obj1 = {
  2. ref: null
  3. };
  4. var obj2 = {
  5. ref: obj1
  6. };
  7. obj1.ref = obj2;

这段代码的逻辑非常简单,就是两个相互引用的对象。当我们试图使用深拷贝来复制
obj1 和 obj2 中的任何一个时,问题就出现了。因为深拷贝的做法是遇到对象就进行递归
复制,那么结果只能无限循环下去。对于这种情况,简单的递归已经无法解决,必须设计一
套图论算法,分析对象之间的依赖关系,建立一个拓扑结构图,然后分别依次复制每个顶点,
并重新构建它们之间的依赖关系。这已经超出了本书的讨论范围,而且在实际的工程操作中
几乎不会遇到这种需求,所以我们就不继续讨论了。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注