JS的声明提升、this、作用域
声明提升
大部分编程语言都需要先声明变量再使用,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
变量名可以是任意合法标识符,值可以是任意合法表达式。
重点:
- 变量声明,不管在哪里发生(声明),都会在任意代码执行前处理。
- 以var声明的变量的作用域就是当前执行上下文(execution context),即某个函数,或者全局作用域(声明在函数外)。
- 赋值给未声明的变量,当执行时会隐式创建全局变量(成为global的属性)。
声明变量和未声明变量的区别:
- 声明变量通常是局部的,未声明变量通常全局的。
- 声明变量在任意代码执行前创建,未声明变量直到赋值时才存在。
- 声明变量是execution context(function/global)的non-configurable 属性,未声明变量则是configurable。
在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 种:
- 所有的作用域默认都会给出 this 和 arguments 两个变量名(global没有arguments);
- 函数有形参,形参会添加到函数的作用域中;
- 函数声明,如 function foo() {};
- 变量声明,如 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
由函数怎么调用来确定。
-
简单调用,即独立函数调用。由于
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
-
箭头函数。
在箭头函数中,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
-
对象方法函数。当函数作为对象方法调用时,
this
指向该对象。
var obj = {
a: 'isObja',
fn: function() {
return this.a
}
}
console.log(obj.fn()) // 'isObja'
原型链上的方法和对象方法一样,作为对象方法调用时this指向该对象。
-
构造函数。在构造函数(函数用new调用)中,this指向要被constructed的新对象。
-
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
当用call
和apply
传进去第一参数o
,此时o
具备了add
的方法和属性,此时this
指向的是o
,注意:如果传入的是null/undefined
,this
指向全局对象。
6.bind。ES5引进了Function.prototype.bind。f.bind(someObject)会创建新的函数(函数体和作用域与原函数一致),但this被永久绑定到someObject,不论你怎么调用。
7.As a DOM event handler。this
自动设置为触发事件的dom元素。
作用域
作用域(scope)是什么?
先尝试从几个方面描述下:
- Scope这个术语被用来描述在某个代码块可见的所有实体(或有效的所有标识符),更精准一点,叫做上下文(context)或环境(environment)。
- 当前执行的上下文(The current context of execution)。https://developer.mozilla.org/en-US/docs/Glossary/Scope
综合一下,Scope即上下文,包含当前所有可见的变量。
Scope分为Lexical Scope
和Dynamic Scope
。JavaScript采用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有eval
和with
两种机制,但两者都会导致代码性能差。
-
eval
。接受字符串为参数,把这些字符串当做真的在程序的这个点写下的代码——意味着可以编码方式来在某个点生成代码,就像真的在程序运行前在这里写了代码。
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" )
-
with
。以对象为参数,并把这个对象当做完全独立的Lexical Scope,然后这个对象的属性就被当做定义的变量了。
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
另外,with
和 try catch
都可以创建Block Scope。
try {
undefined() // illegal operation to force an exception!
}
catch (err) {
console.log( err )
}
console.log( err ) // err not found