Javascript函数和作用域总结


2017-09-01 添加this对象分析


一、执行环境和作用域

执行环境(execution context)定义了函数或变量有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。全局执行环境是最外围的一个执行环境。某个执行环境的所有代码执行完毕后,该环境销毁,保存在其中的所有变量和函数定义也随之销毁。(全局执行环境直到应用程序退出才会销毁)每个函数都有自己的执行环境。当执行流程进入一个函数时,函数的环境就会被推入一个环境栈中,在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域的用途是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端始终是当前执行的代码所在的环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对象最开始只包含一个变量,即arguments对象(这个对象在全局环境中不存在的),作用域链中的下一个对象来自包含(外部)环境,一直延续到全局执行环境。全局执行环境的变量对象始终都是作用域链的最后一个对象。

标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程中始终从作用域链的前端开始。搜索过程始终从作用域链的前端开始然后逐级地向后回溯,知道找到标识符为止(若找不到,通常会导致错误发生)

js没有块级作用域,但是在es6中新增let关键字可定义块级变量。

二、函数

定义

函数声明

1
2
3
function sum(num1,num2){
return num1 + num2;
}

函数表达式

1
2
3
var sum = function(num1,num2){
return num1 + num2;
}

解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);
函数表达式则必须等到解析器执行到它所在的代码行,才会真正被解析执行。
在代码开始之前,解析器就已通过一个名为函数声明提升(function declaration hoisting)的过程,将函数声明添加到执行环境中。对代码求值时,Javascript引擎在第一遍会声明函数并将他们放到源代码树的顶部。所以,即使声明函数的代码在调用它的代码后面,Javascript引擎也能把函数声明提升到顶部。

Function构造函数

1
2
//接收任意数量参数,最后一个参数始终被看做函数体
var sum = new Function('num1','num2','return num1 + num2');//不推荐

函数是对象,函数名是指针

没有重载

同名函数后一个函数会覆盖前一个函数,不会发生函数重载

三、参数传递

基本类型按值传递

1
2
3
4
5
6
7
function setNum(num){
num += 10;
return num;
}
var a = 10;
var result = setNum(a);
console.log(result);//20

引用类型按指针的值传递,并非按引用传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function setObj(obj){
obj.name = 'zxlg';
return obj;
}
var person = new Object();
setObj(person);
console.log(person.name);// 'zxlg'

//使用对象,看似按引用传递,
//其实不然,参数传递的是指针的值,
//指针的值是不会变的,指针指向的内容有可能会被改变
function setObj(obj){
obj.name = 'zxlg';
obj = new Object();
obj.name = 'fool';
return obj;
}
var person = new Object();
setObj(person);
console.log(person.name);//'zxlg'

四、arguments对象和this对象

arguments对象

类数组对象,包含着传入函数中的所有参数,可以用方括号访问它的每一个元素,使用length来确定传进多少个元素。函数命名的参数只提供便利,但不是必需的。

1
2
3
4
function doAdd(num1,num2){
arguments[1] = 10;
console.log(arguments[0] + num2);
}

arguments的值永远与对应命名参数的值保持同步。在非严格模式下,重写arguments[1],也就修改了num2,结果均变为10(严格模式下重写arguments的值会导致语法错误)。但它们的内存空间是独立的,而arguments的值会与参数的值同步。

若只传入一个参数,那么arguments[1]设置的值不会反应到命名参数中,因为arguments对象的长度是由传入参数的个数决定的,不是由定义函数的参数的个数决定的。

没有传递值的命名参数自动被赋予undefined值,类似于定义变量但没有初始化。

arguments的主要用途是保存函数参数,但这个对象还有一个名叫callee的属性,该属性是一个指针,指向这个拥有arguments对象的函数。

1
2
3
4
5
6
7
8
//阶乘函数
function factorial(num){
if(num < 1){
return 1;
}else{
return num * factorial(num - 1);
}
}

函数有名字,函数的执行与函数名factorial紧紧耦合在一起,使用arguments.callee可消除这种耦合。

1
2
3
4
5
6
7
function factorial(num){
if(num < 1){
return 1;
}else{
return num * arguments.callee(num - 1);
}
}

this对象

this是一个完全根据调用点(函数在代码中被调用的位置而不是被声明的位置)而为每次函数调用建立的绑定。
this引用的是函数执行的环境对象,即调用函数的那个对象
参见《你不懂JS: this 与对象原型》第二章: this 豁然开朗!

this的四种绑定方式

默认绑定

1
2
3
4
5
6
// 1 默认绑定全局变量
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 非严格模式下:2; 严格模式下:Cannot read property 'a' of undefined

我们考察调用点来看看foo()是如何被调用的。在我们的代码段中,foo()是被一个直白的,毫无修饰的函数引用调用的。没有其他的我们将要展示的规则适用于这里,所以默认绑定在这里适用。如果strict mode在这里生效,那么对于默认绑定来说全局对象是不合法的,所以this将被设置为undefined

隐含绑定

1
2
3
4
5
6
7
8
9
10
11
// 2 obj 对象在函数被调用的时间点上“拥有”或“包含”这个 函数引用
function foo() {
console.log(this.a);
}

var obj = {
a: 2,
foo: foo
};

obj.foo(); // 2

调用点使用obj环境来引用函数,所以可以说obj对象在函数被调用的时间点上“拥有”或“包含”这个函数引用。
foo()被调用的位置上,它被冠以一个指向obj的对象引用。当一个方法引用存在一个环境对象时,隐含绑定规则会说:是这个对象应当被用于这个函数调用的this绑定。
注意foo()被声明然后作为引用属性添加到obj上的方式。无论foo()是否一开始就在obj上被声明,还是后来作为引用添加(如上面代码所示),这个函数都不被obj所真正“拥有”或“包含”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 3 对象属性引用链的最后一层是影响调用点的
function foo() {
console.log(this.a);
}

var obj2 = {
a: 42,
foo: foo
};

var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42

只有对象属性引用链的最后一层是影响调用点的。

隐含丢失
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 4 隐含丢失
function foo() {
console.log(this.a);
}

var obj = {
a: 2,
foo: foo
};

var bar = obj.foo; // 函数引用!

var a = "oops, global"; // `a` 也是一个全局对象的属性

bar(); // "oops, global"

尽管bar似乎是obj.foo的引用,但实际上它只是另一个foo本身的引用而已。另外,起作用的调用点是bar(),一个直白毫无修饰的调用,因此默认绑定适用于这里。

1
2
3
4
5
6
7
8
9
10
11
//等价于
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数引用!
var a = "oops, global"; // `a` 也是一个全局对象的属性
foo(); // "oops, global"

当我们考虑传递一个回调函数时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo() {
console.log( this.a );
}

function doFoo(fn) {
// `fn` 只不过 `foo` 的另一个引用

fn(); // <-- 调用点!
}

var obj = {
a: 2,
foo: foo
};

var a = "oops, global"; // `a` 也是一个全局对象的属性

doFoo( obj.foo ); // "oops, global"

参数传递仅仅是一种隐含的赋值,而且因为我们在传递一个函数,它是一个隐含的引用赋值,所以最终结果和我们前一个代码段一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//等价于
function foo() {
console.log( this.a );
}

function doFoo(fn) {
// `fn` 只不过 `foo` 的另一个引用

fn(); // <-- 调用点!
}

var obj = {
a: 2,
foo: foo
};

var a = "oops, global"; // `a` 也是一个全局对象的属性

doFoo( function foo() {
console.log( this.a );// "oops, global"
});

那么如果接收你所传递回调的函数不是你的,而是语言内建的呢?没有区别,同样的结果。

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log( this.a );
}

var obj = {
a: 2,
foo: foo
};

var a = "oops, global"; // `a` 也是一个全局对象的属性

setTimeout( obj.foo, 100 ); // "oops, global"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//等价于
function foo() {
console.log( this.a );
}

var obj = {
a: 2,
foo: foo
};

var a = "oops, global"; // `a` 也是一个全局对象的属性

setTimeout( function foo() {
console.log( this.a );// "oops, global"
}, 100 );

明确绑定

JavaScript语言中的“所有”函数都有一些工具(通过他们的[[Prototype]])可以用于这个任务。具体地说,函数拥有call(..)apply(..)方法。
它们接收的第一个参数都是一个用于this的对象,之后使用这个指定的this来调用函数。因为你已经直接指明你想让this是什么,所以我们称这种方式为明确绑定。

1
2
3
4
5
6
7
8
9
10
function foo() {
console.log( this.a );
}

var obj = {
a: 2
};

foo.call( obj ); // 2
// 通过 foo.call(..) 使用 明确绑定 来调用 foo,允许我们强制函数的 this 指向 obj。

硬绑定

单独依靠明确绑定仍然b不能解决函数“丢失”自己原本的this绑定,或者被第三方框架覆盖,等等问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo() {
console.log( this.a );
}

var obj = {
a: 2
};

var bar = function() {
foo.call( obj );
};

bar(); // 2
setTimeout( bar, 100 ); // 2

// `bar` 将 `foo` 的 `this` 硬绑定到 `obj`
// 所以它不可以被覆盖
bar.call( window ); // 2

由于硬绑定是一个如此常用的模式,它已作为ES5的内建工具提供:Function.prototype.bind

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(something) {
console.log( this.a, something );
return this.a + something;
}

var obj = {
a: 2
};

var bar = foo.bind( obj );

var b = bar( 3 ); // 2 3
console.log( b ); // 5

bind(..)返回一个硬编码的新函数,它使用你指定的this环境来调用原本的函数。

注意:在 ES6 中,bind(..)生成的硬绑定函数有一个名为.name的属性,它源自于原始的目标函数(target function)。举例来说:bar = foo.bind(..)应该会有一个bar.name属性,它的值为"bound foo",这个值应当会显示在调用栈轨迹的函数调用名称中。

new绑定

首先,让我们重新定义JavaScript的“构造器”是什么。在JS中,构造器仅仅是一个函数,它们偶然地与前置的new操作符一起调用。它们不依附于类,它们也不初始化一个类。它们甚至不是一种特殊的函数类型。它们本质上只是一般的函数,在被使用new来调用时改变了行为。
实际上不存在“构造器函数”这样的东西,而只有函数的构造器调用。

当在函数前面被加入new调用时,也就是构造器调用时,下面这些事情会自动完成:

  1. 一个全新的对象会凭空创建(就是被构建)
  2. 这个新构建的对象会被接入原形链([[Prototype]]-linked
  3. 函数调用的this绑定到这个新构建的对象
  4. 除非函数返回一个它自己的其他对象,否则这个被new调用的函数将自动返回这个新构建的对象。
1
2
3
4
5
6
function foo(a) {
this.a = a;
}

var bar = new foo( 2 );
console.log( bar.a ); // 2

通过在前面使用new来调用foo(..),我们构建了一个新的对象并把这个新对象作为foo(..)调用的this

四种绑定方式的优先级

默认绑定在四种规则中优先级最低的。

隐含绑定和明确绑定优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function foo() {
console.log( this.a );
}

var obj1 = {
a: 2,
foo: foo
};

var obj2 = {
a: 3,
foo: foo
};

obj1.foo(); // 2
obj2.foo(); // 3

obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2

明确绑定的优先级要高于隐含绑定,这意味着你应当在考察隐含绑定之前首先考察明确绑定是否适用。

new绑定和明确绑定、隐含绑定的优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function foo(something) {
this.a = something;
}

var obj1 = {
foo: foo
};

var obj2 = {};

obj1.foo( 2 );
console.log( obj1.a ); // 2

obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3

var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4

new绑定的优先级要高于隐含绑定

注意:newcall/apply不能同时使用,所以new foo.call(obj1)是不允许的,也就是不能直接对比测试new绑定和明确绑定。但是我们依然可以使用硬绑定来测试这两个规则的优先级。
在我们进入代码中探索之前,回想一下硬绑定物理上是如何工作的,也就是Function.prototype.bind(..)创建了一个新的包装函数,这个函数被硬编码为忽略它自己的this绑定(不管它是什么),转而手动使用我们提供的。那么似乎硬绑定(明确绑定的一种)的优先级要比new绑定高,而且不能被new覆盖。

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(something) {
this.a = something;
}

var obj1 = {};

var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2

var baz = new bar( 3 );
console.log( obj1.a ); // 2
console.log( baz.a ); // 3

bar是硬绑定到obj1的,但是new bar(3)并没有像我们期待的那样将obj1.a变为3。反而,硬绑定(到obj1)的bar(..)调用可以被new所覆盖。使用new创建了一个名为baz的新创建的对象,而且我们确实看到baz.a的值为3。

结论

现在,我们可以按照优先顺序来总结一下从函数调用的调用点来判定this的规则了。按照这个顺序来问问题,然后在第一个规则适用的地方停下。

  1. 函数是通过new被调用的吗(new绑定)?如果是,this就是新构建的对象。

    1
    var bar = new foo()
  2. 函数是通过callapply被调用(明确绑定),甚至是隐藏在bind硬绑定之中吗?如果是,this就是那个被明确指定的对象。

    1
    var bar = foo.call( obj2 )
  3. 函数是通过环境对象(也称为拥有者或容器对象)被调用的吗(隐含绑定)?如果是,this就是那个环境对象。

    1
    var bar = obj1.foo()
  4. 否则,使用默认的this(默认绑定)。如果在strict mode下,就是undefined,否则是global对象。

    1
    var bar = foo()

以上,就是理解对于普通的函数调用来说的this绑定规则。是的……几乎是全部。

例外

被忽略的this

如果你传递nullundefined作为callapplybindthis绑定参数,那么这些值会被忽略掉,取而代之的是默认绑定规则将适用于这个调用。
一个很常见的做法是,使用apply(..)来将一个数组散开,从而作为函数调用的参数。相似地,bind(..)可以柯里化参数(预设值),也可能非常有用。

1
2
3
4
5
6
7
8
9
10
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}

// 将数组散开作为参数
foo.apply( null, [2, 3] ); // a:2, b:3

// 用 `bind(..)` 进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3

注意:ES6中有一个扩散操作符:...,它让你无需使用apply(..)而在语法上将一个数组“散开”作为参数,比如foo(...[1,2])表示foo(1,2)——如果this绑定没有必要,可以在语法上回避它。不幸的是,柯里化在ES6中没有语法上的替代品,所以bind(..)调用的this参数依然需要注意。

可是,在你不关心this绑定而一直使用null的时候,有些潜在的“危险”。如果你这样处理一些函数调用(比如,不归你管控的第三方包),而且那些函数确实使用了this引用,那么默认绑定规则意味着它可能会不经意间引用(或者改变,更糟糕!)global对象(在浏览器中是window)。

更安全的this

也许某些“更安全”的做法是:为了this而传递一个特殊创建好的对象,这个对象保证不会对你的程序产生副作用。从网络学(或军事)上借用一个词,我们可以建立一个“DMZ”(非军事区)对象——只不过是一个完全为空,没有委托的对象。

如果我们为了忽略自己认为不用关心的this绑定,而总是传递一个DMZ对象,那么我们就可以确定任何对this的隐藏或意外的使用将会被限制在这个空对象中,也就是说这个对象将global对象和副作用隔离开来。
创建完全为空的对象的最简单方法就是Object.create(null)Object.create(null){}很相似,但是没有指向Object.prototype的委托,所以它比{}“空得更彻底”

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}

// 我们的 DMZ 空对象
var ø = Object.create( null );

// 将数组散开作为参数
foo.apply( ø, [2, 3] ); // a:2, b:3

// 用 `bind(..)` 进行 currying
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3

间接引用

另外一个要注意的是,你可以(有意或无意地!)创建对函数的“间接引用(indirect reference)”,在那样的情况下,当那个函数引用被调用时,默认绑定规则也会适用。
一个最常见的间接引用产生方式是通过赋值:

1
2
3
4
5
6
7
8
9
10
11
function foo() {
console.log( this.a );
}

var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };

o.foo(); // 3
//将o.foo函数赋值给p.foo函数,然后立即执行。相当于仅仅是foo()函数的立即执行
(p.foo = o.foo)(); // 2

赋值表达式p.foo = o.foo的结果值是一个刚好指向底层函数对象的引用。如此,起作用的调用点就是foo(),而非你期待的p.foo()o.foo()。根据上面的规则,默认绑定适用。

1
2
3
4
5
6
7
8
9
10
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
//将o.foo函数赋值给p.foo函数,之后p.foo函数再执行,是属于p对象的foo函数的执行
p.foo = o.foo;
p.foo();//4

提醒:无论如何得到适用默认绑定的函数调用,被调用函数的内容的strict mode状态——而非函数的调用点——决定了this引用的值:不是global对象(在非strict mode下),就是undefined(在strict mode下)。

软绑定(Softening Binding)

硬绑定是一种通过将函数强制绑定到特定的this上,来防止函数调用在不经意间退回到默认绑定的策略(除非你用new去覆盖它!)。问题是,硬绑定极大地降低了函数的灵活性,阻止我们手动使用隐含绑定或后续的明确绑定来覆盖this
如果有这样的办法就好了:为默认绑定提供不同的默认值(不是globalundefined),同时保持函数可以通过隐含绑定或明确绑定技术来手动绑定this

我们可以构建一个所谓的软绑定工具来模拟我们期望的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this,
curried = [].slice.call( arguments, 1 ),
bound = function bound() {
return fn.apply(
(!this ||
(typeof window !== "undefined" &&
this === window) ||
(typeof global !== "undefined" &&
this === global)
) ? obj : this,
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}

这里提供的softBind(..)工具的工作方式和ES5内建的bind(..)工具很相似,除了我们的软绑定行为。它用一种逻辑将指定的函数包装起来,这个逻辑在函数调用时检查this,如果它是globalundefined,就使用预先指定的默认值(obj),否则保持this不变。它也提供了可选的柯里化行为(见先前的bind(..)讨论)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo() {
console.log("name: " + this.name);
}

var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };

var fooOBJ = foo.softBind( obj );

fooOBJ(); // name: obj

obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!

fooOBJ.call( obj3 ); // name: obj3 <---- 看!

setTimeout( obj2.foo, 10 ); // name: obj <---- 退回到软绑定

软绑定版本的foo()函数可以如展示的那样被手动this绑定到obj2obj3,如果默认绑定适用时会退到obj

this词法–ES6箭头函数

箭头函数不是通过function关键字声明的,而是通过所谓的“大箭头”操作符:=>。与使用四种标准的this规则不同的是,箭头函数从封闭它的(函数或全局)作用域采用this绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo() {
// 返回一个箭头函数
return (a) => {
// 这里的 `this` 是词法上从 `foo()` 采用的
console.log( this.a );
};
}

var obj1 = {
a: 2
};

var obj2 = {
a: 3
};

var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是3!

foo()中创建的箭头函数在词法上捕获foo()被调用时的this,不管它是什么。因为foo()this绑定到obj1bar(被返回的箭头函数的一个引用)也将会被this绑定到obj1一个箭头函数的词法绑定是不能被覆盖的(就连new也不行!)。

最常见的用法是用于回调,比如事件处理器或计时器:

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
setTimeout(() => {
// 这里的 `this` 是词法上从 `foo()` 采用
console.log( this.a );
},100);
}

var obj = {
a: 2
};

foo.call( obj ); // 2

虽然箭头函数提供除了使用bind(..)外,另外一种在函数上来确保this的方式,这看起来很吸引人,但重要的是要注意它们本质是使用广为人知的词法作用域来禁止了传统的this机制。在ES6之前,我们已经有了相当常用的模式,这些模式几乎和ES6的箭头函数的精神没有区别:

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
var self = this; // 词法上捕获 `this`
setTimeout( function(){
console.log( self.a );
}, 100 );
}

var obj = {
a: 2
};

foo.call( obj ); // 2

虽然对不想用bind(..)的人来说self = this和箭头函数都是看起来不错的“解决方案”,但它们实质上逃避了this而非理解和接受它。

如果你发现你在写this风格的代码,但是大多数或全部时候,你都用词法上的self = this或箭头函数“技巧”抵御this机制,那么也许你应该:

  1. 仅使用词法作用域并忘掉虚伪的this风格代码。
  2. 完全接受this风格机制,包括在必要的时候使用bind(..),并尝试避开self = this和箭头函数的“词法this”技巧。

一个程序可以有效地同时利用两种风格的代码(词法和this),但是在同一个函数内部,特别是对同种类型的查找,混合这两种机制通常是自找麻烦,而且很难维护。

javascript如此复杂的原因是因为函数过于强大。因为,函数是对象,所以原型链比较复杂;因为函数可以作为值被传递,所以执行环境栈比较复杂;同样地,因为函数具有多种调用方式,所以this的绑定规则也比较复杂。只有理解了函数,才算理解了javascript

五、prototype属性

六、变量提升

变量提升(Hoisting)被认为是思考执行上下文(特别是创建和执行阶段)在JavaScript中如何工作的一般方式。但变量提升(Hoisting)可能会导致误解。例如,提升教导变量和函数声明被物理移动到编码的顶部,这不算什么。真正发生的什么是在编译阶段将变量和函数声明放入内存中,但仍然保留在编码中键入的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 不推荐的方式:先调用函数,再声明函数
*/

catName("Chloe");

function catName(name) {
console.log("My cat's name is " + name);
}

/*
The result of the code above is: "My cat's name is Chloe"
*/

// 等价于

/*函数声明提升*/
function catName(name) {
console.log("My cat's name is " + name);
}

catName("Tigger");

/*
The result of the code above is: "My cat's name is Chloe"
*/

即使我们先在代码中调用函数,在写该函数之前,代码仍然可以工作。

Hoisting 也适用于其他数据类型和变量。变量可以在声明之前进行初始化和使用。但是如果没有初始化,就不能使用。
JavaScript 仅提升声明,而不是初始化。如果你使用的是在使用后声明和初始化的一个变量,那么该值将是 undefined。以下两个示例演示了相同的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var x = 1; // 声明 + 初始化 x

console.log(x + " " + y); // y 是未定义的

var y = 2;// 声明 + 初始化 y


//上面的代码和下面的代码是一样的

var x = 1; // 声明 + 初始化 x

var y; //声明 y

console.log(x + " " + y); //y 是未定义的

y = 2; // 初始化 y

ES6 : let 不存在 Hoisting

七、检测类型

  1. 检测基本数据类型:typeof

    1
    2
    3
    4
    5
    6
    var str = 'zxlg'; console.log(typeof str); // string
    var num = 9; console.log(typeof num);// number
    var bool = true; console.log(typeof bool);// boolean
    var u; console.log(typeof u);// undefined
    var nul = null; console.log(typeof nul);// object
    var obj = new Object(); console.log(typeof obj);// object
  2. 检测引用类型:instanceof
    typeof检测所有引用类型均为object,想得到何种引用类型可使用instanceof


(2017.8.7新增)
检测一个引用类型值和Object构造函数时,instanceof操作符始终会返回true
如果使用instanceof操作符检测基本类型的值,则该操作符始终会返回false,因为基本类型都不是对象。

1
2
3
4
5
6
var arr = [1,2,3];
console.log(arr instanceof Object); // true
console.log(arr instanceof Array); // true
var reg = /\d+/;
console.log(arr instanceof Array); // false
console.log(arr instanceof RegExp); // true

参考文献