var obj = {
id: "awesome",
cool: function coolFn() {
console.log( this.id );
}
};
var id = "not awesome";
obj.cool(); // awesome
setTimeout( obj.cool, 100 ); // not awesome
- 关于闭包
for (var i=1; i<=5; i++) {
(function() {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})();
} // 输出全是6
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})();
}
- 关于this
function foo(num) {
console.log( "foo: " + num );
// 记录 foo 被调用的次数
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( foo.count ); // 0 -- 什么?!
在执行 foo.count=0
时,的确想函数对象foo
添加了一个属性,但是函数内部代码this.count
中this
并不是执行那个函数对象,虽然属性名相同,跟对象却并不相同,如果此时打印this.count
它为NaN。
这里我们可以看到,需要从函数对象内部引用它自身,只使用
this
是不够的,一般来说,需通过一个指向函数对象的词法标识符(变量)来引用它。因此,此时我们可以通过强制this指向foo函数对象来解决这个问题:
function foo(num) {
console.log( "foo: " + num );
// 记录 foo 被调用的次数
// 注意,在当前的调用方式下(参见下方代码),this 确实指向 foo
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
// 使用 call(..) 可以确保 this 指向函数对象 foo 本身
foo.call( foo, i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( foo.count ); // 4
在 JavaScript 内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript代码访问,它存在于 JavaScript 引擎内部。
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); // ReferenceError: a is not defined
this
实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。foo
在外部被调用,此时this
指向window
,bar
在window
环境下,但是window
中没有变量a
,所以报错。
- 关于
this
运行机制
this
运行时绑定,不是编写时绑定,它的上下文取决于函数调用时的各种条件,this
的绑定方式和函数声明位置没有关系,只取决于函数调用方式,当一个函数被调用时会创建一个活动记录(执行上下文),包含函数在哪里被调用、调用方式、传入的参数信息等。this
就是这个记录的一个属性,会在函数执行的过程中用到。
绑定方式:
I. 默认绑定
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
这里需要注意只有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象;则不能将全局对象用于默认绑定,因此 this 会绑定到undefined :
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined
这里有一个微妙但是非常重要的细节,虽然 this 的绑定规则完全取决于调用位置,但是只
有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象;在严格模式下调用
foo() 则不影响默认绑定:
function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})();
II. 隐式绑定
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。
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() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
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"
相当于
function doFoo() {
console.log( this.a );
}
var a = "oops, global"; // a 是全局对象的属性
doFoo( ); // "oops, global"
回调函数丢失 this 绑定是非常常见的。除此之外,还有一种情况 this 的行为会出乎我们意料:调用回调函数的函数可能会修改 this 。在一些流行的JavaScript 库中事件处理器常会把回调函数的 this 强制绑定到触发事件的 DOM 元素上。
III. 显式绑定
可以使用函数的 call(..) 和apply(..) 方法:它们的第一个参数是一个对象,是给 this 准备的,接着在调用函数时将其绑定到 this 。如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new String(..) 、 new Boolean(..) 或者new Number(..) )。这通常被称为“装箱”。
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 调用 foo(..) 时把 this 绑定到 obj
[1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome
VI. new 绑定
想回忆一下使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
a. 创建(或者说构造)一个全新的对象。
b. 这个新对象会被执行 [[Prototype]] 连接。
c. 这个新对象会绑定到函数调用的 this 。
d. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
V. 绑定例外
如果你把 null 或者 undefined 作为 this 的绑定对象传入 call 、 apply 或者 bind ,这些值
在调用时会被忽略,实际应用的是默认绑定规则:
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2
创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。
对于默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式, this 会被绑定到 undefined ,否则this 会被绑定到全局对象。
判断 this
现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的
顺序来进行判断:
a. 函数是否在 new
中调用( new
绑定)?如果是的话 this
绑定的是新创建的对象。
var bar = new foo()
b. 函数是否通过 call
、 apply
(显式绑定)或者硬绑定调用?如果是的话, this
绑定的是
指定的对象。
var bar = foo.call(obj2)
c. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话, this
绑定的是那个上
下文对象。
var bar = obj1.foo()
d. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined
,否则绑定
到全局对象。
var bar = foo()
- 关于箭头函数的this
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 !
这里我们看到,箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this 。
对比普通函数
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
// 捕获所有 curried 参数
var curried = [].slice.call( arguments, 1 );
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this,
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}
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
上面的代码给默认绑定指定一个全局对象和 undefined 以外的值,实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。
- 关于类型判断
var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
var strObject = new String( "I am a string" );
typeof strObject; // "object"
strObject instanceof String; // true
// 检查 sub-type 对象
Object.prototype.toString.call( strObject ); // [object String]