块级作用域
什么是块级作用域
ES6 中新增了块级作用域。块作用域由 { } 包括,if
语句和 for
语句里面的 { } 也属于块作用域。
为什么需要块级作用域
第一种场景:内部变量会覆盖外部变量
var time = new Date()
function fx () {
console.log(time) // undefined
if (false) {
var time = 'hello'
}
}
fx()
第二种场景:用来计数的循环变量泄漏为全局变量
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
实现块级作用域(ES6提供let变量实现块级作用域)
function fxFn () { // 这是一个块级作用域
let fx = 'fx is a great girl'
if (true) { // 这是一个块级作用域
let fx = 'fx is 18 years old'
}
console.log(fx) // fx is a great girl
}
fxFn()
// 块级作用域之间相互不影响
ES6 允许块级作用域的任意嵌套。
{
{
{
{
{
let insane = 'Hello World'
}
console.log(insane); // 报错;
}
}
}
}
上面代码使用了一个五层的块级作用域,每一层都是一个单独的作用域。第四层作用域无法读取第五层作用域的内部变量。
内层作用域可以定义外层作用域的同名变量。
{
{
{
{
let insane = 'Hello World';
{
let insane = 'Hello World'
}
}
}
}
}
块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。
// IIFE 写法;
(function () {
var tmp = 'a'
console.log(tmp)
}())
// 块级作用域写法
{ let tmp = ...; ...}
块级作用域与函数声明
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
// 情况一
if (true) {
function f() {
}
}
// 情况二
try {
function f() {
}
} catch (e) {
// ...
}
上面两种函数声明,根据 ES5 的规定都是非法的。
但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let
,在块级作用域之外不可引用。
function f() {
console.log('I am outside!');
}
(function () {
if (false) {
// 重复声明一次函数f
function f() {
console.log('I am inside!');
}
}
f();
}());
上面代码在 ES5 中运行,会得到“I am inside!”,因为在if
内声明的函数f
会被提升到函数头部,实际运行的代码如下。
// ES5 环境
function f() {
console.log('I am outside!');
}
(function () {
function f() {
console.log('I am inside!');
}
if (false) {
}
f();
}());
ES6 就完全不一样了,理论上会得到“I am outside!”。因为块级作用域内声明的函数类似于let
,对作用域之外没有影响。但是,如果你真的在 ES6 浏览器中运行一下上面的代码,是会报错的,这是为什么呢?
// 浏览器的 ES6 环境
function f() {
console.log('I am outside!');
}
(function () {
if (false) {
// 重复声明一次函数f
function f() {
console.log('I am inside!');
}
}
f();
}());
// Uncaught TypeError: f is not a function
上面的代码在 ES6 浏览器中,都会报错。
原来,如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式
- 允许在块级作用域内声明函数。
- 函数声明类似于
var
,即会提升到全局作用域或函数作用域的头部。 - 同时,函数声明还会提升到所在的块级作用域的头部。
注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let
处理。
根据这三条规则,浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var
声明的变量。上面的例子实际运行的代码如下。
// 浏览器的 ES6 环境
function f() {
console.log('I am outside!');
}
(function () {
var f = undefined;
if (false) {
function f() {
console.log('I am inside!');
}
}
f();
}());
// Uncaught TypeError: f is not a function
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
// 块级作用域内部的函数声明语句,建议不要使用
{
let a = 'secret';
function f() {
return a;
}
}
// 块级作用域内部,优先使用函数表达式
{
let a = 'secret';
let f = function () {
return a;
};
}
ES6 的块级作用域必须有大括号
如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
// 第一种写法,报错
if (true) let x = 1;
// 第二种写法,不报错
if (true) {
let x = 1;
}
上面代码中,第一种写法没有大括号,所以不存在块级作用域,而let
只能出现在当前作用域的顶层,所以报错。第二种写法有大括号,所以块级作用域成立。
函数声明也是如此,严格模式下,函数只能声明在当前作用域的顶层。
// 不报错
'use strict';
if (true) {
function f() {
}
}
// 报错
'use strict';
if (true)
function f() {}
变量提升/函数提升
什么是变量/函数提升
- 包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理,这种现象称为提升。
- 但只有声明本身会被提升,而赋值或其他运行逻辑会留在原地
javascript
并不是严格的自上而下执行的语言
变量提升详解
JavaScript
的变量提升是针对var
的,而let
和const
不存在变量提升这一特性- 通过
var
定义的变量,在定义语句之前就可以访问到 值:undefined
- 变量提升就是变量会被提升到作用域的最顶上去,也就是该变量不管是在作用域的哪个地方声明的,都会提升到作用域的最顶上去。
- 当你看到
var a = 2
; 时,可能会认为这是一个声明。但JavaScript
实际上会将其看成两个声明:var a
; 和a = 2
;。 - JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为
undefined
,
第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。 第一个代码片段会以如下形式进行处理:
var a;
a = 2;
console.log(a);
其中第一部分是编译,而第二部分是执行。
类似地,我们的第二个代码片段实际是按照以下流程处理的:
var a;
console.log(a);
a = 2;
打个比方,这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动” 到了最上面。这个过程就叫作提升。
JavaScript
只会将变量声明提升,但是不会把初始化提升
但如果变量一直都没有声明过,则会抛出ReferenceError
,比如直接输出:
console.log(b) // Uncaught ReferenceError: b is not defined
另外值得注意的是,每个作用域都会进行提升操作
var a = 100
function fn () {
console.log(a)
var a = 200
console.log(a)
}
fn()
console.log(a)
var a
console.log(a)
var a = 300
console.log(a)
这段代码将会依次输出 undefined 200 100 100 300
在fn()
函数中由于声明了var a = 200
, 所以 var a
会被提升到fn
的作用域顶端,第一输出则为undefined
下面这段代码,由于es6
之前,js
是没有块级作用域的,所以 if
中声明的a
变量会被当成全局变量处理
var a = 1
if (true) {
var a = 2
}
console.log(a) // 2
比较下面两段代码
var a = 10
function fx () {
console.log(a) // undefined
var a = 20
console.log(a) // 20
}
fx()
console.log(a) // 10
====================================
var a = 10
function fx () {
console.log(a) // 10
a = 20
console.log(a) // 20
}
fx()
console.log(a) // 20
第二段代码 fx()
中的a
没有使用var
定义,会造成fx
函数中没有变量声明,所以 fx
里面访问的变量a
,其实都是访问的全局变量a
,a = 20
又相当于给全局变量a
重新赋值20
函数声明提升
通过function
声明的函数,在之前就可以直接调用
fx() // fx is a great girl 之前之后都可调用
function fx () {
console.log('fx is a great girl')
}
fx() // fx is a great girl 之前之后都可调用
但如果是这种写法:函数表达式声明的函数
console.log(fx) // undefined
var fx = function () {
console.log('fx is a great girl')
}
==========================
fx() // 不是 ReferenceError, 而是 TypeErr
var fx = function () {
console.log('fx is a great girl')
}
这段程序中的变量标识符 fx()
被提升并分配给所在作用域(在这里是全局作用域),因此 fx()
不会导致 ReferenceError
。但是 fx
此时并没有赋值(如果它是一个函数声明而不是函数表达式,那么就会赋值)。fx()
由于对 undefined
值进行函数调用而导致非法操作, 因此抛出 TypeError
异常。
当前函数声明和变量声明使用同一个变量名称时,函数的优先级高于变量的优先级
console.log(fx) // 会输出 fx 定义的函数
function fx () {
console.log('fx is a great girl')
}
var fx = 'fx'
console.log(fx) // fx
console.log(fx()) // TypeError: fx is not a function
有多个同名函数声明的时候,是由最后面的函数声明来替代前面的
fx() // fx is a great girl
function fx () {
console.log('fx')
}
function fx () {
console.log('fx is a great girl')
}
下面列举几道题,理解了之后就很好懂
function fx () {
console.log(a) // undefined
if (false) {
var a = 1
}
console.log(a) // undefined
console.log(b) // Uncaught ReferenceError: b is not defined
}
fx()
即使if
语句的条件是false
,也一样不影响a
变量提升
function fx () {
console.log('fx is a great girl')
}
var fx
console.log(typeof fx) // function
===========================
function fx () {
console.log('fx is a great girl')
}
var fx = 'good girl'
console.log(typeof fx) // string
===========================
console.log(typeof fx) // function
var fx = 'good girl'
function fx () {
console.log('fx is a great girl')
}
console.log(typeof fx) // string
===========================
console.log(typeof fx) // function
var fx
function fx () {
console.log('fx is a great girl')
}
console.log(typeof fx) // function
if(!(fx in window)) {
var fx = 1
}
console.log(fx) // undefined
var c = 1
function c(c) {
console.log(c)
}
c(2) // c is not a function
// 同名变量与函数。函数先提升,然后又定义变量,最后是变量
========
c(2) // 2
var c = 1
function c(c) {
console.log(c)
}
console.log(c) // 1
========
var c = function(c) {
console.log(c)
}
c(2) // 2
var c = 1
========
var c = function(c) {
console.log(c)
}
var c = 1
c(2) // Uncaught TypeError: c is not a function
========
console.log(c) // undefined
c(2) // Uncaught TypeError: c is not a function
var c = function(c) {
console.log(c)
}
var c = 1
匿名函数
什么是匿名函数: 没有实际名字的函数
匿名函数的作用:
1、通过匿名函数可以实现闭包
2、模拟块级作用域,减少全局变量。执行完匿名函数,存储在内存中相对应的变量会被销毁,使用块级作用域,会大大降低命名冲突的问题,不必担心搞乱全局作用域了。
详解匿名函数:
声明一个普通函数:
function fx () {
console.log('good girl')
}
将函数的名字去掉
function () { // 此时浏览器会报错
console.log('good girl')
}
正确定义的匿名函数
(function () {
// 由于没有执行该匿名函数,所以不会执行匿名函数体内的语句。
console.log('fx')
})
对去掉名字的函数加入括号后就是一个匿名函数了:
小括号的作用:
小括号能把我们的表达式组合分块,并且每一块,也就是每一对小括号,都有一个返回值。这个返回值实际上也就是小括号中表达式的返回值。所以,当我们用一对小括号把匿名函数括起来的时候,实际上小括号返回的就是一个匿名函数的Function对象。因此,小括号对加上匿名函数就如同有名字的函数般被我们取得它的引用位置了。所以如果在这个引用变量后面再加上参数列表,就会实现普通函数的调用形式。 通俗点讲就是,加入小括号后就实现了和具名函数一样的形式。
匿名函数自执行,也称为立即执行函数表达式(IIFE)
- 方式一
// 无参数的匿名函数
(function () {
console.log('fx')
})();
// 带参数的匿名函数
(function (a, b, c) {
console.log('参数一:', a) // 参数一: 这是普通函数传参的地方
console.log('参数二:', b) // 参数二: 我是参数二
console.log('参数三:', c) // 参数三: fx
})('这是普通函数传参的地方', '我是参数二', 'fx')
- 方式二
// 推荐使用
(function () {
console.log('fx')
}())
方式三
!function (fx) {
console.log(fx)
}('fx')
方式四
let fx = function (fx) {
console.log(fx)
}('fx')
IIFE常用用法
IIFE 的另一个非常普遍的进阶用法是把它们当作函数调用并传递参数进去。
var a = 2;
(function IIFE (global) {
var a = 3
console.log(a) // 3
console.log(global.a) // 2
})(window)
console.log(a) // 2
IIFE 还有一种变化的用途是倒置代码的运行顺序,
将需要运行的函数放在第二位,
在 IIFE 执行之后当作参数传递进去
var a = 2;
(function IIFE (def) {
def(window)
})(function def (global) {
var a = 3
console.log(a) // 3
console.log(global.a) // 2
})
匿名函数应用场景
1.事件
$('#fx').onclick = function () {
console.log('给按钮添加点击事件')
}
2.对象
var obj = {
name: 'fx',
fx: function () {
return this.name + ' is' + ' good girl'
}
}
console.log(obj.fx()) // fx is good girl
3.函数表达式
var fx = function () {
return 'fx is good girl'
}
console.log(fx()) // fx is good girl
4.回调函数
setInterval(function () {
console.log('fx is good girl')
}, 1000)
5.作为函数的返回值
function fx () {
// 返回匿名函数
return function () {
return 'fx'
}
}
console.log(fx()()) // fx
匿名函数模仿块级作用域
if (true) {
var a = 12 // a为全局变量
}
console.log(a) // 12
for (var i = 0; i < 3; i++) {
// console.log(i)
}
console.log(i) // 3 for没有自己的作用域,所以当循环结束后i就成为全局变量
if () {}for () {} 等没有自己的作用域。
如果有,出了自己的作用域,
声明的变量就会立即被销毁了。
但可以通过匿名函数来模拟块级作用域:
function fn () {
(function () { // 这里是我们的块级作用域(私有作用域)
var fx = 'good girl!' // 此变量在外部并未定义
console.log(fx) // good girl!
})()
console.log(fx) // 报错Uncaught ReferenceError: fx is not defined
}
fn()
习题一
function test(a, b, c, d){
console.log(a + b + c + d);
}(1, 2, 3, 4);
// 不执行也不报错
==============
function test(){
console.log(a + b + c + d);
}();
// 报错:Uncaught SyntaxError: Unexpected token )
习题二
function fxFn (){
var arr = [];
for(var i = 0; i < 10; i ++){
arr[i] = function (){
console.log(i);
}
}
return arr;
}
var fx = fxFn();
for(var j = 0; j < 10; j++){
fx[j]();
}
详解
fxFn中由于for不是块级作用域,所以var i 变成 fxFn的局部变量,每次新的i都会覆盖原来的,最终i=10。所以会输出10个10
习题三
function fxFn(){
var arr = [];
for(var i = 0; i < 10; i ++){
(function(j){
arr[i] = function (){
console.log(j + " ");
}
}(i))
}
return arr;
}
var fx= fxFn();
for(var j = 0; j < 10; j++){
fx[j]();
}
详解:
这题使用了立即执行函数,把fxFn中的i当参数传给了,匿名函数的j,所以每次执行j的状态都会更新,所以会输出0 1 2 3 4 5 6 7 8 9
匿名函数的缺点
1. 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
2. 如果没有函数名,当函数需要引用自身时只能使用已经过期的 arguments.callee 引用, 比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑自身。
3. 匿名函数省略了对于代码可读性 / 可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。
let
特点
- let 声明的变量只在自身的块级作用域有效,存在暂时性死区
- 不能重复声明(会报错提示已经定义)
- 不会预处理,不存在变量提升
应用
- 循环遍历监听
- 使用 let 替换 var 是趋势
- 主要是解决了块级作用域的需求
- 防止出现先使用(变量),后声明(变量)
简单示例
// 只在所在的代码块生效
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
============
for (var i = 0; i < 5; i++) {
console.log(i) // 0 1 2 3 4
}
console.log(i) // 5 i成了全局变量
==============
for (let j = 0; j < 5; j++) {
console.log(j) // 0 1 2 3 4
}
// let定义的j变量只在for循环的块级作用域中生效,不存在变量提升
console.log(j) // Uncaught ReferenceError: j is not defined
var a = []
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i)
}
}
console.log(a[6]()) // 6
console.log(i) // Uncaught ReferenceError: i is not defined
===
var a = []
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i)
}
}
a[6]() // 10
a[1]() // 10
console.log(i) // 10
- 上面代码中,变量
i
是var
命令声明的,在全局范围内都有效,所以全局只有一个变量i
。每一次循环,变量i
的值都会发生改变,而循环内被赋给数组a
的函数内部的console.log(i)
,里面的i
指向的就是全局的i
。也就是说,所有数组a
的成员里面的i
,指向的都是同一个i
,导致运行时输出的是最后一轮的i
的值,也就是 10。 - 如果使用
let
,声明的变量仅在块级作用域内有效,最后输出的是 6。
for
循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc'
console.log(i)
}
上面代码正确运行,输出了 3 次abc
。这表明函数内部的变量i
与循环变量i
不在同一个作用域,有各自单独的作用域。
不存在变量提升
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
暂时性死区
- 当你在一个块里面,利用 let 声明一个变量的时候,在块的开始部分到该变量的声明语句之间,我们称之为临时性死区,你不可以在这个区域内使用该变量,直到遇到其 let 语句为止
- 只要在同一作用域内存在let命令,他所声明的变量就“绑定”在这个作用域内,不管外部有没有声明
- ES6 明确规定,如果区块中存在
let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。 - 总之,在代码块内,使用
let
命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区” - 暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
===============
var i = 5;
(function () {
console.log(i) // undefined
var i = 10
})()
===============
let j = 55;
(function () {
console.log(j) // ReferenceError: j is not defined
let j = 77
})()
===============
(function hhh() {
console.log(j) // j的临时性死区 Uncaught ReferenceError: j is not defined
let j = 77 // 从这个函数的开始部分到这里,都是新的j的临时性死区
})()
调用bar
函数之所以报错(某些实现可能不报错),是因为参数x
默认值等于另一个参数y
,而此时y
还没有声明,属于“死区”。
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
下面的代码也会报错,与var
的行为不同。
// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError: x is not defined
上面代码报错,也是因为暂时性死区。使用let
声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量x
的声明语句还没有执行完成前,就去取x
的值,导致报错”x 未定义“。
不能重复声明
let fx = 'fx is a great girl'
let fx = 'fx is 18 years old' // Uncaught SyntaxError: Identifier 'fx' has already been declared
=================
let fx3 = 'fx is a great girl'
var fx3 = 'fx is 18 years old' // Uncaught SyntaxError: Identifier 'fx3' has already been declared
==============
{
let fx3 = 'fx is a great girl'
var fx4 = 'fx is 18 years old'
}
{
let fx3 = 'fx is a good girl'
var fx4 = 'fx is 8 years old'
}
// 不会报错,各个块级作用域之间不互相影响
不能在函数内部重新声明参数。
function func(arg) {
let arg;
}
func() // 报错
function func(arg) {
{
let arg;
}
}
func() // 不报错
const
- 声明一个常量,大部分特点和let一样
- 只在声明所在的块级作用域内有效。
- 声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
- 不可重复声明
声明的时候一定要赋值,否则会报错
const b; // Uncaught SyntaxError: Missing initializer in const declaration
对于基本的类型而言的话,比如number,string,boolean等来说,确实它就是声明一个不会变的常量,只要你修改了它,就会报错
const fx = 'fx is a great girl'
fx = 'NO' // Uncaught TypeError: Assignment to constant variable.
不过,对于引用类型而言的话,它指的并不会对象的内容不变,而是对象的地址不变。也就是说,你可以修改对象的内部成员,但是你不可以修改该变量的地址。
const fx = {
age: 18,
name: 'fx'
}
fx ==> {age: 18, name: "fx"}
fx.age = 8 {age: 8, name: "fx"}
fx.weight = 90 {age: 8, name: "fx", weight: 90}
fx = {} // Uncaught TypeError: Assignment to constant variable.
// 修改了fx的指针,这是不允许的
因为对象是引用类型的,fx中保存的仅是对象的指针,这就意味着,const仅保证指针不发生改变,修改对象的属性不会改变对象的指针,所以是被允许的。也就是说const定义的引用类型只要指针不发生改变,其他的不论如何改变都是允许的。
下面是另一个例子。
const a = [];
a.push('Hello'); // 可执行
a.length = 0; // 可执行
a = ['Dave']; // 报错
上面代码中,常量a
是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a
,就会报错。
const声明的对象冻结方法
如果真的想将对象冻结,应该使用Object.freeze
方法。
const foo = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;
上面代码中,常量 foo
指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。
除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。
var constantize = (obj) => {
Object.freeze(obj)
Object.keys(obj).forEach((key, i) => {
if (typeof obj[key] === 'object') {
constantize(obj[key])
}
})
}
顶层对象的属性
在浏览器环境指的是window
对象,在 Node 指的是global
对象。ES5 之中,顶层对象的属性与全局变量是等价的。
window.a = 1;
a // 1
a = 2;
window.a // 2
上面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事。
ES6 规定,为了保持兼容性,var
命令和function
命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1
let b = 1;
window.b // undefined
上面代码中,全局变量a
由var
命令声明,所以它是顶层对象的属性;全局变量b
由let
命令声明,所以它不是顶层对象的属性,返回undefined
。
闭包
有权访问另一个函数作用域的变量的函数
简单的说,Javascript允许使用内部函数—即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。
- 闭包是嵌套的内部函数
- 闭包存在于嵌套的内部函数中
闭包的主要作用:
1.可以读取函数内部的变量
2.让这些变量的值始终保持在内存中,变量或参数不会被垃圾回收机制回收GC
产生闭包的条件:
- 函数嵌套
- 内部函数引用了外部函数的数据(变量/函数)
闭包的优点:
- 变量长期驻扎在内存中
- 避免全局变量的污染
闭包的缺点:
- 常驻内存会增大内存的使用量
- 使用不当会造成内存泄露
- 闭包会在父函数外部,改变父函数内部变量的值
闭包使用详解:
当想要的得到f1函数的局部变量时,正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数
function f1() {
var n = 999;
function f2() {
console.log(n); // 999
}
}
上面代码中,函数f2
就在函数f1
内部,这时f1
内部的所有局部变量,对f2
都是可见的。但是反过来就不行,f2
内部的局部变量,对f1
就是不可见的。这就是 JavaScript 语言特有的”链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。既然f2
可以读取f1
的局部变量,那么只要把f2
作为返回值,我们不就可以在f1
外部读取它的内部变量了吗!
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
上面代码中,函数f1
的返回值就是函数f2
,由于f2
可以读取f1
的内部变量,所以就可以在外部获得f1
的内部变量了。闭包就是函数f2
,即能够读取其他函数内部变量的函数。由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中
请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。
function fxs(fx) {
return function () {
return fx++;
};
}
var inc = fxs(5);
inc() // 5
inc() // 6
inc() // 7
上面代码中,fx
是函数fxs
的内部变量。通过闭包,fx
的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc
使得函数fxs
的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。为什么会这样呢?原因就在于inc
始终在内存中,而inc
的存在依赖于fxs
,因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。
闭包的另一个用处,是封装对象的私有属性和私有方法:
function Person(name) {
var age
function setAge(n) {
age = n
}
function getAge() {
return age
}
return {name: name, getAge: getAge, setAge: setAge}
}
var p1 = Person('fx')
p1.setAge(18)
p1.getAge() // 18
上面代码中,函数Person
的内部变量age
,通过闭包getAge
和setAge
,变成了返回对象p1
的私有变量。
注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。
函数执行完后,函数内的局部变量没有释放,占用内存时间会变长,容易造成内存泄漏(内存被占用,但是没有用上)
function fn1() {
var arr = new Array[1000]
function fn2() {
console.log(arr.length)
}
return fn2
}
var f = fn1() // 已经产生闭包f() f = null // 让内部函数成为垃圾对象-->回收闭包
习题1
var name = 'The Window'
var object = {
name: 'My Object', getNameFunc: function () {
return function () {
return this.name
}
}
}
alert(object.getNameFunc()()) //The Window
object.getNameFunc() // 执行完变成一个函数,this指向window, 关于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
习题2
function outerFun() {
var a = 0;
function innerFun() {
a++;
alert(a);
}
return innerFun;
}
var obj = outerFun();
obj(); // 结果为1
obj(); // 结果为2
var obj2 = outerFun();
obj2(); // 结果为1
obj2(); // 结果为2
习题3
function foo(x) {
var tmp = 3
function bar(y) {
console.log(x + y + (++tmp))
}
bar(10)
}
foo(2) // 16
foo(2) // 16
foo(2) // 16
foo(2) // 16 这里只是函数调用,不是闭包========================
function foo (x) {
var tmp = 3
return function (y) {
console.log(x + y + (++tmp))
}
}
var bar = foo(2)
bar(10) // 16
bar(10) // 17
bar(10) // 18
bar(10) // 19 当你return的是内部function时,就是一个闭包。一个函数访问了它的外部变量,那么它就是一个闭包
一道经典的闭包面试题
function fun(n, o) {
console.log(o);
return {
fun: function (m) {
return fun(m, n);
}
}
}
var a = fun(0); // undefined
a.fun(1); // 0
a.fun(2); // 0
a.fun(3); // 0
var b = fun(0).fun(1).fun(2).fun(3); //undefined,0,1,2
var c = fun(0).fun(1); //undefined,0
c.fun(2); // 1
c.fun(3); // 1
详解:
转换为等价代码return返回的对象的fun属性对应一个新建的函数对象,这个函数对象将形成一个闭包作用域,使其能够访问外层函数的变量n及外层函数fun,为了不将fun函数和fun属性搞混,我们将上述代码修改如下:function fun(n,o){ console.log(o); return { fun:function(m){ return fun(m,n); } }} var a = fun(0); // undefined 产生了闭包 n = 0a.fun(1); // 0 产生了闭包,但是由于没有绑定变量,闭包马上就消失了, 始终用的是a里面的闭包 a.fun(2); // 0 产生了闭包,但是由于没有绑定变量,闭包马上就消失了, 始终用的是a里面的闭包 a.fun(3); // 0 产生了闭包,但是由于没有绑定变量,闭包马上就消失了, 始终用的是a里面的闭包 var b = fun(0).fun(1).fun(2).fun(3); // undefined,0,1,2 会不断产生新的闭包等价代码var b1 = b.fun(1);var b2 = b1.fun(2);var b3 = b2.fun(3); var c = fun(0).fun(1); // undefined,0,c.fun(2); // 1 当前c的闭包是 1c.fun(3); // 1 当前c的闭包是 1虽然c.fun(2)和c.fun(3)都产生了新的闭包,但是由于没有赋给新的变量,闭包接着就消失了
栈 (stack) 是栈内存的简称。栈是自动分配相对固定大小的内存空间,并由系统自动释放。 堆(heap) 是堆内存的简称。堆是动态分配内存,内存大小不一,也不会自动释放。
基本数据类型:
- 它们都是直接按值存储在栈中的,可以直接按值访问
- 每种类型的数据占用的内存空间的大小是确定的,并由系统自动分配和自动释放。
- 这样带来的好处就是,内存可以及时得到回收
- 相对于堆来说,更加容易管理内存空间。
var a = 10
var b = ab = 20
console.log(a) // 10
console.log(b) // 20
引用类型的数据 :
- 它们是通过拷贝和new出来的,这样的数据存储于堆中。
- 内存空间的大小不确定。
- 引用类型的数据的地址指针是存储于栈中的,当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,通过地址指针找到堆中的数据。
var obj1 = new Object()
var obj2 = obj1
obj2.name = 'fx'
console.log(obj1.name) // fx
说明这两个引用数据类型指向了同一个堆内存对象。obj1赋值给obj2,实际上这个堆内存对象在栈内存的引用地址复制了一份给了obj2,但是实际上他们共同指向了同一个堆内存对象,所以修改obj2其实就是修改那个对象,所以通过obj1访问也能访问的到。
引用类型变量的复制:复制的是存储在栈中的指针,将指针复制到栈中新变量分配的空间中,而这个指针副本和原指针指向存储在堆中的同一个对象;复制操作结束后,两个变量实际上将引用同一个对象。因此,在使用时,改变其中的一个变量的值,将影响另一个变量。
var b = a;
b.sex = 'boy';
基本类型与引用类型最大的区别实际就是 传值与传址 的区别
值传递:基本类型采用的是值传递。 地址传递:引用类型则是地址传递,将存放在栈内存中的地址赋值给接收的变量。
传值与传址
var arr1 = [1, 2, 5, 8];
var arr2 = arr1;
var str1 = arr1[2];
console.log(arr2); // 1, 2, 5, 8
console.log(str1); // 5
arr2[4] = 99; // arr1,arr2两个变量实际上将引用同一个指针,修改arr2,会影响arr1
console.log(arr2); //1, 2, 5, 8, 99
console.log(arr1); //1, 2, 5, 8, 99
arr1[1] = 3 // 同理修改arr1会影响arr2
console.log(arr1); //1, 3, 5, 8, 99
console.log(arr2); //1, 3, 5, 8, 99
str1 = 6; // str1是基本类型数据,改变不影响
console.log(arr2); //1, 3, 5, 8, 99
console.log(arr1); //1, 3, 5, 8, 99
console.log(arr1[2]); // 5
上方例子得知,当我改变arr2中的数据时,arr1中数据也发生了变化,当改变str1的数据值时,arr1却没有发生改变。为什么?这就是传值与传址的区别。
因为arr1是数组,属于引用类型,所以它赋予给arr2的时候传的是栈中的地址(相当于新建了一个不同名“指针”),而不是堆内存中的对象的值。str1得到的是一个基本类型的赋值,因此,str1仅仅是从arr1堆内存中获取了一个数值,并直接保存在栈中。arr1、arr2都指向同一块堆内存,arr2修改的堆内存的时候,也就会影响到arr1,str1是直接在栈中修改,并且不能影响到arr1堆内存中的数据。
为什么基本数据类型保存在栈中,而引用数据类型保存在堆中?
- 堆比栈大,栈比堆速度快;
- 基本数据类型比较稳定,而且相对来说占用的内存小;
- 引用数据类型大小是动态的,而且是无限的,引用值的大小会改变,不能把它放在栈中,否则会降低变量查找的速度,因此放在变量栈空间的值是该对象存储在堆中的地址,地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响;
- 堆内存是无序存储,可以根据引用直接获取;
变量存储在闭包中的问题
按照常理来说栈中数据在函数执行结束后就会被销毁,那么 JavaScript
中函数闭包该如何实现,先简单来个闭包:
function count () {
let num = -1;
return function () {
num++;
return num;
}
}
let numCount = count();
numCount();// 0
numCount();// 1
按照结论,num
变量在调用 count
函数时创建,在 return
时从栈中弹出。 既然是这样的逻辑,那么调用 numCount
函数如何得出 0
呢?num
在函数 return
时已经在内存中被销毁了啊! 因此,在本例中 JavaScript
的基础类型并不保存在栈中,而应该保存在堆中,供 numCount
函数使用。
抛开栈,只在堆中存储数据
function test() {
let num = 1;
let string = 'string';
let bool = true;
let obj = {attr1: 1, attr2: 'string', attr3: true, attr4: 'other'}
return function log() {
console.log(num, string, bool, obj);
}
}
伴随着 test
的调用,为了保证变量不被销毁,在堆中先生成一个对象就叫 Scope
吧,把变量作为 Scope
的属性给存起来。堆中的数据结构大致如下所示:
由于 Scope
对象是存储在堆中,因此返回的 log
函数完全可以拥有 Scope
对象 的访问。下图是该段代码在 Chrome
中的执行效果:
例子中 JavaScript
的变量并没有存在栈中,而是在堆里,用一个特殊的对象(Scopes
)保存。
变量到底是如何在 JavaScript
中存储的
在 JavaScript
中,变量分为三种类型:
- 局部变量
- 被捕获变量
- 全局变量
局部变量
在函数中声明,且在函数返回后不会被其他作用域所使用的对象。下面代码中的fx*
都是局部变量。
function fxFn() {
let fx1 = 1
var fx2 = 'str'
const fx3 = true
let fx4 = {name: 'fx'}
return
}
被捕获变量
被捕获变量就是局部变量的反面:在函数中声明,但在函数返回后仍有未执行作用域(函数或是类)使用到该变量,那么该变量就是被捕获变量。下面代码中的 fx*
都是被捕获变量。
function fxFn() {
let fx1 = 1
var fx2 = 'str'
const fx3 = true
let fx4 = {name: 'fx'}
return function () {
console.log(fx1, fx2, fx3, fx4)
}
}
let fx = fxFn()
console.dir(fx)
function fxFn() {
let fx1 = 1
var fx2 = 'str'
const fx3 = true
let fx4 = {name: 'fx'}
return class {
constructor() {
console.log(fx1, fx2, fx3, fx4)
}
}
}
let fx = fxFn()
console.dir(fx)
复制代码到 Chrome
即可查看输出对象下的 [[Scopes]]
下有对应的 Scope
。
全局变量
全局变量就是 global
,在 浏览器上为 window
在 node
里为 global
。全局变量会被默认添加到函数作用域链的最低端,也就是上述函数中 [[Scopes]]
中的最后一个。
全局变量需要特别注意一点:var
和 let/const
的区别。
var:全局的 var
变量其实仅仅是为 global
对象添加了一条属性。
var name = 'fx'; // 与下述代码一致
windows.name = 'fx';
let / const:全局的 let/const
变量不会修改 windows
对象,而是将变量的声明放在了一个特殊的对象下(与 Scope
类似)。
var pwd = 123
变量赋值
其实不论变量是存在栈内,还是存在堆里(反正都是在内存里),其结构和存值方式是差不多的,都有如下的结构:
赋值为常量
何为常量?常量就是一声明就可以确定的值,比如 1
、"string"
、true
、{a: 1}
,都是常量
假设现在有如下代码:
let foo = 1
JavaScript
声明了一个变量 foo
,且让它的值为 1
,内存中就会发生如下变化
如果现在又声明了一个 bar
变量:
let bar = 2
那么内存中就会变成这样:
对于对象类型
let obj = {
foo: 1, bar: 2
}
内存模型如下:
通过该图,我们就可以知道,其实 obj
指向的内存地址保存的也是一个地址值,那好,如果我们让 obj.foo = 'foo'
其实修改的是 0x1021
所在的内存区域,但 obj
指向的内存地址不会发生改变,因此,对象是常量!
赋值为变量
何为变量?在上述过程中的 foo
、bar
、obj
,都是变量,变量代表一种引用关系,其本身的值并不确定。
那么如果我将一个变量的值赋值给另一变量,会发生什么?
let x = foo
如上图所示,仅仅是将 x
引用到与 foo
一样的地址值而已,并不会使用新的内存空间。
OK
赋值到此为止,接下来是修改。
变量修改
与变量赋值一样,变量的修改也需要根据 =
号右边变量的类型分为两种方式:
修改为常量
foo = ‘foo’
如上图所示,内存中保存了 'foo'
并将 foo
的引用地址修改为 0x0204
。
修改为变量
foo = bar
如上图所示,仅仅是将 foo
引用的地址修改了而已。
const 的工作机制
const
为 ES6
新出的变量声明的一种方式,被 const
修饰的变量不能改变。
其实对应到 JavaScript
的变量储存图中,就是变量所指向的内存地址不能发生变化。也就是那个箭头不能有改变。
比如说以下代码:
const foo = 'foo';
foo = 'bar'; // Error
如上图的关系图所示,foo
不能引用到别的地址值。那好现在是否能解决你对下面代码的困惑:
const obj = {
foo: 1,
bar: 2
};
obj.foo = 2;
其 obj
所引用的地址并没有发生变化,发生变的部分为另一区域。如下图所示
对象的修改
OK
进入一个面试时极度容易问到的问题:
let obj1 = {foo: 'foo', bar: 'bar'}
let obj2 = obj1;
let obj3 = {foo: 'foo', bar: 'bar'}
console.log(obj1 === obj2);
console.log(obj1 === obj3);
obj2.foo = 'foofoo';
console.log(obj1.foo === 'foofoo');
请依次说出 console
的结果。
我们不讨论结果,先看看内存中的结构。所以结果为 true false true
今天的文章闭包/堆栈/基本类型/引用类型/分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/19320.html