js作用域,闭包的简单介绍(含函数柯里化)

作用域

JavaScript 中大部分情况下,只有两种作用域类型

  • 全局作用域: 全局作用域为程序的最外层作用域,一直存在
  • 函数作用域: 函数作用域只有函数被定义时才会创建,包含在父级函数作用域/全局作用域内

由于作用域的限制,每段独立的执行代码块只能访问自己作用域和外层作用域中的变量,无法访问到内层作用域的变量。

/* 全局作用域开始 */
var a = 1;

function func () { /* func 函数作用域开始 */
  var a = 2;
  console.log(a);
}                  /* func 函数作用域结束 */

func(); // => 2

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

/* 全局作用域结束 */

作用域链

当可执行代码内部访问变量时,会先查找本地作用域,如果找到目标变量即返回,否则会去父级作用域继续查找…一直找到全局作用域。我们把这种作用域的嵌套机制,称为 作用域链。

function foo(a) {
  var b = a * 2;

  function bar(c) {
    console.log( a, b, c );
  }

  bar(b * 3);
}

foo(2); // 2 4 12

词法作用域(静态作用域)

**词法作用域**是JavaScript中使用的作用域类型

词法作用域,就意味着函数被定义的时候,它的作用域就已经确定了,和拿到哪里执行没有关系,因此词法作用域也被称为 “静态作用域”。

var value = 1;

function foo() {
  console.log(value);
}

function bar() {
  var value = 2;
  foo();
}

bar(); // 打印1

块级作用域

if (true) {
    var a = 1
}

console.log(a) // 打印1

ES6 标准提出了使用 letconst 代替 var 关键字,来“创建块级作用域”。也就是说,上述代码改成如下方式,块级作用域是有效的:

if (true) {
    let a = 1
}
console.log(a) // ReferenceError

创建作用域

创建/改变作用域的手段

  1. 定义函数,创建函数作用
function foo () {
  // 创建了一个 foo 的函数作用域
}
  1. 使用 letconst 创建块级作用域(推荐):
for (let i = 0; i < 5; i++) {
  console.log(i);
}

console.log(i); // ReferenceError

闭包

能够访问其他函数内部变量的函数,被称为 闭包

上面这个定义比较难理解,简单来说,闭包就是函数内部定义的函数,被返回了出去并在外部调用。我们可以用代码来表述一下:

function foo() {
  var a = 2;

  function bar() {
    console.log( a );
  }

  return bar;
}

var baz = foo(); // 2

baz(); // 这就形成了一个闭包

我们可以简单剖析一下上面代码的运行流程:

  1. 编译阶段,变量和函数被声明,作用域即被确定。
  2. 运行函数 foo(),此时会创建一个 foo 函数的执行上下文,执行上下文内部存储了 foo 中声明的所有变量函数信息。
  3. 函数 foo 运行完毕,将内部函数 bar 的引用赋值给外部的变量 baz ,此时 baz 指针指向的还是 bar ,因此哪怕它位于 foo 作用域之外,它还是能够获取到 foo 的内部变量。
  4. baz 在外部被执行,baz 的内部可执行代码 console.log 向作用域请求获取 a 变量,本地作用域没有找到,继续请求父级作用域,找到了 foo 中的 a 变量,返回给 console.log,打印出 2

闭包的执行看起来像是开发者使用的一个小小的 “作弊手段” ——绕过了作用域的监管机制,从外部也能获取到内部作用域的信息。闭包的这一特性极大地丰富了开发人员的编码方式,也提供了很多有效的运用场景。

闭包的应用场景

单例模式

单例模式是一种常见的设计模式,它保证了一个类只有一个实例。实现方法一般是先判断实例是否存在,如果存在就直接返回,否则就创建了再返回。单例模式的好处就是避免了重复实例化带来的内存开销:

// 单例模式
function Singleton(){
  this.data = 'singleton';
}

Singleton.getInstance = (function () {
  var instance;
    
  return function(){
    if (instance) {
      return instance;
    } else {
      instance = new Singleton();
      return instance;
    }
  }
})();

var sa = Singleton.getInstance();
var sb = Singleton.getInstance();
console.log(sa === sb); // true
console.log(sa.data); // 'singleton'

模拟私有属性

javascript 没有 java 中那种 public private 的访问权限控制,对象中的所用方法和属性均可以访问,这就造成了安全隐患,内部的属性任何开发者都可以随意修改。虽然语言层面不支持私有属性的创建,但是我们可以用闭包的手段来模拟出私有属性:

// 模拟私有属性
function getGeneratorFunc () {
  var _name = 'John';
  var _age = 22;
    
  return function () {
    return {
      getName: function () {return _name;},
      getAge: function() {return _age;}
    };
  };
}

var obj = getGeneratorFunc()();
obj.getName(); // John
obj.getAge(); // 22
obj._age; // undefined

柯里化

柯里化的优势之一就是 参数的复用,它可以在传入参数的基础上生成另一个全新的函数,来看下面这个类型判断函数:

实现一个add函数,完成函数柯里化
add(1)(2)(3) = 6
add(1,2,3)(4) = 10
add(1)(2)(3)(4)(5) = 15

function add () {
	var args = Array.prototype.slice.call(arguments);

    var fn = function () {
        // 把参数都放在一个相当于全局变量的 args 里面 
        args.push(...arguments)
        return fn;
    }

    fn.valueOf = function () {
        return args.reduce(function(a, b) {
            return a + b;
        })
    }

    return fn;
}
console.log(add(1,2)) // 3
console.log(add(1)(2)) // 3
console.log(add(1)(2)(3)) // 6
console.log(add(1,2,3)(4)) // 10

闭包的问题

极容易导致内存泄露

function foo() {
  var a = 2;

  function bar() {
    console.log( a );
  }

  return bar;
}

var baz = foo();

baz(); // 这就形成了一个闭包

我们知道,javascript 内部的垃圾回收机制用的是引用计数收集:即当内存中的一个变量被引用一次,计数就加一。垃圾回收机制会以固定的时间轮询这些变量,将计数为 0 的变量标记为失效变量并将之清除从而释放内存。

上述代码中,理论上来说, foo 函数作用域隔绝了外部环境,所有变量引用都在函数内部完成,foo 运行完成以后,内部的变量就应该被销毁,内存被回收。然而闭包导致了全局作用域始终存在一个 baz 的变量在引用着 foo 内部的 bar 函数,这就意味着 foo 内部定义的 bar 函数引用数始终为 1,垃圾运行机制就无法把它销毁。更糟糕的是,bar 有可能还要使用到父作用域 foo 中的变量信息,那它们自然也不能被销毁… JS 引擎无法判断你什么时候还会调用闭包函数,只能一直让这些数据占用着内存。

这种由于闭包使用过度而导致的内存占用无法释放的情况,我们称之为:内存泄露。

内存泄露

内存泄露 是指当一块内存不再被应用程序使用的时候,由于某种原因,这块内存没有返还给操作系统或者内存池的现象。内存泄漏可能会导致应用程序卡顿或者崩溃。

  1. 意外的全局变量

    function foo(){ 
        bar=2 
        console.log('bar没有被声明!')
    }
    

    bar 没被声明,会变成一个全局变量,在页面关闭之前不会被释放.使用严格模式可以避免.

  2. 被遗忘的计时器或回调函数

    如果没有清除定时器,那么 someResource 就不会被释放,如果刚好它又占用了较大内存,就会引发性能问题. 但是 setTimeout ,它计时结束后它的回调里面引用的对象占用的内存是可以被回收的. 当然有些场景 setTimeout 的计时可能很长, 这样的情况下也是需要纳入考虑的.

  3. 脱离 DOM 的引用
    很多时候,为了方便存取,经常会将 DOM 结点暂时存储到数据结构中.但是在不需要该DOM节点时,忘记解除对它的引用,则会造成内存泄露.

const refA = document.getElementById('refA');
document.body.removeChild(refA); // dom删除了
console.log(refA, 'refA'); // 但是还存在引用能console出整个div 没有被回收
refA = null;
console.log(refA, 'refA'); // 解除引用
  1. 闭包

    相互循环引用.这是经常容易犯的错误,并且有时也不容易发现.

function foo() { 
    var a = {}; 
    function bar() { 
        console.log(a); 
    }; 
    a.fn = bar; 
    return bar; 
};

避免

避免策略:

  1. 使用严格模式

  2. 减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收(即赋值为null);

  3. 注意程序逻辑,避免“死循环”之类的 ;

  4. 避免创建过多的对象 原则:不用了的东西要记得及时归还。

  5. 减少层级过多的引用

总结、摘抄自
作者:不写bug的米公子
链接:https://juejin.cn/post/6844904165672484871
来源:稀土掘金

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值