Skip to content

Latest commit

 

History

History
222 lines (190 loc) · 12.6 KB

聊聊function与作用域链.md

File metadata and controls

222 lines (190 loc) · 12.6 KB

2020.4.12

聊聊function与作用域链

前言:function在js中扮演了举足轻重的角色,是一等公民。function和new在一起会有实例、原型、原型链等一系列问题。 function中访问变量会有作用域、作用域链的问题,以及闭包等概念。

js中数据类型:基本类型和引用类型 引用类型的值是保存在内存中的对象。与其他语言不同,JavaScript不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。在操作对象时,实际上是在操作对象的引用而 不是实际的对象。

基本类型和引用类型的不同点:

  • 存储方式不同。基本数据类型存储在栈内存中,引用类型存储在堆内存中,变量存的是指向堆内存的地址
  • 复制时的不同。基本类型的值会创建一个值的额副本;引用类型,复制的其实是指针,因此两个变量最终都指向同一个对象 传递参数 传递参数,ECMAScript中所有函数的参数都是按指传递的。也就是说,把函数外部的值复制给函数内部的参数,就和把指从一个变量复制到另一个变量一样。基本类型值的传递如同基本类型变量 的复制一样,而引用类型类型值的传递,则如同引用类型变量的复制一样。 #####检测类型 typeof 检测出基本类型和”object”类型,具体哪种对象类型检测不出
var s = "Nicholas";  //"string"
var b = true;        //"boolean"
var i = 22;          //"number"
var u;               // "undefined"
var n = null;          //"object"
var o = new Object();  //"object"

instanceof 可以检测出对象是哪种类型的

var result = variable instanceof constructor

只要在该对象的原型链上有该构造函数,结果就是true。而Object是所有对象的原型链上最顶层的构造函数,所以在检测一个引用类型值和Object构造函数时,instanceof操作符一定返回true 。如果使用instanceof操作符检测基本类型值时,始终返回false,因为基本类型不是对象。

所以,确定一个值是哪种基本类型可以使用 typeof 操作符,而确定一个值是哪种引用类型可以使用instanceof操作符。

执行环境及作用域 所有变量(包括基本类型和引用类型)都存在于一个执行环境(也成为作用域)当中,这个执行环境决定了变量的生命周期,以及哪一部分代码可以访问其中的变量。

执行环境有全局执行环境(也称为全局环境)和函数执行环境之分; 每次进入一个新执行环境,都会创建一个用于搜索变量和函数的作用域链; 函数的局部环境不仅有权访问函数作用域中的变量,而且有权访问其包含(父)环境,乃至全局环境; 全局环境只能访问在全局环境中定义的变量和函数,而不能直接访问局部环境中的任何数据; 变量的执行环境有助于确定应该何时释放内存。 代码:

var color = "blue"; 
function changeColor(){ 
 var anotherColor = "red"; 
 function swapColors(){ 
 var tempColor = anotherColor; 
 anotherColor = color; 
 color = tempColor; 
 // 这里可以访问 color、anotherColor 和 tempColor 
 } 
 // 这里可以访问 color 和 anotherColor,但不能访问 tempColor 
 swapColors(); 
} 
// 这里只能访问 color 
changeColor();

以上代码共涉及 3 个执行环境:全局环境、changeColor()的局部环境和 swapColors()的局部 环境。全局环境中有一个变量 color 和一个函数 changeColor()。changeColor()的局部环境中有 一个名为 anotherColor 的变量和一个名为 swapColors()的函数,但它也可以访问全局环境中的变 量 color。swapColors()的局部环境中有一个变量 tempColor,该变量只能在这个环境中访问到。 无论全局环境还是 changeColor()的局部环境都无权访问 tempColor。然而,在 swapColors()内部 则可以访问其他两个环境中的所有变量,因为那两个环境是它的父执行环境。

JavaScript 是一门具有自动垃圾收集机制的编程语言,开发人员不必关心内存分配和回收问题。

离开作用域的值将被自动标记为可以回收,因此将在垃圾收集期间被删除。 “标记清除”是目前主流的垃圾收集算法,这种算法的思想是给当前不使用的值加上标记,然后再回收其内存。 另一种垃圾收集算法是“引用计数”,这种算法的思想是跟踪记录所有值被引用的次数。JavaScript引擎目前都不再使用这种算法;但在 IE 中访问非原生 JavaScript 对象(如 DOM 元素)时,这种算法仍然可能会导致问题。 当代码中存在循环引用现象时,“引用计数”算法就会导致问题。 解除变量的引用不仅有助于消除循环引用现象,而且对垃圾收集也有好处。为了确保有效地回收内存,应该及时解除不再使用的全局对象、全局对象属性以及循环引用变量的引用。 闭包为何有用? 闭包是指有权访问另一个函数作用域中的变量的函数。

上面的作用域链对理解闭包至关重要,在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。作用域链中包含两个变量对象:本地活动对象和全局变量对象。显然, 作用域链本质上是一个指向变量对象的指针列表,它只是引用但不实际包含变量对象。

一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是闭包的情况由有所不同。 闭包是有权访问包含函数的作用域的,闭包保存的是包含函数的变量对象,所以它的包含函数作用域不会被销毁掉。

关于this对象 每个函数在被调用时都会自动取得两个特殊变量:this 和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止, 因此永远不可能直接访问外部函数中的这两个变量。不过,把外部作用域中的 this 对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了

var name = "The Window"; 
var object = { 
 name : "My Object", 
 getNameFunc : function(){ 

 var that = this; 
 return function(){ 
 return that.name; 
 }; 
 } 
}; 
alert(object.getNameFunc()()); //"My Object"

在定义匿名函数之前,我们把 this 对象赋值给了一个名叫 that 的变量。而在定义了闭包之后,闭包也可以访问这个变量,因为它是我们 在包含函数中特意声名的一个变量。即使在函数返回之后,that 也仍然引用着 object,所以调用 object.getNameFunc()()就返回了”My Object”。

this 和 arguments 也存在同样的问题。如果想访问作用域中的 arguments 对 象,必须将对该对象的引用保存到另一个闭包能够访问的变量中。

闭包有可能产生的问题:内存泄漏

由于 IE9 之前的版本对 JScript 对象和 COM 对象使用不同的垃圾收集例程,因此闭包在 IE 的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个 HTML 元素,那么就意味着该元素将无法被销毁。来看下面的例子。

Javascript function assignHandler(){ var element = document.getElementById("someElement"); element.onclick = function(){ alert(element.id); }; } 以上代码创建了一个作为 element 元素事件处理程序的闭包,而这个闭包则又创建了一个循环引 用(事件将在第 13 章讨论)。由于匿名函数保存了一个对 assignHandler()的活动对象的引用,因此 就会导致无法减少 element 的引用数。只要匿名函数存在,element 的引用数至少也是 1,因此它所 占用的内存就永远不会被回收。

不过,这个问题可以通过稍微改写一下代码来解决,如下所示。

function assignHandler(){ 
 var element = document.getElementById("someElement"); 
 var id = element.id; 
 element.onclick = function(){ 
 alert(id); 
 }; 
 element = null; 
}

在上面的代码中,通过把 element.id 的一个副本保存在一个变量中,并且在闭包中引用该变量消 除了循环引用。但仅仅做到这一步,还是不能解决内存泄漏的问题。必须要记住:闭包会引用包含函数 的整个活动对象,而其中包含着 element。即使闭包不直接引用 element,包含函数的活动对象中也 仍然会保存一个引用。因此,有必要把 element 变量设置为 null。这样就能够解除对 DOM 对象的引 用,顺利地减少其引用数,确保正常回收其占用的内存。

1.使用闭包可以在JavaScript中模仿块级作用域(JavaScript本身没有块级作用域的概念)。 创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用; 结果就是函数内部的所有变量都会被立即销毁——除非将某些变量赋值给了包含作用域(即外部作用域)中的变量。 像C++是由块级作用域的就是在{}中声明的变量的生命周期只在这对花括号中,在花括号外是访问不到的。而js并不是这样。

function outputNumbers(count){ 
 for (var i=0; i < count; i++){ 
 alert(i); 
 } 
 alert(i); //计数
}

这个函数中定义了一个 for 循环,而变量 i 的初始值被设置为 0。在 Java、C++等语言中,变量 i 只会在 for 循环的语句块中有定义,循环一旦结束,变量 i 就会被销毁。可是在 JavaScrip 中,变量 i 是定义在 ouputNumbers()的活动对象中的,因此从它有定义开始,就可以在函数内部随处访问它。即 使像下面这样错误地重新声明同一个变量,也不会改变它的值。

可以借助匿名函数实现块级作用域,函数表达式后面跟圆括号,代表立即执行,形成私有作用域,执行完之后,里面的变量也会被回收调。

(function(){ 
 //这里是块级作用域
})();
Javascript
function outputNumbers(count){ 
 (function () { 
 for (var i=0; i < count; i++){ 
 alert(i); 
 } 
 })(); 
 alert(i); //导致一个错误!
}

这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。 一般来说,我们都应该尽量少向全局作用域中添加变量和函数。在一个由很多开发人员共同参与的大型 应用程序中,过多的全局变量和函数很容易导致命名冲突。而通过创建私有作用域,每个开发人员既可 以使用自己的变量,又不必担心搞乱全局作用域。

2.闭包还可以用于对象中创建私有变量 即使JavaScript中还没有正式的私有对象属性的概念,但可以使用闭包来实现公有方法,而通过公有方法可以访问在包含作用域中的定义的变量。 有权访问私有变量的公有方法叫做特权方法。 可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。 严格来讲,JavaScript 中没有私有成员的概念;所有对象属性都是公有的。不过,倒是有一个私有 变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。 私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。

function Person(name){ 
 this.getName = function(){ 
 return name; 
 }; 
 this.setName = function (value) { 
 name = value; 
 }; 
} 
var person = new Person("Nicholas"); 
alert(person.getName()); //"Nicholas" 
person.setName("Greg"); 
alert(person.getName()); //"Greg"

在构造函数中定义特权方法也有一个缺点,那就是你必须使用构造函数模式来达到这个目的。构造函数模式的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方 法就可以避免这个问题。

静态私有变量。主要使用原型模式,公有方法定义在原型上,使用一个未经声明的变量(变成全局变量),能够在私有作用域之外被访问到。 模块模式。模块模式通过为单例(一个实例对象)添加私有变量和特权方法能够使其得到增强。 增强的模块模式。在模块模式的基础上,在返回对象之前加入对其增强的代码。 JavaScript中的函数表达式和闭包都是极其有用的特性,利用它们可以实现很多功能。不过因为创建闭包必须维护额外的作用域,所以过度使用它们可能会占用大量内存。