作用域

问题

先看这段代码

1
2
3
4
5
6
7
{
a = 1;
function a() { };
a = 2;
console.log(a); // 输出:2
}
console.log(a); // 输出:1

是不是很不解???常规思路都是输出2啊。
没关系,在了解了JS块级作用域之后你就懂了

全局作用域和function

Ecma5之前只有函数和全局作用域,也就是全局window或者function(){...}函数之内,而且var和function,在未声明之前可以访问,原因是js有内部变量提前的特性
在同一作用域下function函数和var声明的变量都会被提至当前作用域的顶层,var优先声明,function其次,其中function提升的同时,函数体的实现也定义了出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function test() {
b();
console.log('我是函数');
var num = 1;

function b() {
console.log('我是内嵌函数b,num:', num);
}
}
test(); // 输出:1.我是内嵌函数b,num:undefined 2.我是函数

// 解释器解释代码之后会变成如下这个样子

function test() {
var num = undefined;
var b = function () { // function提升的同时,方法实现也定义了出来
console.log('我是内嵌函数b,num:', num);
}
b();
console.log('我是函数');
num = 1;
}
test();

块级作用域

Ecma6新增了块级作用域,增加了两个变量修饰符:let(值可变,不可二次声明)和const(常量、值不可变,可二次声明),未声明之前访问会报错,而且varlet以及const声明的变量不能互换
理解了函数和变量提升之后,那么问题来了,如果块级作用域和块外作用域共有一个同名的变量,而function函数写在块中,该函数引用到同名变量,那么该函数到底是用块内还是块外变量呢?如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var a = 1;
{
let a = 2;
function test() {
console.log(a);
}
}
test();

// 按照上面的函数和变量的提升思路,那么这就是解释器解释后的代码样子
var a = undefined;
var test = function () {
console.log(a);
}
a = 1;
{
let a = 2;
}
test();

很明显,按照变量和函数提升至顶层之后的思路,解释之后会输出1,可我们实际想要的是2呀。输出为1就违背了块级作用域的概念,那么该如何解决呢?毕竟变量和函数的提升是老的特性,新设计的特性肯定要兼容旧的。没办法,js制定者只能在做一些取舍了
取舍如下:

  1. 函数如果在块中,那么funtionvar照样提升至前,只不过function的实现不允许提前定义,这样可以避免块中的内容溢出到块外,即块的内容只在块里面
  2. 解释器在解释到块级作用域时,如果块中有函数,那么会在块中的最初位置用let以及新的变量名,重新定义一下这个函数,因为funtionlet定义在块中了,那么该function肯定可以访问到块中的变量
    • 为什么会在块中用新的变量从新定义呢,因为一个变量不能从varlet以及const相互转换
  3. 因为块内的函数的名称变了,所以块内涉及到的老的函数名称时,也要随着变。不然用let修饰的新变量名称也没有任何意义啊~
  4. 然后又因为函数的作用域不仅仅在块内,块外也可以访问(要兼容之前的特性),所以在执行到函数原有声明的位置时,他会用var以及原有的变量名再次声明一下
    这样就解决了块外和块内的变量名一样的问题了。代码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    var 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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
a = 1;
function a() { };
a = 2;
console.log(a); // 输出:2
}
console.log(a); // 输出:1

// 解释器解释之后的代码

var a = undefined; // 函数和变量提升至前,不给其函数实现的定义,以免块内污染块外
{
let new_a = function () { }; // 新变量名称用let进行修饰,并给出原有函数体的定义,使其函数在块内生效,在这个块中,涉及到原有变量名的都用新的变量名'new_a'
new_a = 1;
var a = new_a; // 执行到原有代码时,需要把函数用原有的名称用'var'重新修饰一下,使其块外能访问到
new_a = 2;
console.log(new_a); // 输出:2
}
console.log(a); // 输出:1

例子2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
a = 1;
{
function a() { };
a = 2;
console.log(a); // 输出:2
}
console.log(a); // 输出:function

// 解释器解释之后的代码

var a = undefined; // 变量提升
a = 1;
{
let new_a = function () { }; // 块内的方法用'let'以及新的变量名修饰, 在这个块中,涉及到原有变量名的都用新的变量名'new_a'
var a = new_a; // 执行到原有代码时,用var把原有的函数重新修饰,使其块外能访问到
new_a = 2;
console.log(new_a); // 输出:2
}
console.log(a); // 输出:function

闭包

在块级作用域出现之前,只有函数作用域和全局作用域,为了解决变量的污染,就有了闭包,何为闭包?我理解的就是立即调用一个没有名字的函数,使其变量都在该函数中

(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function test(arg1, arg2) {
this.arg1 = arg1;
this.arg2 = arg2;
arg3 = arguments;
// 本方法名称
this.functionName = arguments.callee.name;
// 本方法的调用者,为空为window
this.caller = arguments.callee.caller;
}


var o = new test(1, 2); // 对象创建
console.log(o.arg1); // 输出:1
console.log(window.arg1); // 输出:undefined

test(3, 4); // 方法调用
console.log(window.arg2); // 输出:4
console.log(window.arg3); // 输出:[3,4]

// 一句话描述this,谁调用的我,这个this就是谁的

每个方法都有隐含的参数arguments类型为数组,该属性包含了该方法的所有参数,这个特性也就确定了js方法是没有重载的
JS方法中的this是可以改变的

  • test.apply(this, arguments);
  • test.call(this, ...arguments);

apply和call都可以改变方法中的this,区别就是apply的参数必须传递数组,call只能把参数拆开,而...语法就是用来拆参数的(就是用来脱衣服的)

原型链

异步