作用域(scope)
什么是作用域?它是一个范围?一个对象?还是一个环境?对于新手来说这个很难理解
想像有一个计算机管理员,他对公司的系统有很大的控制权,因此可以向任何人授予完全访问权限。
假设你的公司里有三名计算机管理员,他们所有人都拥有对系统的完全访问权限。
开始一切运行顺利,但是突然你的系统感染了病毒,你开始分析是谁造成的,是管理员自己不小心感染了病毒吗?又或者是某个管理员私自授予其他人完全控制权限,然后那个人恶意植入病毒?说真的,你几乎找不出线索,因为你根本无从下手。
你应该意识到只为他们授予基本权限的账户,并且仅在他们需要时才授予完全访问权限。这样事情就变得透明可控,你能非常轻松的知道:哪个管理员在什么时间,对系统的哪个部位,做出了什么操作。
这称为最小访问原则(The Principle of Least Access),该原理也适用于编程语言设计,在大多数编程语言(包括我们接下来将要学习的 JavaScript 中被称为作用域
现在你应该对作用域有了一个大概的了解,我的理解就是:作用域决定了程序中的数据(变量、函数、对象等等)的访问规则
全局作用域
只要你在 script 标签内定义变量,那么这些变量就处在全局作用域;只要你在函数的内部定义了变量,那么这些变量就处于局部作用域
相对于浏览器而言,全局作用域就是 window 对象,你使用 Javascript 操控网页所定义的任何变量最终都属于 window 对象
<body>
<div>Hello World</div>
</body>
<script>
const a = 'Hello' // 全局作用域的变量
const b = 'World' // 全局作用域的变量
// 函数 c 是全局作用域的变量
function c(){
console.log(a+b)
// 函数 d 也是全局作用域的变量
function d(){
const d = 'ddd'
}
}
</script>
局部作用域
每个函数运行时都会创建自己的作用域,外部无法直接访问这个作用域内部,而内部却可以访问到外部
<body>
<div>Hello World</div>
</body>
<script>
const a = 'Hello'
const b = 'World'
function c(){
const d = 'I am Aelly' // 局部作用域的变量
console.log(a+b+d)
}
console.log(d) // Uncaught ReferenceError: d is not defined
</script>
上面的代码中,a、b、c都是全局作用域的变量,但是变量 d 则是处于局部作用域中,因为他声明在函数内部
块级语句
所谓块级语句就是用 { } 括起来的一段语句,比如 if 、try..catch、switch、for 等语句块。
在 ES5 中,使用 var 关键字声明变量,这些语句块不会创建额外的局部作用域,其内部声明的变量则会存在于当前语句块所处的作用域——全局作用域
var x = true
if(x){
var a = 'Hello'
}
console.log(a) // 'Hello'
上面的代码表明,if 这个块级语句并没有创建新的作用域,所以 if 内部声明的变量 a 在外部可以被访问到,这非常不符合直觉。
在 ES6 中,整个语言实际上都被升级成了严格模式,向上面的那样怪异现象,自然就被规范化了;ES6 中使用 let、const、import、class 等声明的变量统统具有块级作用域,也就是说 {} 内的变量存在于这个块级语句所创建的局部作用域中,外层作用域无法访问
let x = true
if(x){
let a = 'Hello'
console.log(a) // 'Hello'
}
a = 'Changed' // Uncaught ReferenceError: a is not defined
作用域链
const outside = 'outside'
function f(){
const inside = 'inside'
console.log(outside)
}
console.log(outside) // 'outside'
f() // 'outside'
console.log(inside) // Uncaught ReferenceError: inside is not defined
这段代码不长,仔细看完,你会发现一些东西:
- 第一个
console.log语句运行在最外层(全局执行环境下),它成功访问到了变量outside并打印出了值 - 函数
f也是运行在全局执行环境下,也成功的访问到了变量outside并打印出了值 inside定义在函数 f 的内部,第二个console.log语句发生错误,显示变量inside未定义
为什么会报错呢?这就是作用域的存在的结果:作用域的用途是保证对执行环境有权访问的所有变量和函数的有序访问
外部的执行环境只能访问到定义在外部执行环境中的变量,却无法访问定义在内层执行环境中定义的变量;而内层执行环境却可以访问到任何定义在外层执行环境中的变量,这就形成了一个链式结构,而且这个链还是单向的。
Javascript查找变量的过程称为标识符解析,而标识符解析是沿着这跟链一层层往外查找的,这根链就是作用域链
执行环境(context)
新手可能会对作用域和执行环境傻傻分不清,因为这两个东西实在是太相似了,可是它们又的确不是同一种东西,引用一个简单的比喻就能很清晰的解释作用域scope和执行环境context之间的关系
"If scope is in the method of an object, context will be the object the method is part of"
作用域是指一个对象的方法的内部,那么执行环境就好比这个方法所存在的对象
上面这段话再解释一下就是:作用域决定了变量是否可以被访问到,它是一个范围;而执行环境则是一个对象,这个对象通常的名字是 this ,作用域内部所有的变量都存储在这个对象上
我们知道,在浏览器中全局作用域是 window 对象,所以全局执行环境下的 this 对象就是 window
<body></body>
<script>
console.log(this)
// Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}
function say(){
console.log(this)
// Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}
}
</script>
代码中在全局作用域下打印出 this ,就会显示出 window 对象以及其内部细节;同样的,函数 say 也打印出了 this, 结果也是 window对象,这是因为 函数 say 声明在全局作用域中,所以它的执行环境(this)就是 window 对象。
我们单独地来看看在一个对象中,什么是作用域,什么是执行环境:
const Person = {
name: 'Jack',
age: 26,
say: function(){
console.log(this)
// {name: "Jack", age: "26", say: ƒ}age: "26"name: "Jack"say: ƒ ()__proto__: Object
console.log(this.name) // 'Jack'
console.log(this.age) // 26
}
}
Person.say()
函数say 打印出来 this 对象的值就是 Person 对象本身,因为 say 是定义在 Person 对象内部,而 this.name 和 this.age 当然就是 this 中保存的属性啦。对于 say方法来说:执行环境(this)就是 Person 对象,因为 say 在 Person 对象中声明;而作用域则是 say方法本身,包括其内部声名的所有变量。
改变执行环境
每当函数运行的时候都会创建一个局部作用域,同时会绑定一个执行环境到函数的 this 对象上,这个执行环境也叫上下文执行环境,因为函数可是运行在代码的任何地方,可以是对象中,也可以是块级语句中,所以这个上下文执行环境对于函数来说就很重要,因为它能让函数知道“自己在哪里,能够访问的变量有哪些”。
正因为函数运行的位置千变万化,所以它的上下文执行环境也会跟着变化,对于代码本身来说,就是 this 对象也在相应的发生变化。
下面的代码,演示了函数如何“随机应变”的来执行自己的任务:
const Fruits = {
name: 'Apple'
}
// const 声明的变量并不会被挂到 window 对象上, this.name 将不存在,所以改用 var 声明
var name = 'Orange'
function say() {
console.log(this.name)
}
say() // 'Orange'
从上面的代码可以看出,函数 say 的作用是打印出它作用域中的 name 变量值;Fruits 对象也想让自己的 name 被打印出来,可是它自己是没有定义 say 方法的,那么再写一个功能一模一样 say 方法来执行打印任务?不不不,这样就完全舍弃了函数复用性。
我们可以通过下面的代码,来让 Fruits 对象“借用”一下 say 函数的功能,这样就不必再写一个函数啦!
say.call(Fruits) // 'Apple'
看!通过 call 方法,我们把 say 的上下文执行环境改为了 Fruits 对象,其实等同于直接把 say 声明在 Fruits内部。所以我们可以得出结论:函数的 this 可以被更改,从而更好地实现函数的复用
call & apply
用了这么久的函数,我都没发现,其实函数的调用方式有很多种(菜鸡的我真没看出来😅)。有方法调用、直接调用、构造器调用等等调用方式,但是,所有的函数调用都可以转化为一种形式(call 调用):
var name = 'Orange'
const Fruits = {
name: 'Apple',
say: function(){
console.log(this.name)
}
}
function say() {
console.log(this.name)
}
// 直接调用形式
say()
// 转为 call 调用形式
say.call(this)
// 方法调用形式
Fruits.say()
// 转为 call 调用形式
say.call(Fruits)
发现没? call 方法就是用来给函数绑定上下文执行环境(this)的,call 方法还可以接受多个参数作为函数运行时的参数,看下面的代码:
const Fruits = {
name: 'Apple'
}
function say(color, taste, size) {
console.log(this.name, color, taste, size)
}
say.call(Fruits, 'Red', 'Sweet', 'Small') // Apple Red Sweet Small
call 方法的第一个参数是当前上下文执行环境对象,其余的参数个数不限,但是只能一个一个的传入,用逗号隔开。如果参数太多,一个个传递的话就很麻烦,不过我们可以用 apply 来解决参数繁多的问题:
const Fruits = {
name: 'Apple'
}
function say(color, taste, size) {
console.log(this.name,info)
}
const info = ['Red', 'Sweet', 'Small']
say.call(Fruits, info) // Apple ['Red', 'Sweet', 'Small']
call 和 apply 唯一的区别就是: call 只能一个个的接收参数,而 apply 可以接收参数组成的数组
bind
bind的作用也是改变函数的 this 值,不同的地方是:call 和 apply 都是声明后立即调用,但是 bind 不是,它返回被改变了 this 的那个函数,bind 跟 call 一样,第一个参数是要绑定的 this ,后面接收参数列表,用来给函数使用:
var obj = {
name: 'Jack'
}
function printName() {
console.log(this.name)
}
// 返回绑定了作用域的函数
const men = printName.bind(obj)
man() // Jack
参考链接: