你不知道的JavaScript读书笔记(作用域与闭包)

132 阅读7分钟

第三章 函数作用域和快作用域

隐藏内部实现

function doSomething(a){
  b = a + doSomethingElse(a * 2)
  console.log(b * 3)
}
function doSomethingElse(a){
  return a - 1
}
var b					
doSomething(2) //15

在这个代码片段中,doSomethingElse函数和变量b都是函数doSomething的私有内容,应该放到函数doSomething中。

function doSomething(a){
  function doSomethingElse(a){
    return a - 1
  }
  var b
  b = a + doSomethingElse(a * 2)
  console.log(b * 3)
}
doSomething(2) //15

规避冲突

function foo(){
  function bar(a){
    i = 3 ;//修改for循环中所属作用域的i
    console.log(a + i)
  }
  for(var i = 0;i < 10;i++){
    bar(i * 2); //造成无限循环
  }
}
foo()

这个代码片段中,由于bar函数中的i会导致for循环的i进行变化,从而导致无限循环(将var换成let可行)

全局命名空间

模块管理

函数作用域

可以通过包装函数,将内部变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容

var a = 2
function foo(){ // 《--添加这一行
  var a = 3
  console.log(a); //3
}// 《--添加这一行
foo()// 《--添加这一行

console.log(a); //2

通过这种包装函数foo将内部的变量a和外部变量a给隔离开

需要添加三行才可以实现隔离

var a = 2;
(function foo(){// 《--添加这一行
  var a = 3
  console.log(a); //3
})()// 《--添加这一行
console.log(a); //2

上面这个代码片段中foo被绑定在函数表达式自身中而不是所在的作用域的,也就是说

(function foo(){...})作为函数表达式意味着foo只能在 .. 所代表的位置中所访问。

匿名和具体

setTimeout(function(){
   console.log("I wailted one second")
},1000)

建议给匿名函数加上名字,比如timeoutHandler,这样子可以方便调试、理解代码、引用自身

setTimeout(function timeoutHandler(){
   console.log("I wailted one second")
},1000)

立即执行函数表达式

var a = 2;
(function foo(){// 《--添加这一行
  var a = 3
  console.log(a); //3
})()// 《--添加这一行
console.log(a); //2

第一个()将函数编程表达式,第二个()执行了这个函数

var a = 2;
(function IIFE(global){
  var a = 3
  console.log(a);       //3
  console.log(global.a);//2
})(window)
console.log(a); //2

对这个函数IIFE将全局变量window传递进入,更换变量名字,然后通过global.a获取到函数外的变量a = 2

应用场景

undefined = true; //绝对不要这么做
(function IIFE(undefined){
  var a;
  if(a === undefined){
    console.log("Undefined is safe here")
  }
})();

这样子可以保证函数IIFE中的undefined是正确的而不是外面那个被篡改的,因为没有传入参数所以函数IIFE中的参数就是undefined

var a = 2;
(function IIFE(def){
  def(window);
})(function def(global){
  var a = 3
  console.log(a); //3
  console.log(global.a); //2
})

这里有两个函数IIFEdefIIFE的传入参数是函数def,然后通过def(window),将全部变量传入进入,然后改换名字为global,从而调用变量外部变量a

块作用域

for(var i = 0;i < 10;i++){
  console.log(i)
}
console.log(i)

在这里的话,在for循环中使用var i,会使得i泄露,然后外部可以进行调用

try/catch

try{
  undefined(); //执行一个非法操作来制造一个异常
}
catch (err){
  console.log(err); //能够正常执行
}
console.log(err);//"ReferenceError: err is not defined

let

let关键字可以将变量绑定到所在的任意作用域

let会在所在的作用域中执行。

{
  console.log(bar); //不会提升
  let bar = 2;
}

垃圾收集

let循环

var foo = true;
if(foo){
  var a = 2;
  const b = 3;//包含在if中的块作用域常量
  a = 3;//正确
//   b = 4;//错误
}
console.log(a);//3
console.log(b);//"ReferenceError: b is not defined

第四章 提升

先有鸡还是先有蛋

a = 2;
var a;
console.log(a); //2

输出的结果是2,因为使用var变量会让变量a进行提升

console.log(a) //undefined
var a = 2 

输出的结果是 undefined

编译器

对于变量

对于变量的话,编译器在执行的时候,首先会先定义声明,接着才是赋值声明。

对于函数

函数声明会提升

function f(){...}

在其作用域中可以调用

foo();
function foo(){
  console.log(a);
  var a = 2;//undefined
}
foo();

函数表达式不会提升

let fun = function f(){...}

fun不会被提升

f();//"TypeError: f is not a function,表明被提升了,但是此时函数f = function还没有赋值完成
foo();//ReferenceError
var f = function foo(){
  console.log(a);
  var a = 2;//undefined
}
f();//undefined

函数优先

函数声明和变量声明都会被提升,但是顺序是,函数先被提升然后才是变量

foo();
function foo(){
  console.log(1);
}
foo = function(){
  console.log(2);
};
function foo(){
  console.log(3);
}

输出的结果是3,函数声明重复以最后的3为最新的值,函数声明先于函数表达式

foo();//"TypeError: foo is not a function	表示没有提升
var a = true
if(a){
  function foo(){
    console.log("a")
  }
}
else{
  function foo(){
    console.log("b")
  }
}
foo();//"b"

避免在块内部声明函数

第五章 作用域闭包

function foo(){
  var a = 2;
  function bar(){
    console.log(a);
  }
  bar();
}
foo();
console.log(a);//"ReferenceError: a is not defined,说明a没有到全局变量上

清晰地展现闭包:

function foo(){
  var a = 2;
  console.log(a)
  function bar(){
    console.log(a);
  }
  return bar
}
// foo()
var baz = foo(); //把内部函数的地址传递给它
baz(); //2,朋友这就是闭包

另外一种形式

function foo(){
  var a = 2;
  function baz(){
     console.log(a);//2
  }
  bar(baz);
}
function bar(fn){
  fn();//快看,这就是闭包
}
foo();//这里用了一个内部的函数,这个内部就是foo函数内部的一个函数

把内部寒湖是baz传递给bar,当调用这个内部函数时fn

var fn
function foo(){
  var a = 2;
  function baz(){
    console.log(a)
  }
  fn = baz;//将baz分配给全部变量
}
function bar(){{
  fn();//快看,这就是闭包
}}
foo()
bar();//2

进一步了解

function wait(message){
  setTimeout(function timer(){
    console.log(message)
  },1000)
}
wait("你好呀")

就将一个内部函数timer传递给setTimeout(..)timer具有涵盖wait(..)作用域的闭包。所以可以对变量message进行引用。

只要使用了回调函数,实际上就是在使用闭包!!!

循环和闭包

for(var i = 1;i <= 5;i++){
  console.log("hello");//验证setTimeout是一个异步函数
  setTimeout(function timer(){
    console.log(i);
  },i*1000)
}

在这里的话,输出的时5次每隔1s输出6;应为在这个函数中变量i是一个共享变量,所以说for循环中的话,会先将函数setTimeout()函数先打印出来,然后再进行赋值。因为他是一个异步函数。

改进

for(var i = 1;i <= 5;i++){
  (function(j){
    setTimeout(function timer(){
      console.log(j);
    },i*1000);
  })(i);
}

迭代中每次使用IIFE都会生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每隔迭代内部,每隔迭代中都会含有一个具有正确值的变量供我们使用。

重返块作用域

for(let i = 1;i <= 5;i++){
  
  console.log("hello");//验证setTimeout是一个异步函数
  setTimeout(function timer(){
    console.log(i);
  },i*1000)
}

将原来的var i = 1转化成let i = 1

块作用域和闭包联手便可以天下无敌

模块

function CoolModule(){
  var something = "cool"
  var another = [1,2,3]
  
  function doSomething(){
    console.log(something)
  }
  
  function doAnother(){
    console.log(another.join("!"))
  }
  
  return {
    doSomething: doSomething,
    doAnother:   doAnother
  };
}
var foo = CoolModule()
foo.doSomething();//cool
foo.doAnother();//1!2!3

这个模式再JavaScript中称为模块,创建一个函数CoolModule(),这个对象类型的返回值看作本质上是模块的公共API,可以通过访问API中的属性,foo.doSomething()

  • 必须有外部的封闭函数
  • 封闭函数必须返回至少一个内部函数、

转成成单例模式

var foo = (function CoolModule(){
  var something = "cool"
  var another = [1,2,3]
  
  function doSomething(){
    console.log(something)
  }
  
  function doAnother(){
    console.log(another.join("!"))
  }
  
  return {
    doSomething: doSomething,
    doAnother:   doAnother
  };
})();
foo.doSomething();//cool
foo.doAnother();//1!2!3

\

公共API

var foo = (function CoolModule(id){
  function change(){
    //修改公共API
    publicAPI.identify = identify2;
  }
  function identify1(){
    console.log(id)
  }
  //变成大写
  function identify2(){
    console.log(id.toUpperCase());
  }
  //公共的API
  var publicAPI = {
    change:change,
    identify:identify1
  };
  return publicAPI
})("foo module");
foo.identify();//"foo module"
foo.change();
foo.identify();//"FOO MODULE"

通过模块实例的内部保留对公共API对象的内部引用,可以从内部对模块实例进行修改,包括添加或删除方法和属性,以及修改他们的值

现代的模块机制

var MyModules = (function Manager(){
  var modules = {};
  
  function define(name,deps,impl){
    for(var i = 0;i < deps.length;i++){
      deps[i] = modules[deps[i]]
    }
    //为了模块的定义引入了包装函数(可以传入任何依赖),并且将返回值,,也就是模块的API,存储在一个根据名字来管理的模块列表中
    modules[name] = impl.apply(impl,deps);
  }
  
  function get(name){
    return modules[name]
  }
  
  return {
    define:define,
    get:get
  }
})()
MyModules.define("bar",[],function(){
  function hello(who){
    return "Let me introduce: " + who;
  }
  //返回一个属性
  return {
    hello:hello
  }
})

MyModules.define("foo",["bar"],function(bar){
  var hungry = "hippo";
  
  function awesome(){
    //对hello属性进行大写操作
    console.log(bar.hello(hungry).toUpperCase());
  }
  return {
    awesome:awesome
  }
})
//得到对应的函数
var bar = MyModules.get("bar")
var foo = MyModules.get("foo")

console.log(
  bar.hello("hippo")
);//"Let me introduce: hippo"

foo.awesome();//"LET ME INTRODUCE: HIPPO"

模块的两个主要特征:

  1. 为创建内部作用域而调用了一个包装函数
  2. 包装函数的返回值必须直到包括一个对内部函数的引用,这样子就会创建涵盖整个包装函数内部作用域的闭包

动态作用域

function foo(){
  console.log(a);//2
}

function bar(){
  a = 3;//如果这里是var a = 3;foo()的输出是2,不然就是3
  console.log(a)

  foo()
}

var a = 2
bar()

可以使用语法作用域来理解,使用var a = 3的话就说明作用域被限制再bar()中了