美文网首页
作用域和闭包

作用域和闭包

作者: 陈裔松的技术博客 | 来源:发表于2018-12-06 09:30 被阅读0次

执行上下文

示例代码:

console.log(a); // undefined
var a = 100;

fn('zhangsan'); // zhangsan 20
function fn(name){
    age = 20;
    console.log(name,age);
    var age;
}

为什么执行结果是这个样子?fn函数调用之前都没有定义,应该调用不到才对呀?
要解释这个现象,我们先来看看执行上下文的规则:

  • 范围:
    执行上下文是一段script或一个函数
  • 全局:
    在一段script中,会先把变量定义,函数声明提取出来放到前面
  • 函数:
    在一个函数中,会先把变量定义,函数声明,this,arguments提取出来放到前面

根据执行上下文规则,示例代码可以解释成这个样子:

var a;
function fn(name){
    var age;
    age = 20;
    console.log(name,age);
}

console.log(a); // undefined
a = 100;

fn('zhangsan'); // zhangsan 20

可以很容易看出,执行结果是没有问题的。
下面我们再来看看"函数声明"和"函数表达式"的不同之处。

函数声明
fn1();
function fn1(){
    // 函数声明
    console.log('fn1'); // fn1
}

这是函数声明,可以正常显示fn1,不会报错。
因为这段代码相当于:

function fn1(){
    // 函数声明
    console.log('fn1'); // fn1
}
fn1();
函数表达式
fn2();
var fn2 = function (){
    // 函数表达式
    console.log('fn2');
}

这是函数表达式,这样是会报错的。
报错信息:Uncaught TypeError: fn2 is not a function
因为这段代码相当于:

var fn2;

fn2();
fn2 = function (){
    // 函数表达式
    console.log('fn2');
}

因为在执行fn2()的时候,还不知道fn2是个函数,所有会报错。

this

重要:this要在执行时才能确认值,定义时无法确认!!

var name = 'C';
var a = {
    name:'A',
    fn:function (){
        console.log(this.name);
    }
}
a.fn();                 // this === a,输出结果为:A
a.fn.call({name:'B'});  // this === {name:'B'},输出结果为:B

var fn1 = a.fn;
fn1();                  // this === window,输出结果为:C

分析:

  • 作为对象属性执行
    a.fn()是对象的一般使用方式,this指向的是a,所以输出结果是:A
  • call,apply,bind
    a.fn.call({name:'B'})this指向的是call里的参数,也就是 {name:'B'},所以输出结果是:B
  • 作为普通函数执行
    fn1()是window对象调用的,this指向的就是window对象,所以输出对象是:C
  • 作为构造函数执行
    this指向的是构造函数的实例,这里就不再演示了。

补充:call,apply,bind的区别

function fn(name,age){
    console.log(name);
    console.log(age);
    console.log(this);
}
// this指向的是window对象
fn('aaa',10);
// aaa
// 10
// Window

// this指向的是{x:200}
fn.call({x:200},'bbb',20);
// bbb
// 20
// {x: 200}

// this指向的是{x:300},然后参数要以数组形式传递
fn.apply({x:300},['ccc',30]);
// ccc
// 30
// {x: 300}

var fn2 = function(name,age){
    console.log(name);
    console.log(age);
    console.log(this);
}.bind({x:400});

// this指向的是{x:400}
// bind只能用在函数表达式,用在函数声明是不可以的
// 可以向普通函数那样调用
fn2('ddd',40);
// ddd
// 40
// {x: 400}

作用域

!!重要:ES6中的let和const拥有块级作用域,详细参照文章ES6(let和const)

  • 没有块级作用域
    在其他语言(比如Java),以下情况是会报错的。但是在JS中,由于不存在块级作用域,是不会报错的。
    注意,虽然不会报错,但是不要这样写,容易引起误解。
// 无块级作用域
if(true){
    var name = 'zhangsan';
}
console.log(name);  // zhangsan
  • 只有全局和函数作用域
    注意:尽量不要在全局作用域中定义变量,容易被污染。
// 函数和全局作用域
var a = 100;
function fn(){
    var a = 200;
    console.log('fn',a);
}
console.log('global',a);    // global 100
fn();                       // fn 200

// 以下就是全局变量污染的情况
var a= 300;
console.log('global',a);    // global 300
  • 作用域链
    当前函数作用域内没有定义的变量,会去其父级作用域中去寻找。如果还没有,就会再往上找,直到找到全局作用域,这就叫作用域链。
    注意:父级作用域,是指定义函数的作用域,而不是调用函数的作用域。
var a = 100;
function fn(){
    var b = 200;

    // 当前作用域没有定义的变量,即“自由变量”
    console.log(a); // 100

    console.log(b); // 200
}
fn();

预解析

var a = 1;
var a = 2;
// 预解析的时候,a是变量,且值为undefined
var a = 1;
function a(){
}
// 预解析的时候,a是函数a()。可以理解为函数的体量比较大,所以就预解析为函数
function a(){
    x = 1;
}
funciton a(){
    x = 2;
}
// 预解析的时候,a是函数a(),且x = 2
// 这些情况的变量不会预解析
console.log(a); // 报错
a = 1;
let a = 1;
const a = 1;

// 只有var定义的变量,才会预解析
console.log(b); // undefined
var b =1;
// script标签中的预解析是按标签一个一个解析的
// 这种情况下输出1
<script>
    var a = 1;
</script>
<script>
    console.log(a);  // 1
</script>

// 这种情况下会报错
<script>
    console.log(a); // 报错
</script>
<script>
    var a = 1;
</script>

闭包

闭包的概念:

闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数)

闭包的示例:
// 这就是一个普通的闭包

// 特点:函数b嵌套在函数a内,函数a需要返回函数b

// 用途:
// 1:读取函数内部变量
// 2:让变量i保留在内存中,记住就行,没有为什么

function a(){
    var i = 0;

    function b(){
        console.log(i);
    }
    return b;
}
var c = a();
c();    // 1
闭包的优缺点:

优点:有利于封装,可以访问局部变量
缺点:内存占用浪费严重,会造成内存泄漏

闭包的使用场景1:函数作为返回值
function F1(){
    var a = 100;

    // 返回一个函数(函数作为返回值)
    return function(){
        console.log(a);  // a是自由变量,去父级作用域中寻找
    }
}
// f1得到一个函数
var f1 = F1();
var a = 200;
f1();   // 100

分析:
console.log(a);中的a是一个自由变量,所有要在定义函数的父级作用域中寻找。
定义函数的父级作用域时F1函数作用域,所以输出结果是100。

闭包的使用场景2:函数作为参数传递
function F1() {
    var a = 100;

    // 返回一个函数(函数作为返回值)
    return function () {
        console.log(a); // a是自由变量,去父级作用域中寻找
    }
}
// f1得到一个函数
var f1 = F1();

function F2(fn){
    var a = 200;
    fn();
}
F2(f1);

常见问题

问题1:请举一个实际开发中闭包应用的例子
// 闭包在实际应用中主要用于封装变量,收敛权限
function isFirstLoad(){
    var _list = [];
    return function(id){
        if(_list.indexOf(id) >= 0){
            return false;
        }else{
            _list.push(id);
            return true;
        }
    }
}
var fn = isFirstLoad();

console.log(fn(100));   // true
console.log(fn(100));   // false
console.log(fn(200));   // true
console.log(fn(200));   // false

注意:
以下滑线开头的变量表示私有变量(比如_list)。
在isFirstLoad函数外面,根本访问不到变量_list,这就是闭包的意义。

提问:
这个list数组之所以在第二次执行firstLoad()的时候仍保存了上一次执行的结果,是因为 isFirstLoad()只执行了一次的原因吗?
回答:
isFirstLoad() 执行了一次,就产生了一个函数作用域,然后 _list 数组就存在这个作用域里面

问题2:创建10个<a>标签 点击的时候弹出对应的序号
  • 错误示范:无论点击哪个,弹出的都是10
var i,a;
for(i=0;i<10;i++){
    a = document.createElement('a');
    a.innerHTML = i + '<br/>';
    a.addEventListener('click',function(e){
        e.preventDefault();
        alert(i);   // 自由变量,要去父作用域中获取值
    });
    document.body.appendChild(a);
}

分析:因为i是自由变量,要去父作用域中获取值。在父作用域中的i是一个全局变量,在For循环运行完之后,值已经变成10了。

  • 正确示范1:通过添加自执行函数解决这个问题
var i;
for (i = 0; i < 10; i++) {
    (function (i) {
        // 函数作用域
        var a = document.createElement('a');
        a.innerHTML = i + '<br/>';
        a.addEventListener('click', function (e) {
            e.preventDefault();
            alert(i); // 自由变量,要去父作用域中获取值
        });
        document.body.appendChild(a);
    })(i);
}

// 要知道,以上就相当于
(function (i) {
    // 函数作用域
    var a = document.createElement('a');
    a.innerHTML = i + '<br/>';
    a.addEventListener('click', function (e) {
        e.preventDefault();
        alert(i); // 自由变量,要去父作用域中获取值
    });
    document.body.appendChild(a);
})(1);

(function (i) {
    // 函数作用域
    var a = document.createElement('a');
    a.innerHTML = i + '<br/>';
    a.addEventListener('click', function (e) {
        e.preventDefault();
        alert(i); // 自由变量,要去父作用域中获取值
    });
    document.body.appendChild(a);
})(2);

// ...依次类推,一直到10...

分析:因为i是自由变量,要去父作用域中获取值。在父作用域中的i是自执行函数的函数作用域里的变量。找到这个变量之后,不会再往上去寻找,所以i的值就对应每个序号了。

  • 正确示范2:使用ES6的let定义变量
var a;
for(let i=0;i<10;i++){
    a = document.createElement('a');
    a.innerHTML = i + '<br/>';
    a.addEventListener('click',function(e){
        e.preventDefault();
        alert(i);   // 自由变量,要去父作用域中获取值
    });
    document.body.appendChild(a);
}

分析:
let定义的变量拥有块级作用域,所以i的值就对应每个序号了。
关于ES6的let和const,参照文章ES6(let和const)

相关文章

  • 作用域和闭包

    目录 概述 作用域编译过程词法作用域全局作用域函数作用域 闭包循环和闭包闭包的用途性能 总结 概述 作用域和闭包一...

  • javaScript门道之闭包

    闭包的学习路径:变量的作用域 -> 闭包的概念 ->闭包的应用 1.变量的作用域 变量的作用域分为作用于全局和作用...

  • 2023-01-12

    变量提升调用栈块级作用域作用域链和闭包 闭包 => 作用域链(词法作用域) => 调用栈(栈溢出) => 上下文...

  • 闭包(closure)

    ● 闭包基础 ● 闭包作用 ● 闭包经典例子 ● 闭包应用 ● 闭包缺点 ● 参考资料 1、闭包基础 作用域和作...

  • 2018-01-07 关于javascript闭包和作用域的理解

    关于 javascript 闭包的一些思考 作用域 词法作用域 函数作用域 块作用域 闭包 什么是作用域? 作用域...

  • js作用域、闭包

    闭包 闭包作用 全局 局部 作用域链

  • 浓缩解读《JavaScript设计模式与开发实践》③

    三、闭包和高阶函数 3.1 闭包 3.1.1 变量的作用域 所谓变量的作用域,就是变量的有效范围。通过作用域的划分...

  • js闭包的理解

    什么是闭包 通俗的来讲,个人觉得闭包就是延长变量作用域的函数。众所周知js的作用域分为全局作用域和链式作用域。在函...

  • 14.JS基础之作用域与闭包

    作用域: 全局作用域 函数作用域 块级作用域(ES6新增) 常见的闭包有:作为函数返回值的闭包与作为函数参数的闭包...

  • 执行环境 & 作用域 & 闭包

    执行环境 & 作用域 & 闭包 执行环境 , 作用域 , 闭包 , 闭包应用 执行环境 执行环境定义了 当前环境的...

网友评论

      本文标题:作用域和闭包

      本文链接:https://www.haomeiwen.com/subject/cqdncqtx.html