JS的声明提升、this、作用域

2019-03-03  本文已影响0人  dosher_多舍

声明提升

大部分编程语言都需要先声明变量再使用,JS可以先使用再声明,JS可以正常输出undefined, 而不是报错,就是因为声明提升。

变量声明

var a = 100
console.log(a)        // 100
cosnole.log(b)        // undefined
var b = 10
console.log(b)        // 10
cosnole.log(c)        // c is not defined

变量名可以是任意合法标识符,值可以是任意合法表达式。

重点:

声明变量和未声明变量的区别:

es5 strict mode,赋值给未声明的变量将报错。

函数声明

定义一个函数有两种方式: 函数声明函数表达式

如果是function 的声明方式写一个函数,函数会被提前声明解析,则下面执行不会报错。如下面:

fn()
functon fn() {
    // 声明
}

则实际解析为:

function fn() {}
fn()

函数表达式中函数可以不需要名字,即匿名函数。通过var声明,则会出现var声明提前,如下

fn1()                         // fn1 is not a function
var fn1 = function() {
    // 表达式
}

实际解析为

var fn1;                      // fn1 = undefined
fn1();                        
fn1 = function() {};          // fn1 = function() {}
// fn1();    可执行

声明提升

var 声明的变量会在任意代码执行前处理,这意味着在任意地方声明变量都等同于在顶部声明——即声明提升,函数声明提升需要综合考虑一般变量和函数。

在JS中,一个变量名进入作用域的方式有 4 种:

  1. 所有的作用域默认都会给出 this 和 arguments 两个变量名(global没有arguments);
  2. 函数有形参,形参会添加到函数的作用域中;
  3. 函数声明,如 function foo() {};
  4. 变量声明,如 var foo,包括函数表达式

函数声明和变量声明总是会被移动(即hoist)到它们所在的作用域的顶部(这对你是透明的)。

而变量的解析顺序(优先级),与变量进入作用域的4种方式的顺序一致。

一个更详细的例子:

console.log(a)                  
var a = 100                     
console.log(a)                 
var age = 20
fn('what')                            
function fn(arg) {
    console.log(arg)
    console.log(age)
    var arg = 'how'
    console.log(arg)
    age = 120
    var age
    fn1()
    function fn1() {
        console.log(age)       
    }
}
console.log(age)                
fn1()

输出结果为

undefined
100
'what'
undefined
'how'
120
20
fn1 is no defined

上面的代码解析为什么出现这样子的结果?先看看实际解析如下

// 在实际运行时,声明会提前
var a                           
// 声明被提前
var age                        
// 声明被提前
function fn(arg) {                 
    // 声明被提前
    var age                    
    // 声明被提前
    function fn1(){             
        console.log(age)        
    }
    // arg是形参,不会被重新定义
    cosnole.log(arg)
    console.log(age)
    // var arg;变量声明被忽略,变量被赋值
    arg = 'how'
    console.log(arg)
    // 变量被赋值
    age = 120                   
    console.log(age)
    fn1()
}
// 变量被声明,但此时值为 undefined
console.log(a)                          // undefined     
// 变量a被赋值100
a = 100                         
console.log(a)                          // 100
// 变量age被赋值20
age = 20                    
fn('what')                            
/*
    // 函数内部执行

    console.log(arg)                    // 'what'
    // 在{}作用域内 此时age被重新声明,未被赋值,所以输出undefined
    cosnole.log(age)                    // undefined
    // 变量arg被赋值'how'
    arg = 'how'
    console.log(arg)                    // 'how'
    // 变量age被赋值120
    age = 120                   
    fn1()
        // 内部能读取外部变量,所有输出120
        console.log(age)                // 120
*/
// fn作用域内部的值为120,但是不能作用于外部作用域的age,所以为20
console.log(age)                        // 20
// 外部作用域并没有定义fn1,fn内部的fn1作用域仅在fn内部
fn1()                                   // fn1 is not defined


this

this是非常特殊的关键词标识符,在每个函数的作用域中被自动创建。

当函数被调用,一个activation record(即 execution context)被创建。这个record包涵信息:函数在哪调用(call-stack),函数怎么调用的,参数等等。record的一个属性就是this,指向函数执行期间的this对象。

简单说就是: this要在执行时才能确认值,定义时无法确定。

this在具体情况下的分析

在全局上下文(任何函数以外),this 指向的是全局对象。

console.log(this === window)            // true

在函数内部时,this 由函数怎么调用来确定。

  1. 简单调用,即独立函数调用。由于 this 没有通过 call 来指定,且 this 必须指向对象,那么默认就只指向全局对象。
function fn() {
    return this
}
fn() === window                         // true

在严格模式下,this保持进入execution context时被设置的值。如果没有设置,那么默认是undefined。它可以被设置为任意值(包括null/undefined/1等等基础值,不会被转换成对象)

function fn(){
    'use strict'
    return this
}
fn() === undefined                      // true
fn() === window                         // false
fn() === null                           // false
  1. 箭头函数
    在箭头函数中,this由词法/静态作用域设置(set lexically)。它被设置为包含它的execution context的this,并且不再被调用方式影响(call/apply/bind)。
var globalObj = this
var fn = (() => this)
console.log( globalObj === fn() )          // true

// call as a method of a object
var obj = { foo: fn }
console.log( globaclObj === obj.foo() )    // true

// attemp to set this using call
console.log( globalObj === fn.call(obj) )  // true

// attemp to set this using bind
foo2 = fn.bind(obj)
console.log( globalObj === foo2() )        // true
  1. 对象方法函数。当函数作为对象方法调用时,this 指向该对象。
var obj = {
    a: 'isObja',
    fn: function() {
        return this.a
    }
}

console.log(obj.fn())                       // 'isObja'

原型链上的方法和对象方法一样,作为对象方法调用时this指向该对象。

  1. 构造函数。在构造函数(函数用new调用)中,this指向要被constructed的新对象。

  2. call 和 apply。Function.prototype上的call和apply可以指定函数运行时的this。

function add(c, d){
  return this.a + this.b + c + d;
}

var o = {a:1, b:3};
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

当用callapply传进去第一参数o,此时o具备了add的方法和属性,此时this指向的是o,注意:如果传入的是null/undefinedthis指向全局对象。
6.bind。ES5引进了Function.prototype.bind。f.bind(someObject)会创建新的函数(函数体和作用域与原函数一致),但this被永久绑定到someObject,不论你怎么调用。

7.As a DOM event handlerthis自动设置为触发事件的dom元素。


作用域

作用域(scope)是什么?

先尝试从几个方面描述下:

综合一下,Scope即上下文,包含当前所有可见的变量。

Scope分为Lexical ScopeDynamic ScopeJavaScript采用Lexical Scope,没有Dynamic Scope。

Lexical Scope正如字面意思,即词法阶段定义的Scope。换种说法,作用域是根据源代码中变量和块的位置,在词法分析器(lexer)处理源代码时设置。

function fn(a) {
    var c = 12
    console.log(a+b)
}

var b = 2 
fn(1)                   // 3
console.log(c)          // c is not defined

Scope是分层的,内层Scope可以访问外层Scope的变量,反之则不行。变量的查找是从里往外的,直到最顶层(全局作用域),并且一旦找到,即停止向上查找。所以内层的变量可以shadow外层的同名变量。

Cheating Lexical

如果Scope仅仅由函数在哪定义的决定(在写代码时决定),那么还有方式更改Scope吗?JS有evalwith两种机制,但两者都会导致代码性能差。

function foo(str, a) {
    eval( str )             // cheating!
    console.log( a, b )
}

var b = 2
foo( "var b = 3;", 1 )      // 1, 3

默认情况下,eval会动态执行代码,并改变当前Scope。但非直接(indirectly)调用eval可以让代码执行在全局作用域,即修改全局Scope。

function bar(str) {
    (0, eval)( str );       // cheating in global!
}
bar('var hello = "hi";')

window.hello                // "hi"

另外,严格模式下,eval运行在它自己的Scope下,即不会修改包含它的Scope。

function foo(str) {
   "use strict"
   eval( str )
   console.log( a )         // ReferenceError: a is not defined
}

foo( "var a = 2" )
function foo(obj) {
    with (obj) {
        a = 2
    }
}

var o1 = { a: 3 }
var o2 = { b: 3 }

foo( o1 )
console.log( o1.a )      // 2

foo( o2 )
console.log( o2.a )      // undefined
console.log( a )         // 2 -- Oops, leaked global!

注意:尽管把对象当做Scope, var 定义的变量仍然scoped到包含 with 的函数中。

不像 eval 可以改变当前Scope,with 凭空创建了全新的Scope,并把对象传进去。所以 o1 传进去时可以正确更改 o1.a ,而 o2 传进去时,创建了全局变量 a

Function vs. Block Scope

Scope除了Global Scope,function可以创建新作用域(Function Scope),除此之外,ES6引入了Block Scope。

{
    let x = 0
}
console.log(x)          // x is not defined

另外,withtry catch 都可以创建Block Scope。

try {
    undefined()         // illegal operation to force an exception!
}
catch (err) {
    console.log( err )
}

console.log( err )      // err not found
上一篇下一篇

猜你喜欢

热点阅读