提升(Hoisting)
- 简介
In JavaScript, functions and variables are hoisted. Hoisting is JavaScript's behavior of moving declarations to the top of a scope(the global scope or the current function scope).
That means that you are able to use a function or a variable before it has been declared, or in other words: a function or variable can be declared after it has been used already.
-
示例
2.1 变量提升
foo = 2;
var foo;
// is implicitly understood as:
var foo;
foo = 2;
2.2 函数提升
hoisted(); //logs "foo"
function hoisted() {
console.log("foo");
}
作用域和作用域链(Scope and Scope Train)
-
作用域
任何程序设计语言都有作用域的概念,简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在JavaScript中,变量的作用域有全局作用域和局部作用域两种。
全局作用域
在代码中任何地方都能访问到的对象拥有全局作用域。一般来说以下几种情形拥有全局作用域。
(1)最外层函数和在最外层函数外定义的变量拥有全局作用域。
(2)所有未声明直接赋值的变量拥有全局作用域。
(3)所有window对象的属性拥有全局作用域。
全局变量都是window对象的属性。局部作用域
和全局作用域相反,拥有局部作用域的变量或函数只在固定的代码片段之内可以被访问到。
-
作用域链
在JavaScript中,函数也是对象。函数对象和其他对象一样,拥有可以通过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是[[scope]],由ECMA-262标准第三版定义,该内部属性包含了函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问。
函数的作用域链是由一系列的对象(函数的活动对象+0个到多个的上层函数的活动对象+最后的全局对象)组成的。在函数执行的时候,会按照先后顺序从这些对象的属性中寻找函数体中用到的标识符的值。函数会在定义时将它们各自所处环境(全局上下文或者函数上下文)的作用域链存储到自身的[[scope]]内部属性中。
全局对象:JavaScript引擎在脚本开始执行之前就会创建全局对象,并添加一些预定义的属性。在脚本中定义的全局变量也会成为全局对象的属性。
活动对象:当JavaScript引擎调用函数时,被调用的函数会创建一个新的活动对象。所有在函数内部定义的局部变量、传入函数的命名参数和arguments对象都会作为这个活动对象的属性。这个活动对象加上该函数的[[scope]]内部属性中存储的作用域链就构成了本次函数调用的作用域链。
全局函数作用域链
var x = 10;
var y = 0;
function testFn(i){
var x = true;
y = y + 1;
alert(i);
}
testFn(10);
内部函数作用域链
function outerFn(i, j) {
var x = i + j;
return innerFn(x) {
return i + x;
}
}
var func1 = outerFn(5, 6);
var func2 = outerFn(10, 20);
alert(func1(10)); //返回15
alert(func2(10)); //返回20
问题:一个活动对象在函数执行时被创建,但在函数执行完毕后会不会被销毁?
- 没有内部函数的函数
function outerFn(x) {
return x * x;
}
var y = outerFn(2);
如果函数没有内部函数,则在该函数执行时,当前活动对象会被添加到该函数的作用域链的最前端.作用域链是唯一引用这个活动对象的地方.当函数退出时,活动对象会被从作用域链上删除,由于再没有任何地方引用这个活动对象,则它随后会被垃圾回收器销毁.
- 包含内部函数的函数,但这个内部函数没有被外部函数之外的变量引用
function outerFn(x) {
//在outerFn外部没有指向square的引用
function square(x) {
return x * x;
}
//在outerFn外部没有指向cube的引用
function cube(x) {
return x * x * x;
}
var temp = square(x);
return temp / 2;
}
var y = outerFn(5);
在这种情况下,函数执行时创建的活动对象不仅添加到了当前函数的作用域链的前端,而且还添加到了内部函数的作用域链中.当该函数退出时,活动对象会从当前函数的作用域链中删除,活动对象和内部函数互相引用着对方,outerFn函数的活动对象引用着嵌套的函数对象square和cube,内部函数对象square和cube的作用域链中引用了outerFn函数的活动对象.但由于它们都没有外部引用,所以都将会被垃圾回收器回收.
- 包含内部函数的函数,但外部函数之外存在指向这个内部函数的引用
例子1:
function outerFn(x) {
//内部函数作为外部函数的返回值被引用到了外部
return innerFn() {
return x * x;
}
}
//引用着返回的内部函数
var square = outerFn(5);
square();
例子2:
var square;
function outerFn(x) {
//通过全局变量引用到了内部函数
square = function innerFn() {
return x * x;
}
}
outerFn(5);
square();
在这种情况下,outerFn函数执行时创建的活动对象不仅添加到了当前函数的作用域链的前端,而且还添加到了内部函数innerFn的作用域链中(innerFn的[[scope]]内部属性).当外部函数outerFn退出时,虽然它的活动对象从当前作用域链中删除了,但内部函数innerFn的作用域链仍然引用着它. 由于内部函数innerFn存在一个外部引用square,且内部函数innerFn的作用域链仍然引用着外部函数outerFn的活动对象,所以在调用innerFn时,仍然可以访问到outerFn的活动对象上存储着的变量x的值。
- 作用域链和代码优化
- 从作用域链的结构可以看出,在运行期上下文的作用域链中,标识符所在的位置越深,读写速度就会越慢。因为全局变量总是存在运行期上下文作用域链的最末端,因此在标识符解析的时候,查找全局变量是最慢的。所以,在编写代码的时候,应尽量少使用全局变量,尽可能使用局部变量。一个好的经验法则是:如果一个跨作用域的对象被引用了一次以上,则先把它存储到局部变量再使用。
例如:
function changeColor() {
document.getElementById("btnChange").onclick = function() {
document.getElementById("targetCanvas").style.backgroundColor = "red";
}
}
改成:
function changeColor() {
var doc = document;
doc.getElementById("btnChange").onclick = function() {
doc.getElementById("targetCanvas").style.backgroundColor = "red";
}
}
- 避免使用with语句,因为with语句会改变作用域链,会将with后面的对象推到作用域链的顶端,意味着函数所有的局部变量都处在第二个作用域链对象中了,造成了访问的代价。
- catch语句也会改变作用域链,代码转入catch语句的时候,会把异常对象推到函数作用域链的顶端。因此,应该避免在异常处理环节访问局部变量。
分号自动添加机制(ASI)
- 不用使用分号结尾的语句
- for循环和while循环(do...while循环是有分号的)
for(;;;) {}
while(true) {}
do{
a--;
}wile(a > 0);
- 分支语句:if,switch,try
if(true) {}
switch(a) {
case 0:break;
default:;
}
- 函数的声明语句(但函数表达式还是要加分号的)
function func(x) {return x;}
var f = function f(x) {return x};
- 分号的自动添加
- 除了本来就不用写分号的情况,JavaScript引擎还有一个特点,就是在应该写分号却没有写的情况下,它会自动添加(Automatic Semiconlon Insertion)。
但是,这种添加也不是绝对的。如果下一行的开始与本行的结尾可以连在一起解析,就不会自动添加分号。 - 一般来说,如果下一行起首的是(, [, +, -, /这五个字符中的一个,分号不会被自动添加。
- 另外,如果一行的起首是++或--运算符,则它们的前面会自动添加分号。
- 如果continue、break、return和throw这四个语句后面,直接跟换行符,则会自动添加分号。
数据类型(Data Type)
在 ECMAScript 中,变量可以存在两种类型的值,即原始值和引用值。
原始值
存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。
引用值
存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存处。
ECMA-262把术语类型(type)定义为值的一个集合,每一种原始类型定义了它包含的值的范围及其字面量的表示形式。
ECMAScript有5种原始类型(primitive type),即Undefined, Null, Boolean, Number和String。
在许多语言中,字符串都被看作引用类型,而非原始类型,因为字符串的长度是可变的。ECMAScript 打破了这一传统。
ECMAScript 提供了 typeof 运算符来判断一个值是否在某种类型的范围内。可以用这种运算符判断一个值是否表示一种原始类型:如果它是原始类型,还可以判断它表示哪种原始类型。
函数(Function)
- 函数也是一种对象,函数名是变量,变量的值即是函数对象在堆内存中的地址。所有函数都应看作Function类的实例。
- 闭包:闭包指的是一种函数,这种函数能够引用函数外定义的变量。Javascript闭包函数的基础就是前面提到的函数作用域链。使用闭包的目的是为了设计私有的方法和变量。闭包的优点是可以避免全局变量的污染,缺点是闭包会常驻内存,会增大内存的使用量,使用不当很容易造成内存泄漏。
对象(Object)
- 面向对象的原则和要求:
- 封装 - 把相关的信息(无论数据或方法)存储在对象中的能力
- 聚合 - 把一个对象存储在另一个对象内的能力
- 继承 - 由另一个类(或多个类)得来类的属性和方法的能力
- 多态 - 编写能以多种方法运行的函数或方法的能力
ECMAScript只有公有作用域,没有私有作用域和受保护作用域,也没有静态作用域,但是可以给构造函数提供属性和方法。
this指针:this总是指向调用该函数的对象。使用this指针是因为实例化对象时无法确定使用的变量名,所以用this可以保证方法的可重用性。
创建对象实例的方式
- 工厂方法
- 构造函数方式
- 原型方式
- 混合的构造函数/原型方式
- 动态原型方法
- 混合工厂方式
- 继承机制的实现
- 对象冒充
- call方法
- apply方法
- 原型链
- 混合方式