作用域相关/立即执行函数/闭包/闭包应用场景

这次学的有点断断续续的,排版有点乱 😃
[TOC]

作用域

  • 区分LHS 和 RHS

1.LHS -> 左查询 -> 查询是为了赋值
2.RHS -> 非左查询 -> 为了查到变量的值

// 例1:
var a = 1;

// 例2:
- RHS查询demo
- LHS查询num,将2赋值num
- RHS查询num的值

function demo(num){
  console.log(num)
}
demo(2)

// 例3:
console.log(a)
  • LHS 和 RHS 异常报错
  • 若 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError异常;
  • 若引擎执行 LHS 查询时,在顶层(全局作用域)中也无法找到目标变量,“严格模式”下,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎;“严格模式下”,禁止自动或隐式地创建全局变量,LHS 查询失败时,并不会创建并返回一个全局变量,引擎会抛出ReferenceError异常;
  • 若RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,
    比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的属性,那么引擎会抛出 TypeError;

词法作用域

词法作用域:由定义变量或者函数声明的位置决定

  • 举例
  1. b的词法作用域即函数a()的局部作用域
  2. 变量a是全局变量 => 函数aFun,bFun,cFun均可以访问
  3. 变量c是局部变量 => 函数aFun,bFun可以访问
var a
function aFun(){
  var c
  function bFun(){
    var d
    console.log(1)
  }
}
function cFun(){

}
  • 未声明即使用的变量是全局变量,注意任何位置均可访问的问题

函数作用域和块作用域

var声明的变量都会成为全局变量

let

  • let会为其声明的变量隐式绑定所在的块作用域,通常是{...}内部(eg.1)
  • 开发时最好为块作用域显式地创建块, 使变量的附属关系变得更加清晰(eg.2)
  • 块作用域非常有用的原因和闭包及回收内存垃圾的回收机制相关 (eg.3.1 3.2),
    • 原本process执行之后process(..) 执行后,someReallyBigData变量占用的内存就可以销毁,
    • 但是, click 函数形成了一个覆盖整个作用域的闭包,someReallyBigData变量虽然没有使用,JavaScript 引擎极有可能依然保存着这个结构(取决于具体实现),添加了块级作用域后,让引擎清楚地知道没有必要继续保存 someReallyBigDat变量;
// eg.1
for(let i = 0;i<3;i++){
  ...
}
console.log(i)  // ReferenceError

// eg.2
if(foo){
  { // => 显式的块
    let bar = 1;
    console.log(bar);
  }
}

// eg.3.1
function process(data) {
  ...
}
var someReallyBigData = { .. };
process( someReallyBigData );

var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt) {
  console.log("hello");
});

// eg.3.2
function process(data) {
  ...
}
{
  // 在这个块中定义的内容可以销毁了!
  let someReallyBigData = { .. };
  process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
  console.log("hello");
});

函数声明和函数表达式

区分:看function 的位置 (function 是声明中的第一个词,那么就是一个函数声明 )
区别:名称标识符将会绑定在何处:

  1. a : 被绑定在全局作用域
  2. b : 被绑定在函数表达式自身的函数中, 即 (...)所在的位置中访问,外部作用域则不行,b 变量名被隐藏在自身中意味着不会污染外部作用域
// 函数声明
function a(){
  console.log(1)
}

// 函数表达式
(function b(){
  ...
})

块级作用域

立即执行函数表达式(IIFE)

由于函数被包括在一对()括号内部,第一个()使之成为一个表达式,第二个()执行了这个函数

优点:

  1. 不污染全局作用域
  2. 不需显式的通过函数名调用这个函数
// 写法一:
(function(){})()
// 写法二:
(function(){}())
// 函数表达式
var a = 2;
(function foo() {
  var a = 3;
  console.log( a ); // 3
})();
console.log( a ); // 2
foo() // 报错:Uncaught ReferenceError

IIFE进阶用法

  • 将IIFE当作函数调用并传递参数进去(eg.1)
// eg.1
// 实参:window
// 形参:global
var a = 2;
(function IIFE(global){
    var a =3;
console.log(a); // 3
console.log(global.a) // 2
})(window)

什么是闭包?

当函数可以 记住并访问 其所在的 词法作用域 ,使函数在他本身词法作用域以外执行,就产生了闭包;

  1. 内部函数 showA 在被执行之前就被返回, 函数 showA 在定义的词法作用域以外被调用;
  2. 由于函数 showA 占用了函数 result 的变量a, 导致函数a执行完毕后,函数result() 的内存空间不会被垃圾回收机制清除;
  3. 函数 showA 依然持有函数result()函数作用域的引用,这个引用就叫做闭包;
  4. 闭包使得函数可以继续访问定义时的词法作用域;
// eg.1
function result() {
  var a = 1;
  function showA() {
    console.log(a)
  }
  return showA
}
result()()  // 1

闭包应用场景举例

无论以何种方式对函数类型的值进行传递,在函数在别处调用时都可以观察到闭包

// 可以是在全局作用域调用局部函数
function a() {
  var cc = 1
  function b() {
    console.log(cc);
  }
  return b
}
const fn = a()
fn()  // 1

// 也可以是局部作用域调用另一个局部作用域的函数
function a() {
  var cc = 1
  function c() {
    console.log(cc)
  }
  b(c)
}
function b(fn) {
  fn()
}
a()

开发中常见闭包应用场景举例

1. setTimeout()


function wait(message) {
  setTimeout( function timer() {
      console.log( message );
    }, 1000);
}

wait( "Hello,hello!" );

1.timer 还持有变量message的引用 ,因此形成涵盖wait()函数作用域的闭包;
2.wait() 执行1s后,他的内部作用域并不会消失,timer函数依然保有wait()作用域的闭包

2. for循环和setTimeout拿到正确的值问题

for(var i=0;i<10;i++){
    setTimeout(function(){
        console.log(i)//10个10
    },1000)
}

预想:每秒一次,每次一个,打印出1--10
实际:1秒后一次打印出10个10

出现原因

  • setTimeout的参数一直持有变量i,形成了闭包,
  • var 声明的i是全局变量
  • 每次for循环就是将定时器(微任务)加入微任务队列,for循环之后依次执行微任务队列中的任务,而此时的i==10;
  • 任务队列的十个setTimeout 共享同一个词法作用域,由于循环在定时任务触发之前就已经执行完毕,由于var声明变量具有变量提升的特点,此时的i===10,因此每次取出的的setTimeout任务访问到的i的值都是10

?setTimeout是1s后将任务加入任务队列还是立即加入任务队列,去队列中拿出来任务,等1s 再执行?

解决方法:使用let

let会产生局部作用域
let 不仅将 i 绑定到了 for 循环的块中,事实上循环的每一个迭代它将重新绑定,确保使用上一个循环迭代结束时的值重新进行赋值

for(var i=0;i<10;i++){
    setTimeout(function(){
        console.log(i)//10个10
    },1000)
}

上边的代码执行顺序相当于

let a = 1;
console.log(a)
a = 2;
console.log(a)
 a = 3;
console.log(a)

3. 闭包和for循环问题(同二)

闭包只能取得包含函数中任何变量的最后一个值

function a() {
  var arr = new Array()
  for (var i = 0; i < 10; i++) {
    arr[i] = function () {
      return i
    }
  }
  return arr
}
console.log(a()[0]())   // 10
console.log(a()[1]())   // 10
console.log(a()[2]())   // 10

4. 使用闭包模拟私有方法和变量

// 注意与单例模式进行区分
// 区分:todo:闭包各自维护自己的内存空间
function demo() {
  var a = 1
  return {
    add1: function () {
      a += 1;
      return a;
    },
    sub1: function () {
      a -= 1;
      return a;
    },
    aValue: function () {
      return a
    }
  }
}
var Demo1 = demo()
var Demo2 = demo()
console.log(Demo1.add1()) // 2
console.log(Demo2.add1()) // 3

模拟了面向对象编程的样子,实现数据隐藏和封装
Demo1 和Demo2 各自维护独立的各自独立的词法作用域,同时引用的是自己词法作用域的变量a
每次调用 demo 的时候,通过改变这个变量的值,会改变这个闭包的词法作用域,然而在一个闭包内对变量的修改,并不会影响到另一个闭包中的变量

5. 使用闭包设置单例模式

什么是单例模式?

  • 保证一个类只有一个实例

常见的单例模式应用场景:

  1. windows的task manger(任务管理器)(非JS)
  2. 网站的计数器,一般也是采用单例模式实现,否则难以同步
// todo :单例模式仅实例化一次
var demo = (function () {
  var a = 1
  return {
    add1: function () {
      a += 1;
      return a;
    },
    sub1: function () {
      a -= 1;
      return a;
    },
    aValue: function () {
      return a
    }
  }
}())
var demo2 = demo
console.log(demo.add1())  // 2
console.log(demo2.add1()) // 3

6. 内存泄漏问题

什么是内存泄漏?

  • 程序运行需要内存,只要程序提出需求,操作系统就必须提供内存,对于持续运行的服务进程,必须及时释放不再使用的内存,否则,内存占用越来越高, 轻则影响系统性能,重则导致进程崩溃;
  • 不再用到的内存,没有及时释放,就叫做内存泄漏;

闭包会导致内存泄漏?

  • 不会
  • 由于IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集,从而导致内存无法进行回收,所以可能导致内存泄漏;
  • 由于闭包会使函数中的变量一直保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题;

7. 闭包实现数据问题,定时手动销毁

8. 函数防抖

9. 常见闭包中this的问题

10. 闭包常见两个面试题

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

alert(object.getNameFunc()());  // "The Window"

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

alert(object.getNameFunc()());  // "My Object"

使用闭包注意

性能

  • 使用完成之后,记得手动清楚 赋值=null ,以免一直占用内存 (即内存泄漏问题)
  • 闭包会携带包含他的函数作用域,因此会比其他函数占用更多的内存,过度使用可能会导致内存占用过多;

相关参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容