路白-闭包和作用域

本文详细介绍了JavaScript中的闭包和作用域概念。闭包允许访问自由变量,可用于实现柯里化、私有方法/变量和缓存。作用域分为全局、函数和块级作用域,ES6引入了let和const进行块级作用域声明,防止全局污染。作用域链则规定了变量查找的顺序。文章还包含了相关代码示例和面试题解析。
摘要由CSDN通过智能技术生成

一、闭包的概念及应用场景

定义

闭包是指那些能够访问自由变量的函数。
自由变量是指在函数中使用的,但既不是函数参数也不是函数局部变量的变量。

  1. 从理论角度:所有的函数都是闭包。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  2. 从实践角度:以下函数才算是闭包:
    • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    • 在代码中引用了自由变量

应用场景

  1. 柯里化函数
    柯里化的目的在于:避免频繁调用具有相同参数函数,同时又能够轻松的复用。
    其实就是封装一个高阶函数。
// 假设我们有一个求长方形面积的函数
function getArea(width, height) {
    return width * height
}
// 如果我们碰到的长方形的宽老是10
const area1 = getArea(10, 20)
const area2 = getArea(10, 30)
const area3 = getArea(10, 40)

// 我们可以使用闭包柯里化这个计算面积的函数
function getArea(width) {
    return height => {
        return width * height
    }
}

const getTenWidthArea = getArea(10)

console.log(getTenWidthArea)
// 之后碰到宽度为10的长方形就可以这样计算面积
const area1 = getTenWidthArea(20)

// 而且如果遇到宽度偶尔变化也可以轻松复用
const getTwentyWidthArea = getArea(20)

  1. 使用闭包实现私有方法/变量
    其实就是模块的方式, 现代化的打包最终其实就是每个模块的代码都是相互独立的。
function funOne(i){
    function funTwo(){
        console.log('数字:' + i);
    }
    return funTwo;
};
var fa = funOne(110);
var fb = funOne(111);
var fc = funOne(112);
fa();       // 输出:数字:110
fb();       // 输出:数字:111
fc();       // 输出:数字:112

  1. 匿名自执行函数
var funOne = (function(){
	var num = 0;
	return function(){
	    num++;
	    return num;
	}
})();
console.log(funOne());      // 输出:1
console.log(funOne());      // 输出:2
console.log(funOne());      // 输出:3

  1. 缓存一些结果
    比如在外部函数创建一个数组, 闭包函数内可以更改/获取这个数组的值,其实还是延长变量的生命周期,但是不通过全局变量来实现。
function funParent(){
    let memo = [];
    function funTwo(i){
        memo.push(i);
        console.log(memo.join(','))
    }
    return funTwo;
};

const fn = funParent();

fn(1);
fn(2);

总结

  • 创建私有变量
  • 延长变量的生命周期
    一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的

代码题

  1. 实现compose函数, 得到如下输出
// 实现一个compose函数, 用法如下:
function fn1(x) {
    return x + 1;
}

function fn2(x) {
    return x + 2;
}

function fn3(x) {
    return x + 3;
}

function fn4(x) {
    return x + 4;
}


const a = compose(fn1, fn2, fn3, fn4);
console.log(a(1)); // 1+4+3+2+1=11
  1. 实现一个柯里化函数
function currying() {
    
}

const add = (a, b, c) => a + b + c;
const a1 = currying(add, 1);
const a2 = a1(2);
console.log(a2(3)) // 6

二、作用域

作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。

换句话说,作用域决定了代码区块中变量和其他资源的可见性。

作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6的到来,为我们提供了块级作用域,可通过新增命令let和const来体现。

全局作用域

在代码中任何地方都能访问到的对象拥有全局作用域。

  • 最外层函数 和在最外层函数外面定义的变量拥有全局作用域
var outVariable = "我是最外层变量"; //最外层变量
function outFun() { //最外层函数
    var inVariable = "内层变量";
    function innerFun() { //内层函数
        console.log(inVariable);
    }
    innerFun();
}
console.log(outVariable); //我是最外层变量
outFun(); //内层变量
console.log(inVariable); //inVariable is not defined
innerFun(); //innerFun is not defined
  • 所有未定义直接赋值的变量自动声明为拥有全局作用域
function outFun2() {
    variable = "未定义直接赋值的变量";
    var inVariable2 = "内层变量2";
}
outFun2();
console.log(variable); //未定义直接赋值的变量
console.log(inVariable2); //inVariable2 is not defined
  • 所有window对象的属性拥有全局作用域

window.location

  • 弊端

如果我们写了很多行 JS 代码,变量定义都没有用函数包括,那么它们就全部都在全局作用域中。这样就会 污染全局命名空间, 容易引起命名冲突。

函数作用域

函数作用域,是指声明在函数内部的变量,和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部。

function doSomething(){
    var blogName="浪里行舟";
    function innerSay(){
        alert(blogName);
    }
    innerSay();
}
alert(blogName); // 报错
innerSay(); // 报错

作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行

块级作用域

块级作用域可通过新增命令let和const声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:

  • 在一个函数内部
  • 在一个代码块(由一对花括号包裹)内部

let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。

块级作用域有以下几个特点:

  • 声明变量不会提升到代码块顶部
  • 禁止重复声明
  • 变量只在当前块内有效

一道比较经典的面试题, 涉及作用域以及事件循环:

for(var i = 0; i < 10; i++) {
	setTimeout(function(){
		console.log(i)
	})
}
// 输出 10 10 10 10 10 10 10 10 10 10

for(let i = 0; i < 10; i++) {
	setTimeout(function(){
		console.log(i)
	})
}
// 输出 0 1 2 3 4 5 6 7 8 9

第一个变量i是用var声明的,在全局范围内有效,所以全局中只有一个变量i,每次循环时,setTimeOut定时器里指的是全局变量i,而循环里的十个setTimeOut是在循环结束后才执行,所以输出十个10。

第二个变量i是用let声明的,当前的i
只在本轮循环中有效,每次循环的i其实都是一个新的变量,所以setTImeOut定时器的里面的i其实不是同一变量,所以输出0123456789

作用域链

有点类似于原型链, 在原型中我们找一个属性的时候, 如果当前实例找不到, 就会去父级原型去找.

作用域链也是类似的原理, 找一个变量的时候, 如果当前作用域找不到, 那就会逐级往上去查找, 直到找到全局作用域还是没找到,就真找不到了.

Tips: 那最先在哪个作用域里寻找呢? 在执行函数的那个作用域? 还是在创建函数的作用域?

记住!! 要到创建这个函数的那个域”。 作用域中取值,这里强调的是“创建”,而不是“调用”

var a = 10
function fn() {
  var b = 20
  function bar() {
    console.log(a + b) //30
  }
  return bar
}
var x = fn(),
  b = 200
x() //bar()

Coding

  1. 看一下输出
var b = 10;
(function b(){
	b = 20;
    // 内部作用域,会先去查找是有已有变量b的声明,有就直接赋值20,确实有了呀。发现了具名函数 function b(){},拿此b做赋值;
    // IIFE的函数无法进行赋值(内部机制,类似const定义的常量),所以无效。
	console.log(b); // fn b
    console.log(window.b); // 10
})();
  • 函数表达式与函数声明不同,函数名只在该函数内部有效,并且此绑定是常量绑定。
  • 对于一个常量进行赋值,在 strict 模式下会报错,非 strict 模式下静默失败。
  • IIFE中的函数是函数表达式,而不是函数声明。
  1. 看一下输出
var a = 3;

function c() {
    alert(a);
}
(function () {
    var a = 4;
    c(); // 3
})();
  1. 看一下输出
function v() {
    var a = 6;
    function a() {

    }
    console.log(a); 
}

v(); // 6
function v() {
    var a;
    function a() {

    }
    console.log(a); 
}

v(); // fn a

js会把所有变量都集中提升到作用域顶部事先声明好,但是它赋值的时机是依赖于代码的位置,那么js解析运行到那一行之后才会进行赋值,还没有运行到的就不会事先赋值。也就是变量会事先声明,但是变量不会事先赋值。

碰到这种问题可以先想一下变量提升和函数声明提升的规则, 原则上是变量被提升到最顶部, 函数声明被提升到最顶部变量的下方.

尝试着把这两段代码在大脑中编译一下:

  • 第一段代码
function v() {
    var a;
    function a() {

    }
    a=6;
    console.log(a);
}

v(); // 6
  • 第二段代码
function v() {
    var a;
    function a() {

    }
    console.log(a);
}

v(); // fn a
  1. 看一下输出
function v() {
    console.log(a); // fn a

    var a = 1;

    console.log(a); // 1

    function a() {

    }

    console.log(a); // 1

    console.log(b); // fn b

    var b = 2;

    console.log(b); // 2

    function b() {

    }

    console.log(b); // 2
}
v();

按照刚才的思路转换一下:

function v() {
    var a;
    var b;
    function a() {}
    function b() {}

    console.log(a); // fn a
    a=1;
    console.log(a); // 1
    console.log(a); // 1

    console.log(b); // fn b
    b=2;
    console.log(b); // 2
    console.log(b); // 2
}
v();
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值