this、闭包与作用域
this指针详解
函数的this关键字在JavaScript中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别。在绝大多数情况下,函数的调用方式决定了this的值(运行时绑定)。
- this关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象。
- 在函数执行过程中,this一旦被确定了,就不可以再更改。
JavaScript中this不是固定不变的,它会随着执行环境的改变而改变。
- 全局上下文:在全局作用域(也就是不在任何函数内部)中,this指向全局对象。在浏览器环境中,这个全局对象是window。
- 函数被直接调用:函数被直接调用(即不是作为对象的方法或不是通过new关键字)时,this指向全局对象。
- 对象的方法:函数作为对象的方法被调用时,this指向调用该方法的对象。
- 构造函数:函数通过new关键字被调用时(作为一个构造函数),this指向一个新创建的对象实例。
- 箭头函数:箭头函数不绑定自己的this,它继承自包围它的函数或全局作用域的this。箭头函数中,this指向不会改变,用apply等等都不行。
- 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
- 类似 call() 和 apply() 方法可以将 this 引用到任何对象。
作用域详解
作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合。换句话说,作用域决定了代码区块中变量和其他资源的可见性。一般将作用域分成:全局作用域、函数作用域、块级作用域。
可以理解为是一个对象包含了当前执行环境的信息。当查找变量的时候,会先从当前作用域对象中查找,如果没有找到,就会去父级查找,一直找到全局对象window,这样有多个执行上下文的变量对象构成的链条就叫做作用域链。作用域链的变量只能向上访问,变量访问到window对象即被终止。
在js中,包含三种作用域:全局作用域/函数作用域/块级作用域。
全局作用域
任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问。
函数作用域
函数作用域也叫局部作用域,如果一个变量是在函数内部声明的,它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问。
块级作用域
ES6引入了let和const关键字,和var关键字不同,在大括号中使用let和const声明的变量存在于块级作用域中。在大括号之外不能访问这些变量。
默认绑定、显式绑定、隐式绑定
根据不同使用场合,this有不同的值,主要分为下面几种情况:默认绑定、显式绑定、隐式绑定、new绑定。
默认绑定
- 如果在全局作用域(或称为函数外部)调用函数,this指向全局对象。在浏览器中是window对象(在非严格模式下),在严格模式下是undefined。
- 在严格模式下(代码文件顶部使用'use strict';),this会是undefined,不能将全局对象用于默认绑定,this会绑定到undefined。只有函数运行在非严格模式下,默认绑定才能绑定到全局对象。
- 如果函数被当作普通函数调用(即不是作为一个对象的方法或不是通过new关键字),this也指向全局对象。
显式绑定
- 使用call、apply或bind方法可以直接设置函数执行时this的值。
- call和apply会立即执行函数,并接受一个对象作为this的值,同时还可以传递参数列表。
- bind会创建一个新的函数,当这个新函数被调用时,this的值会被设置为bind的第一个参数,并且它也可以接受额外的参数传递给原函数。
隐式绑定
- 当函数作为某个对象的方法被调用时,this会被隐式地绑定到那个对象上。这时this就指这个上级对象,就算一个函数是被最外层的对象所调用,this指向的也只是它上一级的对象,this永远指向的是最后调用它的对象。
- 如果函数不是通过对象来调用,即使它在对象内部定义,this也不会绑定到那个对象上。如果函数是在一个对象的方法内部被调用,并且这个函数是作为普通函数调用(而不是作为对象的方法调用),那么this可能并不会绑定到你所期望的对象上。为了避免这种情况,可以使用call或apply来显式地设置this的值,或者使用箭头函数来自动绑定this。
new 绑定
- 当使用new关键字来调用函数时,会创建一个新的空对象,并将this绑定到这个新创建的对象上。
- 如果函数没有显式地返回一个非原始值(如对象或函数),new表达式的结果就是这个新创建的对象。
- 注意,null虽然也是对象,但是return null的时候,new仍然指向实例对象
闭包的概念
一个函数和对其周围状态(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来,作为函数内部与外部连接起来的一座桥梁。
function init() {var name = "Mozilla"; // name 是一个被 init 创建的局部变量function displayName() { // displayName() 是内部函数,一个闭包alert(name); // 使用了父函数中声明的变量}displayName();
}
init(); //displayName()没有自己的局部变量,由于闭包的特性,它可以访问到外部函数的变量
闭包就是有权访问另一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,并将函数返回。
存储空间、执行上下文
存储空间
在JavaScript中,存储空间通常指内存中的区域,用于存储变量、函数、对象等。JavaScript引擎(如V8、SpiderMonkey等)管理着这部分内存,并且会自动进行垃圾回收,以释放不再使用的内存空间。
存储空间可以大致分为以下几类:
堆内存:用于存储对象实例和闭包等。堆内存是由JavaScript引擎自动管理的,当不再需要某个对象时,垃圾回收器会将其释放。
栈内存:用于存储基本数据类型(如数字、字符串、布尔值)、函数调用的参数和局部变量等。栈内存是自动分配的,并且遵循后进先出(LIFO)的原则。
代码段:存储JavaScript代码本身。代码段是只读的,防止程序意外地修改了它的指令。
执行上下文
执行上下文是JavaScript引擎在执行代码时创建的一个环境,它定义了变量和函数的可访问性。每当JavaScript引擎开始执行一段代码(全局代码、函数代码或eval代码)时,它都会创建一个新的执行上下文,并将其推入执行上下文栈。
执行上下文的类型分为三种:
- 全局执行上下文:只有一个,浏览器中的全局对象就是window对象,this指向这个全局对象。当脚本开始执行时创建,它包含了全局作用域中定义的变量和函数。全局执行上下文始终存在,并且在整个脚本执行期间都保持活动状态。
- 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文。每当一个函数被调用时,JavaScript引擎都会为其创建一个新的函数执行上下文。函数执行上下文包含了函数内部的局部变量、参数、以及函数的内部作用域链。
- Eval函数执行上下文:指的是运行在eval函数中的代码,很少用而且不建议使用。当使用eval()函数执行代码时,也会创建一个新的执行上下文。不过,由于eval()函数的安全风险和性能问题,现代JavaScript开发中很少使用它。
只有全局上下文(的变量)能被其他任何上下文访问。可以有任意多个函数上下文,每次调用函数创建一个新的上下文,会创建一个私有作用域,函数内部声明的任何变量都不能在当前函数作用域外部直接访问。
执行上下文栈是JavaScript引擎用于管理执行上下文的数据结构。当一个新的执行上下文被创建时,它会被推入执行上下文栈的顶部。当当前执行上下文执行完毕后(例如,函数执行完成或遇到return语句),它会被从执行上下文栈中弹出,控制权返回到前一个执行上下文。
闭包的使用场景
任何闭包的使用场景都离不开这两点:
- 创建私有变量和方法
- 延长变量的生命周期
一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的。
闭包的优点是可以避免全局变量的污染,缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。
function makeSizer(size) { //例子:在页面上添加一些可以调整字号的按钮return function() {document.body.style.fontSize = size + 'px';};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
柯里化函数
目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用。 指的是将一个多参数的函数拆分成一系列函数,每个拆分后的函数都只接受一个参数。将多参数的函数转换成单参数的形式。
使用闭包模拟私有方法
在JavaScript中,没有支持声明私有变量,但我们可以使用闭包来模拟私有方法。
其他
例如计数器、延迟调用、回调等闭包的应用,其核心思想还是创建私有变量和延长变量的生命周期。