javascript 作用域 闭包 对象 原理和示例分析(上)-阿里云开发者社区

开发者社区> 开发与运维> 正文
登录阅读全文

javascript 作用域 闭包 对象 原理和示例分析(上)

简介:                                                                                              阅读、理解、思考、实践,再实践、再思考....  深圳小地瓜献上 javascript高级特性包含:作用域、闭包、对象 -----------------------------------------

                                                                                             阅读、理解、思考、实践,再实践、再思考....  深圳小地瓜献上

javascript高级特性包含:作用域、闭包、对象

-----------------------------------------------作用域----------------------------------------------------------------------------------------    

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


      作用域(scope)包含函数作用域和全局作用域

      函数作用域:不同于大多数类 C 的语言,由一对花括号封闭的代码块就是一个作用域,JavaScript 的作用域是通过函数来定义的,在一个函数中定义的变量只对这个函数内部可见,我们称为函
数作用域。在函数中引用一个变量时,JavaScript 会先搜索当前函数作用域,或者称为“局
部作用域” ,如果没有找到则搜索其上层作用域,一直到全局作用域。我们看一个简单的
例子:
var v1 = 'v1';
var f1 = function() {
console.log(v1); // 输出 v1
};
f1();
var f2 = function() {
var v1 = 'local';
console.log(v1); // 输出 local
};
f2();
以上示例十分明了,JavaScript 的函数定义是可以嵌套的,每一层是一个作用域,变量
搜索顺序是从内到外
。下面这个例子可能就有些令人困惑:
var scope = 'global';
var f = function() {
console.log(scope); // 输出 undefined
var scope = 'f';
}
f();
上面代码可能和你预想的不一样, 没有输出  global , 而是 undefined , 这是为什么呢?
这是 JavaScript 的一个特性,按照作用域搜索顺序,在 console.log  函数访问  scope 变
量时,JavaScript 会先搜索函数 f  的作用域,恰巧在 f 作用域里面搜索到 scope 变量,
所以上层作用域中定义的 scope  就被屏蔽了,但执行到  console.log 语句时, scope 还
没被定义,或者说初始化,所以得到的就是  undefined 值了。
我们还可以从另一个角度来理解:对于开发者来说,在访问未定义的变量或定义了但没
有初始化的变量时,获得的值都是  undefined 。
于是我们可以认为,无论在函数内什么地
方定义的变量,在一进入函数时就被定义了,但直到  var 所在的那一行它才被初始化,所
以在这之前引用到的都是  undefined  值。 (事实上,JavaScript 的内部实现并不是这样,未
定义变量和值为 undefined 的变量还是有区别的。 )
函数作用域的嵌套
接下来看一个稍微复杂的例子:

var f = function() {
var scope = 'f0';
(function() {
     
  var scope = 'f1';
       (function() {
       console.log(scope); // 输出 f1
       })();

})();
};
f();
上面是一个函数作用域嵌套的例子,我们在最内层函数引用了  scope 变量,通过作用
域搜索,找到了其父作用域中定义的  scope 变量。
有一点需要注意:函数作用域的嵌套关系是定义时决定的,而不是调用时决定的,也就
是说,JavaScript 的作用域是静态作用域,又叫词法作用域,这是因为作用域的嵌套关系可
以在语法分析时确定,而不必等到运行时确定
。下面的例子说明了这一切:
var scope = 'top';
 var f1 = function() {
 console.log(scope);
 };
f1(); // 输出 top
 var f2 = function() {
   var scope = 'f2';
   f1();
};
f2(); // 输出 top
这个例子中,通过 f2  调用的 f1  在查找  scope  定义时,找到的是父作用域中定义
的 scope 变量,而不是  f2  中定义的  scope 变量。这说明了作用域的嵌套关系不是在调用
时确定的,而是在定义时确定的。


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


满足以下条件的变量属于全局作用域:

在最外层定义的变量;

全局对象的属性;

任何地方隐式定义的变量(未定义直接赋值的变量) 。


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

-----------------------------------------------------闭包--------------------------------------------------------------------------

闭包(closure)是函数式编程中的概念,出现于 20 世纪 60 年代,最早实现闭包的语言
是 Scheme,它是 LISP 的一种方言。之后闭包特性被其他语言广泛吸纳。
闭包的严格定义是“由函数(环境)及其封闭的自由变量组成的集合体。 ”这个定义对
于大家来说有些晦涩难懂,所以让我们先通过例子和不那么严格的解释来说明什么是闭包,
然后再举例说明一些闭包的经典用途。

什么是闭包?

 通俗地讲,JavaScript 中每个的函数都是一个闭包,但通常意义上嵌套的函数更能够体
现出闭包的特性,请看下面这个例子:
var generateClosure = function() {
var count = 0;
var get = function() {
    count ++;
    return count;
  };
return get;
};
var counter = generateClosure();
console.log(counter()); // 输出 1
console.log(counter()); // 输出 2
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() 的局部变量就是一个闭包
。如果还不够清晰,下面这个例子可以帮助
你理解:
var generateClosure = function() {
var count = 0;
var get = function() {
count ++;
return count;
};
return get;
};
var counter1 = generateClosure();
var counter2 = generateClosure();
console.log(counter1()); // 输出 1
console.log(counter2()); // 输出 1
console.log(counter1()); // 输出 2
console.log(counter1()); // 输出 3
console.log(counter2()); // 输出 2
上面这个例子解释了闭包是如何产生的: counter1 和  counter2 分别调用了  generate-
Closure()  函数,生成了两个闭包的实例,它们内部引用的  count  变量分别属于各自的
运行环境。我们可以理解为,在 generateClosure() 返回 get  函数时,私下将  get 可
能引用到的  generateClosure()  函数的内部变量(也就是  count  变量)也返回了,并
在内存中生成了一个副本,之后 generateClosure()  返回的函数的两个实例 counter1
和 counter2  就是相互独立的了。

闭包的用途:

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

 exports.add_user = function(user_info, callback) {
var uid = parseInt(user_info['uid']);
mongodb.open(function(err, db) {
if (err) {
callback(err);
return
}
db.collection('users',
function(err, collection) {
if (err) {
callback(err);
return
}
collection.ensureIndex("uid",
function(err) {
if (err) {
callback(err);
return
}
collection.ensureIndex("username",
function(err) {
if (err) {
callback(err);
return
}
collection.findOne({
uid: uid
},
function(err) {
if (err) {
callback(err);
return
}
if (doc) {
callback('occupied')
} else {
var user = {
uid: uid,
user: user_info,


};
collection.insert(user,
function(err) {
callback(err)
})
}
})
})
})
})
})
};

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


2. 实现私有成员

   我们知道,JavaScript 的对象没有私有属性,也就是说对象的每一个属性都是曝露给外部
的。这样可能会有安全隐患,譬如对象的使用者直接修改了某个属性,导致对象内部数据的一
致性受到破坏等。JavaScript通过约定在所有私有属性前加上下划线(例如 _myPrivateProp ) ,

表示这个属性是私有的,外部对象不应该直接读写它。但这只是个非正式的约定,假设对象
的使用者不这么做,有没有更严格的机制呢?答案是有的,通过闭包可以实现。让我们再看
看前面那个例子:
 var generateClosure = function() {
var count = 0;
var get = function() {
count++;
return count;
};
return get;
};
var counter = generateClosure();
console.log(counter()); // 输出 1
console.log(counter()); // 输出 2
console.log(counter()); // 输出 3
我们可以看到,只有调用 counter()  才能访问到闭包内的  count  变量,并按照规则
对其增加1,除此之外决无可能用其他方式找到 count  变量。受到这个简单例子的启发,
我们可以把一个对象用闭包封装起来, 只返回一个 “访问器” 的对象, 即可实现对细节隐藏。
关于实现JavaScript对象私有成员的更多信息,请参考http://javascript.crockford.com/private.html。

-------------------------------------------------------------------对象-------------------------------------------------------------

提起面向对象的程序设计语言,立刻让人想起的是 C++、Java 等这类静态强类型语言,
以及 Python、Ruby 等脚本语言,它们共有的特点是基于类的面向对象。而说到 JavaScript,
很少能让人想到它面向对象的特性,甚至有人说它不是面向对象的语言,因为它没有类。没
错,JavaScript 真的没有类,但 JavaScript 是面向对象的语言。JavaScript 只有对象,对象就
是对象,不是类的实例。
因为绝大多数面向对象语言中的对象都是基于类的, 所以经常有人混淆类的实例与对象
的概念。对象就是类的实例,这在大多数语言中都没错,但在 JavaScript 中却不适用。
JavaScript 中的对象是基于原型的,因此很多人在初学 JavaScript 对象时感到无比困惑。通过
这一节,我们将重新认识 JavaScript 中对象,充分理解基于原型的面向对象的实质。

创建和访问

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

在 JavaScript 中,你可以用以下方法创建一个简单的对象:
var foo = {};
foo.prop_1 = 'bar';
foo.prop_2 = false;
foo.prop_3 = function() {
return 'hello world';
}
console.log(foo.prop_3());
以上代码中,我们通过  var foo = {};  创建了一个对象,并将其引用赋值给 foo ,
通过  foo.prop1 来获取它的成员并赋值, 其中 {}  是对象字面量的表示方法, 也可以用  var
foo = new Object()  来显式地创建一个对象。
1. 使用关联数组访问对象成员
我们还可以用关联数组的模式来创建对象,以上代码修改为:
var foo = {};
foo['prop1'] = 'bar';
foo['prop2'] = false;
foo['prop3'] = function() {
return 'hello world';
}
在 JavaScript 中,使用句点运算符和关联数组引用是等价的,也就是说任何对象(包括
this 指针)都可以使用这两种模式。使用关联数组的好处是,在我们不知道对象的属性名
称的时候,可以用变量来作为关联数组的索引
。例如:
var some_prop = 'prop2';
foo[some_prop] = false;

2. 使用对象初始化器创建对象
上述的方法只是让你对JavaScript对象的定义有个了解, 真正在使用的时候, 我们会采用
下面这种更加紧凑明了的方法:
var foo = {
'prop1': 'bar',
prop2: 'false',
prop3: function (){
return 'hello world';
}
};
这种定义的方法称为对象的初始化器。注意,使用初始化器时,对象属性名称是否加引
号是可选的, 除非属性名称中有空格或者其他可能造成歧义的字符, 否则没有必要使用引号。


构造函数
前一小节讲述的对象创建方法都有一个弱点,就是创建对象的代码是一次性的。如果我
们想创建多个规划好的对象,有若干个固定的属性、方法,并能够初始化,就像 C++ 语言
中的对象一样,该如何做呢?别担心,JavaScript 提供了构造函数,让我们来看看应该如何
创建复杂的对象。
 function User(name, uri) {
this.name = name;
this.uri = uri;
this.display = function() {
console.log(this.name);
}
}
以上是一个简单的构造函数,接下来用  new  语句来创建对象:
var someuser = new User('byvoid', 'http://www.byvoid.com');
然后就可以通过  someuser  来访问这个对象的属性和方法了。

上下文对象

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

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

var someuser = {
name: 'byvoid',
display: function() {
console.log(this.name);
}
};
someuser.display(); // 输出 byvoid
var foo = {
bar: someuser.display,
name: 'foobar'
};
foo.bar(); // 输出 foobar

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

在 JavaScript 中,本质上,函数类型的变量是指向这个函数实体的一个引用,在引用之
间赋值不会对对象产生复制行为
。我们可以通过函数的任何一个引用调用这个函数,不同之
处仅仅在于上下文
。下面例子可以帮助我们理解:
 var someuser = {
name: 'byvoid',
func: function() {
console.log(this.name);
}
};
var foo = {
name: 'foobar'
};
someuser.func(); // 输出 byvoid
foo.func = someuser.func;
foo.func(); // 输出 foobar
name = 'global';
func = someuser.func;
func(); // 输出 global

为防止func 歧义,再复制一份,将得到同样的结果。

var someuser = {
name: 'byvoid',
func: function() {
console.log(this.name);
}
};
var foo = {
name: 'foobar'
};
someuser.func(); // 输出 byvoid
foo.func = someuser.func;
foo.func(); // 输出 foobar
name = 'global';
var func1 = someuser.func;
func1(); // 输出 global

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

1.  call 和  apply
在 JavaScript 中, call 和  apply 是两个神奇的方法,但同时也是容易令人迷惑的两个
方法,乃至许多对 JavaScript 有经验的人也不太清楚它们的用法。 call 和  apply 的功能是
以不同的对象作为上下文来调用某个函数
。简而言之,就是允许一个对象去调用另一个对象
的成员函数。乍一看似乎很不可思议,而且容易引起混乱,但其实 JavaScript 并没有严格的

所谓“成员函数”的概念,函数与对象的所属关系在调用时才展现出来。灵活使用  call 和
apply 可以节省不少时间,在后面我们可以看到, call 可以用于实现对象的继承
call 和  apply 的功能是一致的,两者细微的差别在于  call 以参数表来接受被调用函
数的参数,而  apply 以数组来接受被调用函数的参数
。 

call 和  apply 的语法分别是:
func.call(thisArg[, arg1[, arg2[, ...]]])
func.apply(thisArg[, argsArray])
其中, func 是函数的引用, thisArg 是  func 被调用时的上下文对象, arg1 、 arg2 或
argsArray 是传入  func 的参数。我们以下面一段代码为例介绍  call 的工作机制:
 var someuser = {
name: 'byvoid',
display: function(words) {
console.log(this.name + ' says ' + words);
}
};
var foo = {
name: 'foobar'
};
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
的使用方法: 

var someuser = {
name: 'byvoid',
func: function() {
console.log(this.name);
}
};
var foo = {
name: 'foobar'
};
foo.func = someuser.func;
foo.func(); // 输出 foobar
foo.func1 = someuser.func.bind(someuser);
foo.func1(); // 输出 byvoid
func = someuser.func.bind(foo);
func(); // 输出 foobar
func2 = func;
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  方法还有一个重要的功能:绑定参数表,如下例所示。
 var person = {
name: 'byvoid',
says: function(act, obj) {
console.log(this.name + ' ' + act + ' ' + obj);
}
};
person.says('loves', 'diovyb'); // 输出 byvoid loves diovyb
byvoidLoves = person.says.bind(person, 'loves');
byvoidLoves('you'); // 输出 byvoid loves you
可以看到, byvoidLoves  将  this 指针绑定到了 person ,并将第一个参数绑定到
loves ,之后在调用  byvoidLoves  的时候,只需传入第三个参数。这个特性可以用于创建
一个函数的“捷径” ,之后我们可以通过这个“捷径”调用,以便在代码多处调用时省略重
复输入相同的参数。

4. 理解  bind
尽管  bind 很优美,还是有一些令人迷惑的地方,例如下面的代码:
 var someuser = {
name: 'byvoid',
func: function() {
console.log(this.name);
}
};
var foo = {
name: 'foobar'
};
func = someuser.func.bind(foo);
func(); // 输出 foobar
func2 = func.bind(someuser);
func2(); // 输出 foobar


全局函数  func  通过 someuser.func.bind 将 this 指针绑定到了 foo ,调用 func() 输
出了 foobar 。我们试图将 func2 赋值为已绑定的 func 重新通过 bind 将 this 指针绑定到
someuser 的结果, 而调用 func2 时却发现输出值仍为 foobar , 即  this 指针还是停留在 foo
对象上,这是为什么呢?要想解释这个现象,我们必须了解 bind  方法的原理。
让我们看一个 bind 方法的简化版本(不支持绑定参数表) :
someuser.func.bind = function(self) {
return this.call(self);
};
假设上面函数是 someuser.func 的 bind  方法的实现,函数体内  this 指向的是
someuser.func ,因为函数也是对象,所以  this.call(self) 的作用就是以 self  作为
this 指针调用  someuser.func 。
//将func = someuser.func.bind(foo)展开:
func = function() {
return someuser.func.call(foo);
};
//再将func2 = func.bind(someuser)展开:
func2 = function() {
return func.call(someuser);
};
从上面展开过程我们可以看出, func2  实际上是以 someuser 作为 func 的 this 指
针调用了  func ,而  func 根本没有使用  this 指针,所以两次 bind  是没有效果的


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章
最新文章
相关文章