闭包及其知识体系梳理

最近重温了王福朋老师的博客《 深入理解javascript原型和闭包》(非常生动有趣,强烈推荐),打算梳理一下前端面试逃不开的知识点 —— 闭包。也算是一篇读老师博客摘录总结的学习笔记吧。

闭包这个东西,每次看了概念就忘记。究竟要怎么理解?

知识点不是孤立的。把知识点串联起来嵌入我们的知识体系中,能更好帮我们去理解。本篇文章列出了学习闭包涉及的前置基础知识,也包括这些知识点涉及到的常见的面试/笔试题型。通过对这些前置知识点梳理,最后我们能够更清楚理解闭包

接下来会根据图上面的标记顺序去学习知识点。

📖知识点1:执行上下文

浏览器在js执行前的“准备工作”中完成了哪些工作?

1)【变量、函数表达式】变量声明,默认赋值为 undefined;

console.log(f1) // undefined
var f1 = function() {} // 函数表达式

2)【函数声明】赋值;

console.log(f1) // function f1() {}
function f1() {} // 函数声明

3)【this】赋值(下一个知识点讲)

这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。

个人理解: 上下文环境指的是,作用域(函数)里面的值的流动,包括变量、this 等。这些值是多少与这个函数被调用的环境有关。】

🌰练习题:打印的结果是什么?

var a = 10;
(function () {
    console.log(a)
    a = 5
    console.log(a)
    var a = 20;
    console.log(a)
})()

解释:第一处匿名函数内部声明了变量 a,声明提前,所以是 undefined。第二处赋值了5,第三处赋值了20。

📖知识点2: this 赋值

由于 this 的取值是执行上下文环境的一部分,所以,this 到底取何值,是在函数真正被调用执行的时候确定的

1)构造函数

构造函数:所谓构造函数就是用来 new 对象的函数。其实严格来说,所有的函数都可以 new 一个对象,但是有些函数的定义是为了 new 一个对象,而有些函数则不是。另外注意,构造函数的函数名第一个字母大写(规则约定)。例如:Object、Array、Function 等。

如果函数作为构造函数用,那么其中的 this 就代表它即将 new 出来的对象。

function Point(x, y) {
  this.x = x;
  this.y = y;
  console.log(this); // Point {x: 1, y: 2}
}

var p = new Point(1, 2);
console.log(p.x) // 1

如果直接调用就不一样了。

function Point(x, y) {
  this.x = x;
  this.y = y;
  console.log(this); // Window {window: Window, self: Window, document: document, name: '', location: Location, …}
}
Point(1, 2);

另外,在构造函数的 prototype 中,this 代表着当前对象的值(不仅仅是构造函数的 prototype,即便是在整个原型链中都是如此)。

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);
console.log(p.toString()); // (1, 2)

在 Point.prototype.toString 函数中,this 指向的是 p 对象。因此可以通过 this.x 获取 p.name 的值。

顺带一提,构造函数的另一种写法,es6 中的 class。换个写法但是打印结果是一样的。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    console.log(this); // Point {x: 1, y: 2}
  }
  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
var p = new Point(1, 2);
console.log(p.toString()); // (1, 2)

但和普通构造函数不同的是,不使用 new 就没法调用。

class Point{
  constructor(x, y) {
    this.x = x;
    this.y = y;
    console.log(this);  
  }
}
Point(1, 2); 
// Uncaught TypeError: Class constructor Point cannot be invoked without 'new'

2)函数作为对象的一个属性

如果函数作为对象的一个属性 并且 作为对象的一个属性被调用时,函数中的this指向该对象。

var obj = {
    x: 10,
    fn: function() {
        console.log(this); // {x: 10, fn: ƒ}
        console.log(this.x); // 10
    }
}
obj.fn();

如果fn函数被赋值到了另一个变量中,并没有作为对象的属性被调用,那么this的值就是window。

var obj = {
    x: 10,
    fn: function() {
        console.log(this); // Window {window: Window, self: Window, document: document, name: '', location: Location, …}
        console.log(this.x); // undefined
    }
}
var fn1 = obj.fn;
fn1()

3)全局与普通函数调用

this 永远是 window 。

var x = 10;
var fn = function() {
    console.log(this); // Window {window: Window, self: Window, document: document, name: '', location: Location, …}
    console.log(this.x); // 10
}
fn();

4)函数用 call、apply、bind 调用

当一个函数被 call / apply / bind 调用时,this 的值就取传入的对象的值。

var obj = {
    x: 10
}
var fn = function(){
    console.log(this); // {x: 10}
    console.log(this.x); // 10
}
fn.apply(obj);
fn.call(obj);
fn.bind(obj)();

顺带说一下 call、apply、bind 这三者的不同:

var obj = {
    x: 10
}
var fn = function(a,b){
    console.log(this.x); // 10
    console.log(a,b); // A B
}
fn.apply(obj, ['A', 'B']); // 所有参数都放在数组中传
fn.call(obj, 'A', 'B'); // 所有参数直接放进去,用逗号隔开
fn.bind(obj, 'A', 'B')(); // 参数和call一样,方法后面多了个 () 

🌰练习题:打印的结果是什么?

var obj = {
    x: 10,
    fn: function() {
        function f() {
            console.log(this); 
            console.log(this.x); 
        }
        f();
    }
}
obj.fn();

解释:函数 f 虽然是在 obj.fn 内部定义的,但是它仍然是一个普通的函数,this 仍然指向window。

📖知识点3: 作用域

函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。作用域可以理解为函数的地盘。

作用域有上下级的关系,上下级关系的确定就看函数是在哪个作用域下创建的。fn 作用域在全局作用域中创建,fn作用域是全局作用域的下级。

📖知识点4: 自由变量

自由变量:在某一作用域中使用的变量x,却没有在该作用域中声明(即在其他作用域中声明的),对于该作用域来说,x就是一个自由变量。

var x = 10;
function fn(){
    var b = 20;
    console.log(x + b); // 这里的 x 是自由变量 , x 为 10
}

自由变量的取值要到创建这个函数的那个作用域中取值,这就是所谓的“静态作用域”。

var x = 10;
function fn(){
    // x 是自由变量, 在函数创建的时候就确定了 x 要取值的作用域
    console.log(x); // 10  
}
function show(f){
    var x = 20;
    (function(){
        f(); 
    })();
}
show(fn)

📖知识点5: 作用域链

作用域链:继续上面的知识点,如果在创建这个函数的作用域中没有找到自由变量,那就跨一步作用域找,找不到就一直跨,直到到全局作用域。

var x = 10;
function fn(){
     (function(){
       console.log(x); // 10
    })();
}
fn()

📖知识点6: 闭包

我们在知识点1中说到过,当一个函数被调用的时候创建其执行上下文环境被调用完成之后其执行上下文环境被销毁,其中的变量也会被同时销毁。但是,有些时候,函数调用完成之后,其执行上下文环境和变量不会接着被销毁。这就是我们要谈的闭包。

function fn(){
    var max = 10;
    return function bar(x){
        if(x > max){
            console.log(x)
        }
    }
}
var f1 = fn(),
    max = 100;
f1(15);

🎯代码执行到第9行:fn() 调用完成。按理说应该销毁掉 fn() 的执行上下文环境,但是这里不能这么做。因为执行 fn() 时,返回的是一个函数。函数的特别之处在于可以创建一个独立的作用域。而正巧合的是,返回的这个函数体中,还有一个自由变量 max 要引用 fn 作用域下的 fn() 上下文环境中的 max 。因此,这个 max 不能被销毁,销毁了之后 bar 函数中的 max 就找不到值了。

那么有人问了,前面知识点5说的,自由变量在当前作用域取不到就去上一层的作用域中取。这个 max 被销毁了,那为什么不从上一层作用域取?

如果问出这个问题,需要再回顾一下知识点4。因为自由变量的取值与作用域有关,而作用域是在函数创建的时候就生成的。所以这个时候的自由变量取值已经定了。

🎯代码执行到第11行:创建 bar 函数是在执行 fn() 时创建的。fn() 早就执行结束了,但是 fn() 执行上下文环境还存在与栈中,因此 bar(15) 时,max 可以查找到。如果 fn() 上下文环境销毁了,那么 max 就找不到了。

就这样,知识点都学完了,来自测一下:

🌰练习题:alert 的结果是什么?

代码1:

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        return function(){
            return this.name;
        };
  }
};
alert(object.getNameFunc()());

代码2:

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        var that = this;
        return function(){
            return that.name;
        };
  }
};
alert(object.getNameFunc()());

代码3:

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        var that = this;
        return function(){
            return that.name;
        };
  }
};
var b = object.getNameFunc;
alert(b()());

解释

代码1中,函数 getNameFunc 的内部函数只是一个普通的函数,this 指向 window。

代码2中,函数 getNameFunc 的内部函数的 that 是个自由变量,从 getNameFunc 中获取到。而getNameFunc 函数作为对象的一个属性并且作为对象的一个属性被调用,函数中的this指向该对象。

代码3中,函数 getNameFunc 被赋值到了另一个变量中,并没有作为对象的属性被调用,那么this的值就是window。

总结:

1. 闭包是什么?

函数A嵌套函数B返回函数B内部函数B存在引用外部函数A参数或变量的自由变量,这时这个函数B就是闭包。

2. 闭包的优缺点?

优点:通过闭包设计私有的方法和变量,避免全局变量的污染;

缺点:由于闭包的存在,外部函数在执行完以后,其上下执行环境与变量不会被销毁,故而会增大内存使用量,使用不当很容易造成内存泄露。

3. 闭包在实际开发中的使用?

🌰1)闭包在单体模式中的使用:通过闭包设计私有的方法和变量

myNamespace.Singleton = (function () {
    // 私有属性
    var whitespaceRegex = /\s+/;
    // 私有方法
    function stripWhitespace(str) {
        return str.replace(whitespaceRegex, '');
    }
    function stringSplit(str, delimiter) {
        return str.split(delimiter);
    }
    return {
        // 公共方法
        stringToArray: function (str, delimiter, stripWS) {
            if (stripWS) {
                str = stripWhitespace(str);
            }
            var outputArray = stringSplit(str, delimiter);
            return outputArray;
        }
    }
})();

🌰2)闭包在节流防抖中的使用:last 以自由变量的形式缓存时间信息。

// 节流为例
// fn是我们需要包装的事件回调, interval是时间间隔的阈值
function throttle(fn, interval) {
  // last为上一次触发回调的时间
  let last = 0
  
  // 将throttle处理结果当作函数返回
  return function () {
      // 保留调用时的this上下文
      let context = this
      // 保留调用时传入的参数
      let args = arguments
      // 记录本次触发回调的时间
      let now = +new Date()
      
      // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
      if (now - last >= interval) {
      // 如果时间间隔大于我们设定的时间间隔阈值,则执行回调
          last = now;
          fn.apply(context, args);
      }
    }
}

// 用throttle来包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)

document.addEventListener('scroll', better_scroll)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Swift 中的闭包是一个自包含的函数代码块,可以在代码中被传递和使用。闭包可以捕获和存储其所在上下文中任意常量和变量的引用。Swift 中的闭包类似于 C 和 Objective-C 中的 blocks、以及其他一些编程语言中的 lambdas。 闭包有以下三种形式: 1. 全局函数,有名字但不能捕获任何值。 2. 嵌套函数,有名字,也能捕获其封闭函数内的值。 3. 闭包表达式,没有名字,使用轻量级语法,可以捕获上下文中的值。 闭包表达式的基本语法如下: ``` { (parameters) -> return type in statements } ``` 其中 `parameters` 为参数列表,可以为空;`return type` 为返回类型,也可以为空;`statements` 为闭包体,包含了要执行的代码。 例如,下面的代码定义了一个接受两个整数参数并返回它们之和的闭包: ``` let sum = { (a: Int, b: Int) -> Int in return a + b } ``` 可以像函数一样调用这个闭包: ``` let result = sum(1, 2) print(result) // 输出 3 ``` 闭包可以作为函数的参数或返回值。例如,下面的代码定义了一个接受一个整型数组和一个闭包参数的函数 `apply`: ``` func apply(_ array: [Int], _ transform: (Int) -> Int) -> [Int] { var result = [Int]() for element in array { result.append(transform(element)) } return result } ``` 可以使用闭包表达式作为 `transform` 参数传递: ``` let numbers = [1, 2, 3, 4, 5] let squared = apply(numbers, { (number) -> Int in return number * number }) print(squared) // 输出 [1, 4, 9, 16, 25] ``` 闭包还支持尾随闭包语法,可以将闭包表达式作为函数的最后一个参数传递,并将其放在圆括号之外。例如,上面的代码也可以写成: ``` let squared = apply(numbers) { (number) -> Int in return number * number } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值