作用域
问题
先看这段代码
1 | { |
是不是很不解???常规思路都是输出2啊。
没关系,在了解了JS块级作用域之后你就懂了
全局作用域和function
在Ecma5之前只有函数和全局作用域,也就是全局window
或者function(){...}
函数之内,而且var和function,在未声明之前可以访问,原因是js有内部变量提前的特性
在同一作用域下function
函数和var
声明的变量都会被提至当前作用域的顶层,var优先声明,function其次,其中function提升的同时,函数体的实现也定义了出来
1 | function test() { |
块级作用域
Ecma6新增了块级作用域,增加了两个变量修饰符:let
(值可变,不可二次声明)和const
(常量、值不可变,可二次声明),未声明之前访问会报错,而且var
和let
以及const
声明的变量不能互换
理解了函数和变量提升之后,那么问题来了,如果块级作用域和块外作用域共有一个同名的变量,而function函数写在块中,该函数引用到同名变量,那么该函数到底是用块内还是块外变量呢?如下
1 | var a = 1; |
很明显,按照变量和函数提升至顶层之后的思路,解释之后会输出1,可我们实际想要的是2呀。输出为1就违背了块级作用域的概念,那么该如何解决呢?毕竟变量和函数的提升是老的特性,新设计的特性肯定要兼容旧的。没办法,js制定者只能在做一些取舍了
取舍如下:
- 函数如果在块中,那么
funtion
和var
照样提升至前,只不过function的实现不允许提前定义,这样可以避免块中的内容溢出到块外,即块的内容只在块里面 - 解释器在解释到块级作用域时,如果块中有函数,那么会在块中的最初位置用
let
以及新的变量名,重新定义一下这个函数,因为funtion
被let
定义在块中了,那么该function肯定可以访问到块中的变量- 为什么会在块中用新的变量从新定义呢,因为一个变量不能从
var
和let
以及const
相互转换
- 为什么会在块中用新的变量从新定义呢,因为一个变量不能从
- 因为块内的函数的名称变了,所以块内涉及到的老的函数名称时,也要随着变。不然用
let
修饰的新变量名称也没有任何意义啊~ - 然后又因为函数的作用域不仅仅在块内,块外也可以访问(要兼容之前的特性),所以在执行到函数原有声明的位置时,他会用
var
以及原有的变量名再次声明一下
这样就解决了块外和块内的变量名一样的问题了。代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22var a = 1;
{
let a = 2;
function test() {
console.log(a);
}
}
test();
// 解释器解释后的代码的样子
var a = undefined; // 变量提升
var test = undefined; // 块内的函数提升,舍弃方法体的定义
a = 1;
{
let new_test = function () { // 块内的函数,用let以及新的变量名定义,并在此定义出函数实现体
console.log(a);
}
let a = 2;
var test = new_test; // 用var把原有的变量名声明一下,执行完该代码块时,函数也可以在外部访问
}
test();但是也有问题,比如在块中定义的函数,必须执行完块时,函数才可以访问
1
2
3
4
5
6
7// test(); //执行会报错,找不到方法
{
function test() {
console.log(123);
}
}
test(); //执行完块时才可访问世界上没有任何东西是十全十美的,在一件大事件上要尽量争取最好的度,做出最大的兼容(成本最小,接受面最广)
答案
回到最初的问题,我们以新解释器的角度重新审视一下代码,就能彻底的理解作用域的概念啦
例子1
1 | { |
例子2
1 | a = 1; |
闭包
在块级作用域出现之前,只有函数作用域和全局作用域,为了解决变量的污染,就有了闭包,何为闭包?我理解的就是立即调用一个没有名字的函数,使其变量都在该函数中
(function(arg){console.log('我是闭包,arg:', arg)})(123/*把外部变量从这里传进去*/);
这样就把变量锁死在大括号中了,可以理解为对一个匿名方法的调用
也还有其他的写法如:!function(arg){console.log('我是闭包,arg:', arg)}(123/*把外部变量从这里传进去*/);
为什么前面必须有运算符呢?可以理解为一元运算符对后面匿名变量的运算,如果把感叹号
!
去掉的话,执行器就不知道后面到底是什么语法了
可以理解为只要是一元运算符后面都可以接匿名变量,+
或者-
运算符都可以都可以
为什么大多数都用!感叹号呢,因为运算时占用的cpu和空间比较少,他就是一个取反的运算:非真即假,其次因为编写也方便
对象
谈到对象,最熟悉的莫过于this
,this为当前对象,咱们都知道对象都是new
出来的,那么js中的this
到底该怎么用呢?window
为全局对象,在任何一个地方,如果一个变量a = 1
(没有任何修饰,如var、let、const),那么也可以理解为window.a = 1
var
声明的变量的作用域在funtion
中,否则在上层的function,若上层没有function,那么就会延伸到window
中
设计对象的结构
Ecma6之前,对象同方法,只需要new即可,至于对象的结构(包含的字段),在函数中用this,指定即可
1 | function test(arg1, arg2) { |
每个方法都有隐含的参数
arguments
类型为数组,该属性包含了该方法的所有参数,这个特性也就确定了js方法是没有重载的
JS方法中的this是可以改变的
test.apply(this, arguments)
;test.call(this, ...arguments)
;apply和call都可以改变方法中的this,区别就是apply的参数必须传递数组,call只能把参数拆开,而
...
语法就是用来拆参数的(就是用来脱衣服的)