声明提升
大部分编程语言都是先声明变量再使用,但在JavaScript中,事情有些不一样:
console.log(a);//undefined上面是合法的JavaScript代码,正常输出undefined而不是报错Uncaught ReferenceError: a is not defined。为什么?就是因为声明提升(hoisting)。
var a = 0;
1.1 变量声明
参考: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var语法:
var varname1 [= value1 [, varname2 [, varname3 ... [, varnameN]]]]变量名可以是任意合法标识符;值可以是任意合法表达式。
重点:
- 变量声明,不管在哪里发生,都会在任意代码执行前处理;
- 声明的变量的作用域就是当前执行上下文,即某个函数或者全局作用域;
- 赋值给未声明的变量,当执行时会隐式创建全局变量;
- 声明的变量通常是局部的,未声明的变量通常是全局的;
- 声明变量在任意代码执行前创建,未声明的变量直到赋值时才存在;
- 声明的变量是执行上下文(function/global)的non-configurable属性,未声明的变量则是configurable;
- 在es5 strict mode,赋值给未声明的变量将报错;
1.2 定义函数
定义一个函数有两种方式:函数声明和函数表达式函数声明:
语法:function name(arguments){}
函数表达式:
语法:var fun = function(arguments){}
函数表达式中函数可以不需要名字,即匿名函数。
其它
还可以用Function构造函数来创建函数。
语法:var function_name = new Function(arg1, arg2, ..., argN, function_body);
在函数内部引用函数本身有3中方式。比如var foo = function bar(){};
- 函数名字,即bar();
- arguments.callee();
- foo();
1.3 声明提升
声明的变量会在任意代码执行前处理,这意味着在任意地方声明变量都等同于在顶部声明-即声明提升。生命提升中,需要综合考虑一般变量和函数。
在JavaScript中,一个变量名进入作用域的方式有四种:
- Language-defined:所有的作用域默认都会给出this和arguments两个变量名(global没有arguments);
- Formal parameters(形参):函数有形参,形参会添加到函数的作用域中;
- Function declarations(函数声明):如function foo(){};
- Variable declarations(变量声明):如var foo,包括函数表达式;
function testOrder(arg) {变量进入函数作用域的顺序为this,arguments --> 形参 --> 函数声明 --> 变量声明。如果重新定义与形参名字相同的变量,则变量声明部分会被忽略,形参会被重新赋值。代码执行前,声明提升部分已完成,函数的声明会比变量的声明先进入作用域。所以,在变量赋值前打印变量,对于函数则输出函数声明(包括函数体),对于变量则输出undefined。
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
this关键字
this关键字是JavaScript中最令人疑惑的机制之一。this是非常特殊的关键词标识符,在每个函数的作用域中被自动创建,但它到底指向什么(对象),很多人弄不清。 当函数被调用,一个activation record(即execution context)被创建。这个record包含信息:函数在哪调用(call-stack),函数怎么调用的,参数等等。record的一个属性就是this,指向函数执行期间的this对象。- this不是author-time binding,而是runtime binding(即this的值不是在代码解释阶段确定的,而是在函数被调用时确定的);
- this的上下文基于函数调用的情况。和函数在哪定义无关,而和函数怎么调用有关。
this在具体情况下的分析
2.1 Global context
在全局上下文,this指向全局对象。
console.log(this === window); //true
2.2 Function context
在函数内部时,this由函数怎么调用来确定。
2.2.1 Simple call
简单调用,即独立函数调用。由于this没有通过call来指定,且this必须指向对象,那么默认就指向全局对象。function f1(){在严格模式下,this保持进入execution context时被设置的值。如果没有设置,那么默认是undefined。它可以被设置为任意值(包括null/undefined/1等等基础值,不会被转换为对象)。
return this;
}
f1() === window; // global object
function f2(){
"use strict";
return this;
}
console.log(f2() === undefined); //true
2.2.2 Arrow functions
在箭头函数中,this有词法/静态作用域设置(set lexically)。它被设置为包含它的execution context的this,并且不再被调用方式影响(call/apply/bind)。
var globalObject = this;
var foo = (() => this);
console.log(foo() === globalObject); //true
//Call as a method of a object
var obj = {foo: foo};
console.log(obj.foo() === globalObject); //true
//Attempt to set this using call
console.log(foo.call(obj) === globalObject); //true
//Attempt to set this using bind
foo = foo.bind(obj);
console.log(foo() === globalObject); //true
2.2.3 As an object method
当函数作为对象方法调用时,this指向该对象。var o = {this on the object's prototype chain
prop: 37,
f: function() {
return this.prop;
}
};
console.log(o.f()); //logs 37
原型链上的方法跟对象方法一样,作为对象方法调用时this指向该对象。
2.2.4 构造函数
在构造函数(函数用new调用)中,this指向要被constructed的新对象。
2.2.5 call和apply
Function.prototype上的call和apply可以指定函数运行时的this。
function add(c, d) {注意,当用call和apply而穿进去作为this的不是对象时,将会调用内置的ToObject操作转换成对象。所以4将会转换成new Number(4),而null/undefined由于无法转换成对象,全局对象将作为this。
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
2.2.6 bind
ES5引进了Function.prototype.bind。f.bind(someObject)会创建新的函数(函数体和作用域与原函数一致),但this被永久绑定到someObject,不论你怎么调用。
2.2.7 As a DOM event handler
this自动设置为触发事件的dom元素。作用域(Scope)和闭包(closure)
3.1 Scope是什么?
先尝试从几个方面描述下:
- Scope这个术语被用来描述在某个代码块可见的所有实体(或有效的所有标识符),更精准一点,叫做上下文(context)或环境(environment)。
- 当前执行的上下文(The current context of execution)。
function foo(a) {Scope是分层的,内层的Scope可以访问外层Scope的变量,反之则不行。上面的代码中即有嵌套Scope。 Scope在我们写代码的时候就被定义好了,比如谁嵌套在谁里面。
// inner scope 'foo'
// defined argument a, and look-up b upwards
console.log( a + b );
}
// outmost/global scope
// defined b
var b = 2;
foo(2); //4
3.2 JavaScript Scope
JavaScript采用Lexical Scope。 于是,我们仅仅通过查看代码(因为JavaScript采用Lexical Scope),就可以确定各个变量到底指代哪个值。 另外,变量的查找是从里往外的,直到最顶层(全局作用域),并且一旦找到,即停止向上查找。所以内层的变量可以shadow外层的同名变量。3.2.1 Cheating Lexical
如果Scope仅仅由函数在哪定义的决定(在写代码时决定),那么还有方式更改Scope吗?JavaScript有eval和with两种机制,但两者都会导致代码性能差。3.2.1.1 eval
eval接受字符串为参数,把这些字符串当做真的在程序的这个点写下的代码--意味着可以编码方式来在某个点生成代码,就像真的在程序运行前在这里写了代码。function foo(str, a) {默认情况下,eval会动态执行代码,并改变当前Scope。但非直接(indirectly)调用eval可以让代码执行在全局作用域,即修改全局Scope。
eval( str ); // cheating
console.log( a, b );
}
var b = 2;
foo("var b = 3;", 1); // 1, 3
function bar(str) {另外,严格模式下,eval运行在它自己的Scope下,即不会修改包含它的Scope。
(0, eval)(str); // cheating in global!
}
bar("var hello = 'hi'");
window.hello; //"hi"
function foo(str) {
"use strict";
eval(str);
console.log(a); // ReferenceError: a is not defined
}
foo("var a = 2");
3.2.1.2 with
function foo(obj) {with以对象为参数,并把这个对象当做完全独立的Lexical Scope(treats that object as if it is a wholly separate lexical scope),然后这个对象的属性就被当做定义的变量了。 注意:尽管把对象当做Scope,var定义的变量仍然scoped到包含with的函数中。 不像eval可以改变当前Scope,with凭空创建了全新的Scope,并把对象传禁区。所以o1传进去时可以正确更改o1.a,而o2传进去时,创建了全局变量a。
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!
3.3 Dynamic Scope?
JavaScript没有Dynamic Scope。JavaScript中的this机制跟Dynamic Scope很像,都是运行时绑定。3.4 Function vs. Block Scope
JavaScript没有Block Scope 除了Global Scope,只有function可以创建新作用域(Function Scope)。不过这已经是老黄历了,ES6引入了Block Scope。{另外,with和try catch都可以创建Block Scope
let x = 0;
}
console.log(x); // Uncaught ReferenceError: x is not defined
try {
undefined(); // illegal operation to force an exception!
}
catch(err){
console.log(err); //works!
}
console.log(err); // ReferenceError: 'err' is not defined