🦄 2024 独立开发者训练营,一起创业!查看介绍 / 立即报名 →

JavaScript Functional Programming:作用域

全局作用域

打开一个 js 文件,写了一行代码,这行代码所在的位置就会是全局作用域(global scope)。比如:

var name = '狗狗'

局部作用域

全局作用域只有一个,在全局使用域里面定义的其它的作用域都被称为局部作用域(local scope)。局部作用域是由函数创建的,每个函数都会创建一个局部作用域。

下面我创建了一个名字是 greet 的函数,在它里面声明了一个 name 变量,这个变量是在局部作用域之内。

// 作用域 A: 全局作用域

var greet = function() {
  // 作用域 B:局部作用域
  // 这里是由 greet 这个函数创建的局部作用域

  var name = '猫咪'
}

在这个局部作用域里面定义的东西,在这个作用域的外面是访问不到的。试一下:

var greet = function() {
  var name = '猫咪'
}

console.log(name)
// 返回:undefined

在 greet 函数里声明的 name 变量,在外面不能访问到,因为这个 name 是在 greet 函数创建的局部作用域之内。

词法作用域

词法作用域(Lexical Scope)。如果你在一个函数的内部又创建了一个函数,这个内部函数可以访问到外部函数的作用域。这就是词法作用域。

看个例子:

// 作用域 A 
var greet = function() {
  // 作用域 B:在这里定义了局部变量 name
  var name = "小刺猬"
  var logger = function() {
    // 作用域 C:在这里使用了在爸爸(greet)那里声明的变量 name
    console.log(name)
  }
  logger()
}

greet()
// 输出 “小刺猬”

greet 函数创建了一个作用域,在 greet 里面定义的 logger 函数也会创建一个作用域,logger 函数是在 greet 内部定义的,它可以访问到在它之外的作用域里的东西。这里就是我们在 logger 函数里输出了在 greet 创建的作用域里定义的 name 这个变量的值。

下面这个例子,name 变量在 function3,function2,function1 都是可以访问到的:

var name = '猫咪'
var function1 = function() {
  // 在这里可以访问到 name
  var function2 = function() {
    // 在这里也可以访问到 name
    var function3 = function() {
    // 在这里还可以访问到 name
    }
  }
}

假设你在 function3 里使用了 name 变量,JavaScript 在它里面没找到 name,就会往上一层继续寻找有没有 name,还没找到就会继续往上一层寻找,真到找到为止,如果最终结果还是没找到 name,就会报 undefined 。

闭包

闭包(closure)。指的是一种函数,这种函数使用了在它周围作用域下定义的变量。

MDN 里的关于闭包的中文文档非常好的解释了什么是闭包。闭包是一种特别的对象,也可以说是一种特别的函数,这种对象有两个部分组成,一部分是函数本身,还有一部分是创建这个函数的时候的这个函数所在的那个环境。也就是闭包是函数 + 环境。

也就是函数在某种特别情况下被创建,就会形成闭包。这个特别情况就是,函数使用了在它自己的局部作用域以外定义的变量。

看个例子:

function robot() {
  var name = '小猫'
  function greet() {
    console.log(`我是${name},喵 ~`)
  }
  return greet
}

var kitty = robot()
kitty()

robot 函数里定义了一个局部变量叫 name,在 robot 函数里又返回了一个函数,名字是 greet,在这个内部函数里使用了在 robot 局部作用域下定义的变量 name。这就形成了一个闭包。也就是 kitty 就是一个闭包,这个闭包是 greet 函数,还有 greet 函数引用的 name 所组成的。

再看一个例子:

function robot(name) {
  function greet(greeting) {
    console.log(`我是${name}, ${greeting},`)
  }
  return greet
}

var kitty = robot('小猫')
kitty('喵 ~ ') // 输出:我是小猫, 喵 ~

var puppy = robot('小狗')
puppy('汪汪 ~') // 输出:我是小狗,汪汪 ~

这回 robot 函数带一个 name 参数,robot 还会返回一个 greet 函数,返回的这个 greet 函数也带一个参数,就是 greeting,在这个 greet 函数里我们用到了 robot 的 name 参数,还有 greeting 本身的参数 greeting 。

我们基于 robot 又创建了两个函数,kitty,还有 puppy,这两个东西都是闭包 。这两个函数的主体部分都是一样的,不一样的地方是,在创建它们的时候的环境是不一样的。在创建  kitty 的时候,name 的值是 “小猫”,在创建 puppy 的时候,name 的值是 “小狗”。

this

每个作用域里面都绑定了一个特别的值叫 this ,具体 this 表示的是什么要看函数是怎么被执行的。

this 翻译成中文就是“这...”,它本身并没有特殊的意义,它的意义完全取决于你在什么情景下使用它。比如你在跟朋友讨论一部电影:《Brother》,“这部电影是北野武的一部作用”,在这句话里,这(this),指的就是《Brother》这部电影。如果是在另一种语境下,这个 this 表示的可能是完全不同的意思。

在 JavaScript 里面,this 表示的东西也是要看所处的语境(context),也可以说上下文,情境,环境 ...

情形一

在全局作用域的下面,this 一般表示的是 window 对象。打开浏览器的控制台,输入:

this

会返回 window 对象里的东西。

Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage, sessionStorage: Storage, webkitStorageInfo: DeprecatedStorageInfo…}

在全局作用域下定义一个变量,你可以在这个 window 对象里得到。试一下:

var name = '小猫'
this.name // '小猫'
window.name // '小猫'

情形二

再试一下:

var robot = function() {
  console.log(this)
}
robot()

在定义的函数里面输出 this ,同样会得到一个 window 对象。但是如果代码使用了 'use strict' ,像这样:

'use strict';
var robot = function() {
  console.log(this)
}
robot()

返回的会是:undefined

再试一下:

var robot = {
  greet(){
    console.log(this)
  }
}

robot.greet()
// this 表示的是 greet 方法所属的 robot 对象

robot 是个对象,里面有个 greet 方法,在这个方法的里面,this 表示的是方法所属的对象,这里 this 表示的就是 robot 这个对象。

情形三

假设页面上有个元素:

<button id="signup">订阅</button>

我的代码像这样:

var signupButton = document.querySelector('#signup')
var signupLog = function() {
  console.log(this)
}
signupButton.addEventListener('click', signupLog, false)

点击了页面上的 “订阅” 按钮,会在控制台上输出在 click 事件处理器,也就是 signupLog 函数里的 this 所表示的东西,这里 this 表示的是发生 click 事件的那个 button 元素。

情形四

这次 this 出问题了:

var signupButton = document.querySelector('#signup')
var signupLog = function() {
  console.dir(this) // this 是发生 click 事件的元素
  setTimeout(function () {
    console.log(this) // this 表示的是 window 对象
  }, 1000);
}
signupButton.addEventListener('click', signupLog, false)

这次在 signupLog 里的 setTimeout 里的 this 指的是 window 对象。想让这里的 this 表示发生 click 事件的元素,可以这样:

var signupButton = document.querySelector('#signup')
var signupLog = function() {
  var _this = this
  console.dir(_this)
  setTimeout(function () {
    console.log(_this); 
  }, 1000);
}
signupButton.addEventListener('click', signupLog, false)

改变 this 的值

call(),apply(),bind() ,这些方法都可以改变 this 表示的值。

var robot = function() {
 console.log(this)
}

robot.call('hello')
// this 的值是 hello

call() 与 apply() 的用法:

.call(thisArg, arg1, arg2, arg3)
.apply(thisArg, [arg1, arg2])

call() 与 apply() 会执行我们的函数。

参考资料

  1. https://developer.mozilla.org/en/docs/Web/JavaScript/Closures
  2. https://toddmotto.com/everything-you-wanted-to-know-about-javascript-scope/
函数式编程 JavaScript

评论

所以 setTimeout 函数里面的 this 出输出 window,是因为 setTimeout 这个函数本身就是 window 对象的方法吗?

我再求证一下。

我感觉如果理解了作用域链的话,其实理解闭包就很简单了。闭包好像就是为了要“修复”作用域链断开的情况,又有点像 Promise 的连写 then,到最后的 catch

window.setTimeout()

皓哥用的什么代码高亮插件?

用的 highlight.js,atom 主题。

微信好友

用微信扫描二维码,
加我好友。

微信公众号

用微信扫描二维码,
订阅宁皓网公众号。

240746680

用 QQ 扫描二维码,
加入宁皓网 QQ 群。

统计

14696
分钟
0
你学会了
0%
完成

社会化网络

关于

微信订阅号

扫描微信二维码关注宁皓网,每天进步一点