js中声明提升、作用域(链)、`this`关键字和箭头函数

428 阅读7分钟

1.关于声明提前

  • js中,允许变量使用在声明之前,不过此时为undefined
console.log(a); // undefined
var a = 1;
  • 变量不管在哪里声明,都会在任意代码执行前处理。在es5 strict mode,赋值给未声明的变量将报错。
  • 显式声明:带有关键字 var 的声明,作用域就是当前执行上下文,即某个函数,或者全局作用域(声明在函数外,即变量会挂在在window对象上)
  • 隐式声明:如果一个变量没有使用var声明,window便拥有了该属性,因此这个变量的作用域不属于某一个函数体,而是window对象。
function varscope(){
    foo = "I'm in function"; //直接赋值 没有声明
    console.log(foo);//I'm in function
}
varscope();
console.log(window.foo); //I'm in function
  • 关于声明提前的例子
function testOrder(arg) {
    console.log(arg); // arg是形参,不会被重新定义
    console.log(a); // 因为函数声明比变量声明优先级高,所以这里a是函数
    var arg = 'hello'; // var arg;变量声明被忽略, arg = 'hello'被执行
    var a = 10; // var a;被忽视; a = 10被执行,a变成number
    function a() {
        console.log('fun');
    } // 被提升到作用域顶部
    console.log(a); // 输出10
    console.log(arg); // 输出hello
}; 
testOrder('hi');
/* 输出:
hi 
function a() {
        console.log('fun');
    }
10 
hello 
*/

2.关于作用域

  • 函数作用域

    函数作用域内,对外是封闭的,从外层的作用域无法直接访问函数内部的作用域

    function bar() {
      var testValue = 'inner';
    }
    console.log(testValue);	// 报错:ReferenceError: testValue is not defined
    

    通过 return 访问函数内部变量:

    function bar(value) {
      var testValue = 'inner';
      return testValue + value;
    }
    console.log(bar('fun'));// "innerfun"
    

    通过 闭包 访问函数内部变量:

    function bar(value) {
      var testValue = 'inner';
      var rusult = testValue + value;
      function innser() {
        return rusult;
      };
      return innser();
    }
    console.log(bar('fun'));		// "innerfun"
    
  • 立即执行函数作用域

    这是个很实用的函数,很多库都用它分离全局作用域,形成一个单独的函数作用域;它能够自动执行(function() { //... })()里面包裹的内容,能够很好地消除全局变量的影响;

    <script type="text/javascript">
        (function() {
          var testValue = 123;
          var testFunc = function () { console.log('just test'); };
        })();
        console.log(window.testValue);		// undefined
        console.log(window.testFunc);		// undefined
    </script>
    
  • 块级作用域

    在 ES6 之前,是没有块级作用域的概念的。

    for(var i = 0; i < 5; i++) {
      // ...
    }
    console.log(i)				// 5
    

    很明显,用 var 关键字声明的变量,在 for 循环之后仍然被保存这个作用域里;

    这可以说明: for() { }仍然在,全局作用域里,并没有产生像函数作用域一样的封闭效果;

    如果想要实现 块级作用域 那么我们需要用 let 关键字声明

    for(let i = 0; i < 5; i++) {
      // ...
    }
    console.log(i)				// 报错:ReferenceError: i is not defined
    

    在 for 循环执行完毕之后 i 变量就被释放了,它已经消失了!!!

    同样能形成块级作用域的还有 const 关键字:

    if (true) {
      const a = 'inner';
    }
    console.log(a);				// 报错:ReferenceError: a is not defined
    

    let 和 const 关键字,创建块级作用域的条件是必须有一个 { } 包裹:

  • 词法作用域

    当我们要使用声明的变量时:JS引擎总会从最近的一个域,向外层域查找

    testValue = 'outer';
    function afun() {
      var testValue = 'middle';
      console.log(testValue);// "middle"
      function innerFun() {
        var testValue = 'inner';
        console.log(testValue);// "inner"
      }
      return innerFun();
    }
    afun();
    console.log(testValue);	// "outer"
    

    当 JS 引擎查找变量时,发现全局的 testValue 离得更近一些,则取全局的testValue的值即 outer

    var testValue = 'outer';
    function foo() {
      console.log(testValue);// "outer"
    }
    
    function bar() {
      var testValue = 'inner';
      foo();
    }
    bar();
    
  • 动态作用域

    动态作用域,作用域是基于调用栈的,而不是代码中的作用域嵌套;

    作用域嵌套,有词法作用域一样的特性,查找变量时,总是寻找最近的作用域;

3.关于this关键字

在一个函数中,this总是指向当前函数的所有者对象,this总是在运行时才能确定其具体的指向, 也才能知道它的调用对象。

window.name = "window";
function f(){
    console.log(this.name);
}
f();//window

var obj = {name:'obj'};
f.call(obj); //obj

在执行f()时,此时f()的调用者是window对象,因此输出”window”

f.call(obj) 是把f()放在obj对象上执行,相当于obj.f(),此时f中的this就是obj,所以输出的是”obj”

对比以下两段代码:

var foo = "window";
var obj = {
    foo : "obj",
    getFoo : function(){
        return function(){
            return this.foo;
        };
    }
};
var f = obj.getFoo(); 
f(); //输出'window'

/* 分析
执行var  f = obj.getFoo()返回的是一个匿名函数,相当于:
var f = function(){
     return this.foo;
}
f() 相当于window.f(), 因此f中的this指向的是window对象,this.foo相当于window.foo, 所以f()返回"window" 
*/
var foo = "window";
var obj = {
    foo : "obj",
    getFoo : function(){
        var that = this;
        return function(){
            return that.foo;
        };
    }
};
var f = obj.getFoo();
f(); //输出'obj'

/* 分析
执行var f = obj.getFoo() 同样返回匿名函数,即:
var f = function(){
     return that.foo;
}
唯一不同的是f中的this变成了that, 要知道that是哪个对象之前,先确定f的作用域链:f->getFoo->window 并在该链条上查找that,此时可以发现that指代的是getFoo中的this, getFoo中的this指向其运行时的调用者,从var f = obj.getFoo() 可知此时this指向的是obj对象,因此that.foo 就相当于obj.foo,所以f()返回"obj"
*/

4.关于箭头函数

箭头函数有两种格式:

var fn = x => x * x; //只包含一个表达式,连{ ... }和return都省略掉了

x => { //还有一种可以包含多条语句,这时候就不能省略{ ... }和return:
    if (x > 0) {
        return x * x;
    }
    else {
        return - x * x;
    }
}

箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this是词法作用域,由上下文确定。

对比以下两个例子

var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = function () {
            return new Date().getFullYear() - this.birth;
        };
        return fn();
    }
};
//箭头函数完全修复了this的指向,this总是指向词法作用域,也就是外层调用者obj:
var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象
        return fn();
    }
};
obj.getAge(); // 29

//由于this在箭头函数中已经按照词法作用域绑定了,所以,用call()或者apply()调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略:
var obj = {
    birth: 1990,
    getAge: function (year) {
        var b = this.birth; // 1990
        var fn = (y) => y - this.birth; // this.birth仍是1990
        return fn.call({birth:2000}, year);
    }
};
obj.getAge(2015); // 25

箭头函数与this结合例子

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是一个高阶函数,它返回了一个函数,分步走的话,应该是这样:
var func = person3.show()
func()
从而导致最终调用函数的执行环境是window,但并不是window对象调用了它。所以说,this总是指向调用该函数的对象,这句话还得补充一句:在全局函数中,this等于window。
*/


person1.show3().call(person2)//person2 通过person2调用了最终的打印方法
person1.show3.call(person2)()//window 先通过person2调用了person1的高阶函数,然后再在全局环境中执行了该打印方法。

person1.show4()() //person1 箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象
person1.show4().call(person2) //person1 箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象,用person2去调用这个箭头函数,它指向的还是person1。
person1.show4.call(person2)() //person2 箭头函数的this指向的是谁调用箭头函数的外层function,箭头函数的this就是指向该对象,如果箭头函数没有外层函数,则指向window

例题:

var number = 5;
var obj = {
    number: 3,
    fn1: (function () {
        var number;
        this.number *= 2;
        number = number * 2;
        number = 3;
        return function () {
            var num = this.number;
            this.number *= 2;
            console.log(num);
            number *= 3;
            console.log(number);
        }
    })()
}
var fn1 = obj.fn1;
fn1.call(null);
obj.fn1();
console.log(window.number);

输出10 9 3 27 20