Skip to content

作用域和闭包

一、作用域和自由变量

作用域

某个变量合法的使用范围,分为如下三种:

  • 全局作用域

最高级对象的一个属性,也就是window

  • 函数作用域

函数定义时,就已经确定了函数的作用域链

javascript
function fn () {
    var a = 100;
    console.log(a);
}
fn()
console.log(a); // 别的地方使用就会报错,a is not defined
  • 块级作用域(ES6)
javascript
if (true) {
    // 块级作用域
    let a = 100;
    console.log(a)
}
console.log(a); // 别的地方使用就会报错,a is not defined

作用域链

自由变量:一个变量在当前作用域未定义,但被使用了 作用域链:自由变量的查找过程,就是作用域链

作用域链上找,先找自己的作用域有没有该变量, 没有向上找最近的父级作用域有没有该变量, 没有继续往上找,直到最顶层,全局,也就是由内向外查找,把自己当成一个js引擎。

如果在全局作用域都没找到,则报错:xxx is not defined

注意:

(所有)自由变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方

变量提升

使用var声明的变量为函数作用域,会在函数执行之前,进行变量提升,也就是变量的声明和定义,但是不会赋值,赋值是在执行的时候进行的。

javascript
var x = 1;
function f() {
    // 等同于先在这里做了声明 var x; 
    console.log(x); // undefined
    // 然后再赋值 x = 2
    var x = 2;
}
console.log(x); // 1
f();

通用函数的声明也是如此

javascript
function f() {
  x();
  
  function x() {
    console.log(1);
  }
}

f();

var和不用var的区别

  • var定义了一个局部变量,会有变量提升
  • 不用var则生成一个全局变量【无变量提升】

如果在声明之前去访问是不存在的Uncaught ReferenceError: xxx is not defined 但是会在执行过后生成一个全局的变量,也就是window.xxx

javascript
function f() {
    console.log(window.xxx);
    console.log(xxx); // Uncaught ReferenceError: xxx is not defined
    xxx = 1;
    console.log(xxx);
}

f()
console.log(window.y);
var y = 2
console.log(y);

二、闭包

定义

闭包是作用域使用的特殊情况, 我们把函数执行形成私有上下文,来保护和保存私有变量机制称为闭包。 1、是一个函数, 2、可以访问其他作用域中的变量;

有两种表现

  • 返回一个函数
javascript
function create () {
    let a = 200;
    return function () {
        console.log(a); // 200
    }
}
let a = 100
let fn = create();
fn();
  • 作为参数被传递
javascript
function create(fn) {
    let a = 100;
    fn();
}

let a = 200;
function fn() {
    console.log(a); // 200,不是100
}
create(fn)

可以看到,(所有)自由变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方

应用

  1. 模仿块级作用域
  2. 保护外部函数的变量 能够访问函数定义时所在的词法作用域(阻止其被回收)
  3. 封装私有化变量
  4. 创建模块
  5. 设计模式:单例模式,函数节流,函数防抖

隐藏数据

javascript
let createCache = function () {
    let data = {};
    return {
        get: function (key) {
            return data[key];
        },
        set: function (key, value) {
            data[key] = value;
            return data;
        }
    }
}

let cache = createCache();
cache.set('token', 'token');
cache.get('token');

节流、防抖

节流 throttle

稀释函数的执行频率

javascript
function throttle(fn, delay) {
    let lastTime = 0
    return function () {
        let now = Date.now()
        if (now - lastTime > delay) {
            fn.apply(this, arguments)
            lastTime = now
        }
    }
}
防抖 debounce

固定时间不可再次触发

javascript
function debounce(fn, delay) {
    let timeout = null
    return function () {
        timeout && clearTimeout(timeout)
        timeout = setTimeout(() => {
            fn.apply(this, arguments)
        }, delay)
    }
}

实现bind函数

javascript
// 模拟bind的实现
Function.prototype.bind1 = function () {
    // 将参数拆解为数组
    const args = Array.prototype.slice.call(arguments);

    // 获取this,并从数组剔除
    const _this = args.shift();

    // 原来的fn,fn.bind(...)的fn1
    const self = this;

    // 返回一个函数
    return function () {
        return self.apply(_this, args);
    }
};

function fn () {
    console.log(this);
    console.log(arguments);
    return 'this is a fn';
}

let fn1 = fn.bind1({
    name: 'fn',
}, 10, 20);

fn1();

缺点

变量会常驻内存,可能造成内存泄露
因为内存得不到及时的释放,可以人为的去置为null

js垃圾回收机制

  • 标记使用法

当这个函数结束,变量不再使用就得到了释放;

  • 引用计数法

在一定时间内不再使用;

三、this

定义

函数执行所在的环境变量,并且是可以改变的。

this是执行上下文中很重要的一个组成部分,
同一个函数调用方式不同,得到的this值也不同,如下:

javascript
function showThis(){
    console.log(this);
}

var o = {
    showThis: showThis
}

showThis(); // global
o.showThis(); // o

普通函数的this是由他调用他所使用的引用来决定的

注意

调用函数时使用的引用,决定了函数执行时刻的 this

场景

1、作为普通函数

javascript
const f1 = function() {
    console.log(this);
};

f1(); // window

2、作为对象方法被调用

javascript
function showThis() {
    console.log(this);
}

var o = {
    showThis: showThis
}

showThis(); // global
o.showThis(); // 对象本身

3、class方法中

javascript
class  Person {
    constructor () {
        this.duuty = 'person';
    }
    say () {
        console.log(this); // person实例
    }
}

let person = new Person();
person.say();

4、构造函数

javascript
let Person = function () {
    this.duty = 'person';
}
Person.prototype.say = function () {
    console.log(this); // person实例
}
let person = new Person();
person.say();

5、箭头函数

箭头函数,this的取值,是在函数定义的时候确定的

是在定义时所在的对象,也就是上级作用域中的this取值

javascript
const showThis = () => {
    console.log(this);
}

var o = {
    showThis: showThis
}

showThis(); // global
o.showThis(); // global

我们看到,改为箭头函数后,不论用什么引用来调用它,都不影响它的 this 值。

操作this的内置函数

apply和call

作用是在特定的作用域中调用函数, 设置函数体内this的对象,真正的目的其实是扩容函数的作用域。

javascript
function foo(a, b, c) {
    console.log(this);
    console.log(a, b, c);
}
foo.call({}, 1, 2, 3); // 参数依次传递
foo.apply({}, [1, 2, 3]); // 参数放在数组

这里 call 和 apply 作用是一样的,只是传参方式有区别。

bind

此外,还有 Function.prototype.bind 它可以生成一个绑定过的函数【返回一个副本】,这个函数的 this 值固定了参数:

javascript

function foo(a, b, c){
    console.log(this);
    console.log(a, b, c);
}
foo.bind({}, 1, 2, 3)();

手写bind函数

javascript

// 模拟bind的实现
Function.prototype.bind1 = function () {

    // 将参数拆解为数组
    const args = Array.prototype.slice.call(arguments);

    // 获取this,并从数组剔除
    const _this = args.shift();

    // 原来的fn
    const self = this;

    // 返回一个函数
    return function () {
        return self.apply(_this, args);
    }
};

function fn () {
    console.log(this);
    console.log(arguments);
    return 'this is a fn';
}

let fn1 = fn.bind1({
    name: 'fn',
}, 10, 20);

let res = fn1();

console.log(res);

bind、call和apply的调用效果

javascript
let Fruit = function () {
    console.log(this);
};

Fruit(); // window

Fruit.prototype = {
    color: 'green',
    say: function () {
        console.log(`my color is ${this.color}`)
    }
};

var apple = new Fruit();
apple.say();

let banana = {
    color: 'yellow'
};

apple.say.bind(banana)(); // 返回一个副本
apple.say.call(banana); // yellow
apple.say.apply(banana); // yellow