一、作用域
作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了块级作用域,可通过新增命令 let 和 const 来体现。
1.1 全局作用域
1.1.1 定义
直接编写在 script 标签之中的JS代码,或者是一个单独的 JS 文件中的都是全局作用域;
全局作用域在页面打开时创建,页面关闭时销毁;在全局作用域中有一个全局对象 window(代表的是一个浏览器的窗口,由浏览器创建),可以直接使用。
1.1.2 适用场景
- 最外层函数和在最外层函数外面定义的变量拥有全局作用域:所有创建的变量都会作为 window 对象的属性保存,所有创建的函数都会作为 window 对象的方法保存。
- 所有末定义直接赋值的变量自动声明为拥有全局作用域
1
2
3
4
5
6
7function outFun2() {
variable = "未定义直接赋值的变量";
var inVariable2 = "内层变量2";
}
outFun2(); //要先执行这个函数,否则根本不知道里面是啥
console.log(variable); //未定义直接赋值的变量
console.log(inVariable2); //inVariable2 is not defined - 所有 window 对象的属性拥有全局作用域(例如 window.name、window.location、window.top 等等)
1
2
3
4
5JavaScript默认有一个全局对象window,全局作用域的变量实际上被绑定到window上的一个属性。
this === window
var n = 4
console.log(this.n) //4
console.log(window.n)//4
1.2 函数/局部作用域
1.2.1 定义
函数作用域,是指声明在函数内部的变量,和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部。
调用函数时创建函数作用域,函数执行完毕之后,函数作用域销毁。每调用一次函数就会创建一个新的函数作用域,它们之间是相互独立的。
作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行。
值得注意的是:
块语句(大括号“{}”中间的语句),如 if 和 switch 条件语句或 for 和 while 循环语句,不像函数,它们不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中。
1 | if (true) { |
1.3 块级作用域
1.3.1 定义
块级作用域可通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问。
块级作用域在如下情况被创建:1、在一个函数内部;2、在一个代码块(由一对花括号包裹)内部
1.3.1 块级作用域特点
- 声明变量不会提升到代码块顶部
- 禁止重复声明(var可以,但会被后定义的变量覆盖掉)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18思考题1:
在 switch 声明中你可能会遇到这样的错误,因为它只有一个块
解决方法:
嵌套在case子句内的块将创建一个新的块作用域的词法环境
switch(x) {
case 0: {
let foo;
break;
}
case 1: {
let foo;
break;
}
}
思考题2:
由于词法作用域,表达式(foo + 2)内的标识符“foo”会解析为if块的foo,而不是覆盖值为 1 的foo。
在这一行中,if块的“foo”已经在词法环境中创建,但尚未达到(并终止)其初始化(这是语句本身的一部分):它仍处于暂存死1.4 全局变量和局部变量的区别
全局变量: 在任何一个地方都可以使用,全局变量只有在浏览器关闭的时候才会销毁,比较占用内存资源。
局部变量: 只能在函数内部使用,当其所在代码块被执行时,会被初始化;当代码块执行完毕就会销毁,因此更节省节约内存空间。
1.5 静态作用域
因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。
而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。
1 | var value = 1; |
1、假设JavaScript采用静态作用域,让我们分析下执行过程:
执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。
2、假设JavaScript采用动态作用域,让我们分析下执行过程:
执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。
前面我们已经说了,JavaScript采用的是静态作用域,所以这个例子的结果是 1。
1 | var value = 1; |
1.6 动态作用域
bash 就是动态作用域
《JavaScript权威指南》中的例子:
1 | 代码1: |
1 | 代码2: |
二、执行上下文栈
为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组:
1 | ECStack = []; |
试想当 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 globalContext:
1 | ECStack = [ |
1 | function fun3() { |
当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。
1 | // 伪代码 |
1 | 模拟代码1执行过程: |
1 | 模拟代码2执行过程: |
三、语句声明
3.1 var
JavaScript的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部。
JavaScript引擎能自动提升变量的声明,但不会提升变量的赋值。
变量提升:
1 | 代码1: |
1 | 代码2: |
1 | 代码3: |
3.2 let
let声明的变量只在其声明的块或子块中可用,这一点,与var相似。二者之间最主要的区别在于var声明的变量的作用域是整个封闭函数。
1 | function varTest() { |
位于函数或代码顶部的var声明会给全局对象新增属性, 而let不会!!!!
1 | var x = 'global'; |
3.3 const
常量是块级范围的,非常类似用 let 语句定义的变量。但常量的值是无法(通过重新赋值)改变的,也不能被重新声明。
用const定义的对象,可修改其值,因为const定义的变量存储的是对象的地址。
1 | const MY_OBJECT = {'key': 'value'}; |
四、this
4.1 定义
this总是指向调用该函数的对象。在全局函数中,this等于window,this指的是函数运行时所在的环境。
4.2 代码示例
1 | var name = 'Rose' |
不像基类的构造函数,派生类的构造函数没有初始的 this 绑定。在构造函数中调用 super() 会生成一个 this 绑定。
1 | 1、 |
1 | var a = 1; |
1 | function foo() { |
1 | var a = 2 |
1 | var a = 1; |
1 | 前端笔试: |
4.3 this在不同场景中的指向
- 匿名函数中的this指向全局对象
- setInterval和setTimeout定时器中的this指向全局对象
- eval中的this指向调用上下文中的this
- apply 和 call中的this指向参数中的对象
4.4 this绑定的优先级
箭头函数>new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
4.4.1 new绑定
函数使用new调用时,this绑定的是新创建的构造函数的实例
1 | function func() { |
4.4.2 显示绑定
call,apply,bind可以用来修改函数绑定的this
1 | function fn (name, price){ |
4.4.3 隐式绑定
函数是否在某个上下文对象中调用,如果是,this绑定的是那个上下文对象。
1 | var a = 'hello555' |
4.4.4 默认绑定
1 | var a = 'hello' |
1 | var a = 'hello' |
1 | var a = 'hello' |
1 | // 匿名函数中的this指向全局对象 |
4.5 call、apply、bind
通过call,apply,bind可以改变this的指向,this指向一般指向它的调用者,默认挂载在window对象下。es6中的箭头函数中,this指向创建者,并非调用者。
1 | var name = '宇智波佐助', age=17; |
1 | function add(c, d) { |
4.5.1 call
call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
1 | var name = '宇智波佐助', age=17; |
4.5.2 apply
apply() 方法调用一个具有给定this值的函数,以及以一个数组(或类数组对象)的形式提供的参数
1 | obj.myFun.apply(db,['成都','上海']); // 卡卡西 年龄 99 来自 成都去往上海 |
4.5.3 bind
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
1 | obj.myFun.bind(db,'成都','上海')(); // 卡卡西 年龄 99 来自 成都去往上海 |
4.6 箭头函数
4.6.1 定义
一个箭头函数表达式的语法比一个函数表达式更短,并且不绑定自己的 this,arguments,super或 new.target。箭头函数会捕获其所在上下文的 this 值,作为自己的 this 值。
谁调用箭头函数的外层function,箭头函数的this就是指向该对象,如果箭头函数没有外层函数,则指向window。
1、默认指向定义它时,所处上下文的对象的this指向,偶尔没有上下文对象,this就指向window
2、即使是call,apply,bind等方法也不能改变箭头函数this的指向
4.6.2 注意点
由于 箭头函数没有自己的this指针,通过 call() 或 apply() 方法调用一个函数时,只能传递参数(不能绑定this—译者注),他们的第一个参数会被忽略。(这种现象对于bind方法同样成立—译者注)
箭头函数中的this是根据其声明的地方来决定this的,它是ES6中出现的知识点,箭头函数中的this,是无法通过call,apply,bind被修改的,且因箭头函数没有构造函数constructor,导致也不能用new调用,就不能作为构造函数了,否则会出现错误。
1.箭头函数不能用作构造器,和 new一起用会抛出错误。
2.箭头函数没有prototype属性。
4.6.3 示例
- Hello是全局函数,没有直接调用它的对象,也没有使用严格模式,this指向window
1
2
3
4function hello() {
console.log(this); // window
}
hello(); - hello是全局函数,没有直接调用它的对象,但指定了严格模式(’use strict’),this指向undefined
1
2
3
4
5function hello() {
'use strict';
console.log(this); // undefined
}
hello(); - hello直接调用者是obj,第一个this指向obj,setTimeout里匿名函数没有直接调用者,this指向window
1
2
3
4
5
6
7
8
9
10const obj = {
num: 10,
hello: function () {
console.log(this); // obj
setTimeout(function () {
console.log(this); // window
});
}
}
obj.hello(); - hello直接调用者是obj,第一个this指向obj,setTimeout箭头函数,this指向最近的函数的this指向,即也是obj
1
2
3
4
5
6
7
8
9
10const obj = {
num: 10,
hello: function () {
console.log(this); // obj
setTimeout(() => {
console.log(this); // obj
});
}
}
obj.hello(); - diameter是普通函数,里面的this指向直接调用它的对象obj。perimeter是箭头函数,this应该指向上下文函数this的指向,这里上下文没有函数对象,就默认为window,而window里面没有radius这个属性,就返回为NaN。
1
2
3
4
5
6
7
8
9const obj = {
radius: 10,
diameter() {
return this.radius * 2
},
perimeter: () => 2 * Math.PI * this.radius
}
console.log(obj.diameter()) // 20
console.log(obj.perimeter()) // NaN4.7 代码参考
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36/**
* Question 1
*/
var name = 'window'
var person1 = {
name: 'person1',
show1: function () {
console.log(this.name)
},
show2: () => console.log(this.name),
show3: function () {
return function () {
console.log(this.name)
}
},
show4: function () {
return () => console.log(this.name)
}
}
var person2 = { name: 'person2' }
person1.show1() // person1
person1.show1.call(person2) // person2
person1.show2() // window
person1.show2.call(person2) // window
person1.show3()() // window
person1.show3().call(person2) // person2
person1.show3.call(person2)() // window
person1.show4()() // person1
person1.show4().call(person2) // person1
person1.show4.call(person2)() // person2
1 | /** |
代码片段参考:从这两套题,重新认识JS的this、作用域、闭包、对象
五、闭包
闭包特性一:调用函数内部的变量,利用作用域链原理,能获取函数fn1的父级函数的局部变量进行计算。
闭包特性二:让这些变量的值始终保持在内存中,不会再fn1调用后被自动清除,再次执行fn1的时候还能继续上一次的计算。
1 | function makeAdder(x) { |
1 | function makeSizer(size) { |
1 | var Counter = (function() { |