在 js 中,this 这个上下文总是变化莫测,很多时候出现 bug 总是一头雾水,其实,只要分清楚不同的情况下如何执行就 ok 了。
全局执行
首先,我们在全局环境中看看它的 this 是什么:
first. 浏览器:
console.log(this);
// Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage, sessionStorage: Storage, webkitStorageInfo: DeprecatedStorageInfo…}
可以看到打印出了 window 对象;
second. node:
console.log(this);
// global
可以看到打印出了 global 对象;
总结:在全局作用域中它的 this 执行当前的全局对象(浏览器端是 Window,node 中是 global)。
函数中执行
纯粹的函数调用
这是最普通的函数使用方法了:
function test() {
console.log(this);
};
test();
// Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage, sessionStorage: Storage, webkitStorageInfo: DeprecatedStorageInfo…}
我们可以看到,一个函数被直接调用的时候,属于全局调用,这时候它的 this 指向 全局对象;
严格模式 ‘use strict’;
如果在严格模式的情况下执行纯粹的函数调用,那么这里的的 this 并不会指向全局,而是 undefined,这样的做法是为了消除 js 中一些不严谨的行为:
'use strict';
function test() {
console.log(this);
};
test();
// undefined
当然,把它放在一个立即执行函数中会更好,避免了污染全局:
(function (){
"use strict";
console.log(this);
})();
// undefined
作为对象的方法调用
当一个函数被当作一个对象的方法调用的时候:
var obj = {
name: 'qiutc',
foo: function() {
console.log(this.name);
}
}
obj.foo();
// 'qiutc'
这时候,this 指向当前的这个对象;
当然,我们还可以这么做:
function test() {
console.log(this.name);
}
var obj = {
name: 'qiutc',
foo: test
}
obj.foo();
// 'qiutc'
同样不变,因为在 js 中一切都是对象,函数也是一个对象,对于 test ,它只是一个函数名,函数的引用,它指向这个函数,当 foo = test,foo 同样也指向了这个函数。
如果把对象的方法赋值给一个变量,然后直接调用这个变量呢:
var obj = {
name: 'qiutc',
foo: function() {
console.log(this);
}
}
var test = obj.foo;
test();
// Window
可以看到,这时候 this 执行了全局,当我们把 test = obj.foo ,test 直接指向了一个函数的引用,这时候,其实和 obj 这个对象没有关系了,所以,它是被当作一个普通函数来直接调用,因此,this 指向全局对象。
一些坑
我们经常在回调函数里面会遇到一些坑:
var obj = {
name: 'qiutc',
foo: function() {
console.log(this);
},
foo2: function() {
console.log(this); //Object {name: "qiutc"...}
setTimeout(this.foo, 1000); // window 对象
}
}
obj.foo2();
执行这段代码我们会发现两次打印出来的 this 是不一样的:
关于setTimeout的this指向:https://www.talkingcoder.com/article/6356947525374513523
第一次是 foo2 中直接打印 this,这里指向 obj 这个对象,我们毋庸置疑;
但是在 setTimeout 中执行的 this.foo,却指向了全局对象,这里不是把它当作函数的方法使用吗?这一点经常让很多初学者疑惑;
其实,setTimeout 也只是一个函数而已,函数必然有可能需要参数,我们把 this.foo 当作一个参数传给 setTimeout 这个函数,就像它需要一个 fun 参数,在传入参数的时候,其实做了个这样的操作 fun = this.foo,看到没有,这里我们直接把 fun 指向 this.foo 的引用;执行的时候其实是执行了 fun() 所以已经和 obj 无关了,它是被当作普通函数直接调用的,因此 this 指向全局对象。
这个问题是很多异步回调函数中普遍会碰到的;
解决
为了解决这个问题,我们可以利用 闭包 的特性来处理:
var obj = {
name: 'qiutc',
foo: function() {
console.log(this);
},
foo2: function() {
console.log(this);
var _this = this;
setTimeout(function() {
console.log(this); // Window
console.log(_this); // Object {name: "qiutc"}
}, 1000);
}
}
obj.foo2();
可以看到直接用 this 仍然是 Window;因为 foo2 中的 this 是指向 obj,我们可以先用一个变量 _this 来储存,然后在回调函数中使用 _this,就可以指向当前的这个对象了;
setTimeout 的另一个坑
之前啊说过,如果直接执行回调函数而没有绑定作用域,那么它的 this 是指向全局对象(window),在严格模式下会指向 undefined,然而在 setTimeout 中的回调函数在严格模式下却表现出不同:
'use strict';
function foo() {
console.log(this);
}
setTimeout(foo, 1);
// window
按理说我们加了严格模式,foo 调用也没有指定 this,应该是出来 undefined,但是这里仍然出现了全局对象,难道是严格模式失效了吗?
并不,即使在严格模式下,setTimeout 方法在调用传入函数的时候,如果这个函数没有指定了的 this,那么它会做一个隐式的操作—-自动地注入全局上下文,等同于调用 foo.apply(window) 而非 foo();
当然,如果我们在传入函数的时候已经指定 this,那么就不会被注入全局对象,比如: setTimeout(foo.bind(obj), 1);;
http://stackoverflow.com/questions/21957030/why-is-window-still-defined-in-this-strict-mode-code
作为一个构造函数使用
在 js 中,为了实现类,我们需要定义一些构造函数,在调用一个构造函数的时候需要加上 new 这个关键字:
function Person(name) {
this.name = name;
console.log(this);
}
var p = new Person('qiutc');
// Person {name: "qiutc"}
我们可以看到当作构造函数调用时,this 指向了这个构造函数调用时候实例化出来的对象;
当然,构造函数其实也是一个函数,如果我们把它当作一个普通函数执行,这个 this 仍然执行全局:
function Person(name) {
this.name = name;
console.log(this);
}
var p = Person('qiutc');
// Window
其区别在于,如何调用函数(new)。
箭头函数
在 ES6 的新规范中,加入了箭头函数,它和普通函数最不一样的一点就是 this 的指向了,还记得在上文中(作为对象的方法调用-一些坑-解决)我们使用闭包来解决 this 的指向问题吗,如果用上了箭头函数就可以更完美的解决了:
var obj = {
name: 'qiutc',
foo: function() {
console.log(this);
},
foo2: function() {
console.log(this);
setTimeout(() => {
console.log(this); // Object {name: "qiutc"}
}, 1000);
}
}
obj.foo2();
可以看到,在 setTimeout 执行的函数中,本应该打印出在 Window,但是在这里 this 却指向了 obj,原因就在于,给 setTimeout 传入的函数(参数)是一个箭头函数:
函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
根据例子我们理解一下这句话:
在 obj.foo2() 执行的时候,当前的 this 指向 obj;在执行 setTimeout 时候,我们先是定义了一个匿名的箭头函数,关键点就在这,箭头函数内的 this 执行定义时所在的对象,就是指向定义这个箭头函数时作用域内的 this,也就是 obj.foo2 中的 this,即 obj;所以在执行箭头函数的时候,它的 this -> obj.foo2 中的 this -> obj;
简单来说, 箭头函数中的 this 只和定义它时候的作用域的 this 有关,而与在哪里以及如何调用它无关,同时它的 this 指向是不可改变的。
call, apply, bind
在 js 中,函数也是对象,同样也有一些方法,这里我们介绍三个方法,他们可以更改函数中的 this 指向:
- call
fun.call(thisArg[, arg1[, arg2[, …]]])
它会立即执行函数,第一个参数是指定执行函数中 this 的上下文,后面的参数是执行函数需要传入的参数;
- apply
fun.apply(thisArg[, [arg1, arg2, …]])
它会立即执行函数,第一个参数是指定执行函数中 this 的上下文,第二个参数是一个数组,是传给执行函数的参数(与 call 的区别);
- bind
var foo = fun.bind(thisArg[, arg1[, arg2[, …]]]);
它不会执行函数,而是返回一个新的函数,这个新的函数被指定了 this 的上下文,后面的参数是执行函数需要传入的参数;
这三个函数其实大同小异,总的目的就是去指定一个函数的上下文(this),我们以 call 函数为例;
为一个普通函数指定 this
var obj = {
name: 'qiutc'
};
function foo() {
console.log(this);
}
foo.call(obj);
// Object {name: "qiutc"}
可以看到,在执行 foo.call(obj) 的时候,函数内的 this 指向了 obj 这个对象,成功;
为对象中的方法指定一个 this
var obj = {
name: 'qiutc',
foo: function () {
console.log(this);
}
}
var obj2 = {
name: 'tcqiu222222'
};
obj.foo.call(obj2);
// Object {name: "tcqiu222222"}
可以看到,执行函数的时候这里的 this 指向了 obj2,成功;
为构造函数指定 this
function Person(name) {
this.name = name;
console.log(this);
}
var obj = {
name: 'qiutc2222222'
};
var p = new Person.call(obj, 'qiutc');
// Uncaught TypeError: Person.call is not a constructor(…)
这里报了个错,原因是我们去 new 了 Person.call 函数,而非 Person ,这里的函数不是一个构造函数;
换成 bind 试试:
function Person(name) {
this.name = name;
console.log(this);
}
var obj = {
name: 'qiutc2222222'
};
var Person2 = Person.bind(obj);
var p = new Person2('qiutc');
// Person {name: "qiutc"}
console.log(obj);
// Object {name: "qiutc2222222"}
打印出来的是 Person 实例化出来的对象,而和 obj 没有关系,而 obj 也没有发生变化,说明,我们给 Person 指定 this 上下文并没有生效;
因此可以得出: 使用 bind 给一个构造函数指定 this,在 new 这个构造函数的时候,bind 函数所指定的 this 并不会生效;
当然 bind 不仅可以指定 this ,还能传入参数,我们来试试这个操作:
function Person(name) {
this.name = name;
console.log(this);
}
var obj = {
name: 'qiutc2222222'
};
var Person2 = Person.bind(obj, 'qiutc111111');
var p = new Person2('qiutc');
// Person {name: "qiutc111111"}
可以看到,虽然指定 this 不起作用,但是传入参数还是起作用了;
为箭头函数指定 this
我们来定义一个全局下的箭头函数,因此这个箭头函数中的 this 必然会指向全局对象,如果用 call 方法改变 this 呢:
var afoo = (a) => {
console.log(a);
console.log(this);
}
afoo(1);
// 1
// Window
var obj = {
name: 'qiutc'
};
afoo.call(obj, 2);
// 2
// Window
可以看到,这里的 call 指向 this 的操作并没有成功,所以可以得出: 箭头函数中的 this 在定义它的时候已经决定了(执行定义它的作用域中的 this),与如何调用以及在哪里调用它无关,包括 (call, apply, bind) 等操作都无法改变它的 this。
只要记住箭头函数大法好,不变的 this。
Function.prototype.call
- 格式:fx.call( thisArg [,arg1,arg2,… ] );
call的传参个数不限,第一个数表示调用函数(fx)函数体内this的指向.从第二个参数开始依次按序传入函数.
var age = 40;
var xiaoMing = {age:30
};
var xiaoLi = {age: 20
};
var getAge = function(){console.log(this.age);
};
getAge.call( xiaoMing ); //30 表示函数this指向xiaoMing
getAge.call(xiaoLi); //20 表示函数this指向xiaoLi
getAge.call(undefined);//40 getAge.call(undefined)==getAge.call(null)
getAge.call(null);//40
getAge(); //40
如果我们传入fx.call()的第一个参数数为null,那么表示函数fx体内this指向宿主对象,在浏览器是Window对象,这也解释了getAge.call(undefined);//40。
在此基础我们可以理解为 getAge()相当于getAge.call(null/undefined),扩展到所有函数,
fx()==fx.call(null) == fx.call(undefined)
值得注意的是严格模式下有点区别: this指向null
var getAge = function(){
'use strict'
console.log(this.age);
};
getAge(null);//报错 age未定义
再来理解this的使用
this的常用场景:
this位于一个对象的方法内,此时this指向该对象
var name = ‘window’;
var Student = {
name : 'kobe', getName: function () { console.log(this == Student); //true console.log(this.name); //kobe }
}
Student.getName();
var name = ‘window’;
var Student = {name : 'kobe', getName: function () { var name=100; console.log(this == Student); //true console.log(this.name); //kobe }
}
Student.getName(); //getName取得是Student 的name
this位于一个普通的函数内,表示this指向全局对象,(浏览器是window)
var name = ‘window’;
var getName = function () {
var name = 'kobe'; //迷惑性而已 return this.name;
}
console.log( getName() ); //window
this使用在构造函数(构造器)里面,表示this指向的是那个返回的对象.
var name = ‘window’;
//构造器
var Student = function () {this.name = 'student';
}
var s1 = new Student();
console.log(s1.name); //student
注意: 如果构造器返回的也是一个Object的对象(其他类型this指向不变遵循之前那个规律),这时候this指的是返回的这个Objec.
var name = 'window';
//构造器
var Student = function () {
this.name = 'student';
return {
name: 'boyStudent'
}
}
var s1 = new Student();
console.log(s1.name); //boyStudent
this指向失效问题
var name = ‘window’;
var Student = {
name : 'kobe', getName: function () { console.log(this.name); }
}
Student.getName(); // kobe
var s1 = Student.getName;
s1(); //window
原因: 此时s1是一个函数
function () {
console.log(this.name);
}
对一个基本的函数,前面提过this在基本函数中指的是window.
- 在开发中我们经常使用的this缓存法 ,缓存当前作用域下this到另外一个环境域下使用
最后理解apply的用法 Function.prototype.apply
格式: fx.apply(thisArg [,argArray] ); // 参数数组,argArray
- apply与call的作用是一样的,只是传参方式不同,
apply接受两个参数,第一个也是fx函数体内this的指向,用法与call第一个参数一致.第二个参数是数组或者类数组,apply就是把这个数组元素传入函数fx.
var add = function (a ,b ,c) {
console.log(a +b +c);
}
add.apply(null , [1,2,3]); // 6
再吃透这个题目就ok
var a=10;
var foo={
a:20,
bar:function(){
var a=30;
return this.a;
}
}
foo.bar()
//20
(foo.bar)()
//20
(foo.bar=foo.bar)()
//10
(foo.bar,foo.bar)()
//10
上题注解:
时刻牢记:作用域链查找遵循”就近原则”;
this谁调用就指向谁。
var a=10;
var foo={
a:20,
bar:function(){
var a=30; //this 指向 foo :console.log( this == foo) //true
return this.a;
}
}
foo.bar()
//20
// foo.bar() // foo调用,this指向foo , 此时的 this 指的是foo,所以是20
(foo.bar)()
//20
//第一步:
(function(){
var a=30;
return this.a;
})() //作用域链向上查找,this 指向外一层的对象foo
(foo.bar=foo.bar)()
//10
foo.bar=foo.bar,【睁大眼睛,是单等号赋值】就是普通的复制,一个匿名函数赋值给一个全局变量,你可以把右边的foo.bar换成b,
即(b = foo.bar)(),博客里面【this指向失效问题】说过普通的函数里面的this指向window,自然this.a == 10
(foo.bar,foo.bar)() //逗号表达式
//10
//(foo.bar,foo.bar)是一个小括号表达式,小括号表达式会依次创建两个匿名函数,并返回最后一个的匿名函数值,
(foo.bar,foo.bar) 得到的是这个函数
function(){
var a=30;
console.log( this == foo); //如果不是很了解this的指向就加这个代码进行检测
return this.a;
}
,这个是匿名函数,匿名函数的this指的是widnow,那么this.a = 10
this可谓是JavaScript中的开发神器,使用得当的话不仅有事半功倍的效果,而且代码的逼格也更高。但是既然是神器,如果你没有足够的功力的话,那么就不要使用它,否则就有可能自毁身亡。曾几何时,我偶然得到这个神器,之后,,,自残,,,自残,,,再自残...再自残了那么多次后,终于可以拥有强大功力持此神器行走江湖了。接下来,我就为大家来传授传说中神器的使用秘诀。
咳咳。。
入正题。
this是什么?this表示当前运行方法的主体。
注意:函数中的this指向和当前函数在哪定义的话或者在哪执行都没有任何的关系。为啥这样说,请仔细阅读下面的秘籍大全。
神器秘籍大全:
秘籍一:自制行函数里面的this永远都是window
var inner = "window";
var obj = {inner : "obj",
fn : (function () {console.log(this.inner)})()
}
上面浏览器在运行该程序时,会自动运行obj.fn里面的方法,因为obj.fn是一个自制行函数,当执行该函数时,程序会输出window。
额,为什么输出不是obj?
因为人家规定自制行函数里面的this是window,所以其实this.inner就是window.inner,因此这个inner是定义在全局变量的,它的值是"window"。
秘籍二:元素绑定事件驱动方法运行,方法里的this表示当前绑定的元素
var oDiv = document.getElementsByTagName("div")[0];
oDiv.onclick=function(){
console.log(this); //当用鼠标点击该元素,则输出oDiv元素的集合
};
这个好理解,元素绑定某个行为执行的方法,就相当于把这个方法也绑定在这个元素上,所以this也就指向元素本身。
秘籍三:方法执行,看方法名前面是否有".",有的话"."前面是谁this就是谁,没有的话this就是window
var obj={fn:fn};
function fn(){console.log(this)}
fn.prototype.aa=function(){console.log(this)};
var f=new fn;
fn(); //window..
obj.fn(); //Object..
fn.prototype.aa(); //fn.prototype
f.aa(); //f
记住此秘籍!!!
秘籍四:在构造函数模式中,函数体中的this是当前类的一个实例
function Fn(){
this.x = 100;
console.log(this); // 实例 f
}
var f = new Fn;
构造函数生成的实例,故构造函数里的this当然是指向当前这个实例了。
秘籍五(大招):call/apply来改变this的指向
var oDiv = document.getElementsByTagName("div")[0];
function fn() {
console.log(this);
}
fn.call(oDiv);
fn.call(oDiv); //执行这个语句后,fn里面的this指向oDiv元素,applay用法与call类似。
此大招一出来,上面四个秘籍都无效了。
五个典型的 JavaScript 面试题
问题1: 范围(Scope)
思考以下代码:
控制台(console)会打印出什么?
答案
上述代码会打印出5。
这个问题的陷阱就是,在立即执行函数表达式(IIFE)中,有两个命名,但是其中变量是通过关键词var来声明的。这就意味着a是这个函数的局部变量。与此相反,b是在全局作用域下的。
这个问题另一个陷阱就是,在函数中他没有使用”严格模式” (‘use strict’;)。如果 严格模式 开启,那么代码就会报出未捕获引用错误(Uncaught ReferenceError):b没有定义。记住,严格模式要求你在需要使用全局变量时,明确地引用该变量。因此,你需要像下面这么写:
问题2: 创建 “原生(native)” 方法
在String对象上定义一个repeatify函数。这个函数接受一个整数参数,来明确字符串需要重复几次。这个函数要求字符串重复指定的次数。举个例子:
应该打印出hellohellohello.
答案
一种可能的实现如下所示:
这个问题测试了开发人员对于JavaScript中继承的掌握,以及prototype这个属性。这也验证了开发人员是否有能力扩展原生数据类型的功能(虽然不应该这么做)。
这个问题的另一个重点是验证你是否意识到并知道如何避免覆盖已经存在的函数。这可以通过在自定义函数之前判断该函数是否存在来做到。
当你需要为旧浏览器实现向后兼容的函数时,这一技巧十分有用。
问题3: 变量提升(Hoisting)
执行以下代码会有什么结果?为什么?
答案
这段代码的执行结果是 undefined 和 2。
这个结果的原因是,变量和函数都被提升了(hoisted)。因此,在a被打印的时候,它已经在函数作用域中存在(即它已经被声明了),但是它的值依然是 undefined。换言之,上述代码和以下代码是等价的。
问题4: this 在 JavaScript 中是如何工作的
以下代码的结果是什么?请解释你的答案。
答案
上面的代码打印出 Aurelio De Rosa 和 John Doe。原因是在 JavaScript 中,一个函数的上下文环境,也就是this关键词所引用对象,是依赖于函数是如何被调用的,而不是依赖于函数如何被定义的。
在第一个 console.log() 调用中, getFullname() 是作为 obj.prop 的函数被调用的。因此,这里的上下文环境指向后者并且函数返回this对象的 fullname 属性。相反,当 getFullname() 被赋为test变量的值时,那个语境指向全局对象(window)。这是因为,test被隐式设置为全局对象的属性。因此,函数调用返回 window 的 fullname 属性值,在此段代码中,这个值是通过第一行赋值语句设置的。
问题5: call() 和 apply()
修复上一个问题,让最后一个 console.log() 打印出 Aurelio De Rosa。
答案
要解决这个问题,可以通过为函数 call() 或者 apply() 强制函数调用的上下文环境。如果你不知道 call() 和 apply() 之间的区别,我推荐阅读文章“ function.call 和 function.apply 之间有和区别?”。在以下代码中,我会用 call(),但是在这里,用 apply() 也可以获得相同的结果:
常见面试:
下面列举一些简单的实例 总结一下this的一些用法:
1.方法中的this会指向当前执行该方法的对象 如:
var name = "window"
var Tom = {
name:"Tom";
show:function(){alert(this.name)}
}
Tom.show(); //Tom
2.方法中的this不会指向声明它的对象 如下
var Bob={
name:"Bob",
show:function(){alert(this.name);}
};
var Tom={
name:"Tom",
show:Bob.show
};
Tom.show() ; //Tom
因为尽管alert(this.name)是在Bob对象环境中声明的
但该方法是由Tom对象调用执行所以this总是会指向当前执行的对象,而不是声明的对象
3.将方法复制给变量时,执行时仍然会以Tom对象区调用该方法
var name="window";
var Tom={
name:"Tom".
show:function(){alert(this.name)}
};
var fun=Tom.show();
fun(); //Tom
可以看出赋值后再调用,并不影响调用其方法的对象
4.将对象赋值给变量后,再调用方法,执行的对象仍然是Tom
var name="window";
var Tom={
name:"Tom",
show:function(){alert(this.name)},
wait:function(){
var that=this;
that.show();
}
};
Tom.wait(); //Tom
这里that赋值了当前执行的对象,并让它继续调用show,
所以show方法中alert(this.name)自然而然的指向了Tom
可以把上面的 "that赋值对象 然后调用方法" 这个过程看做成执行对象
的延迟,就是让Tom加班的意思
5.另一种 指明调用方法的对象 的办法 如下:
var name = "window";
var Bob= {
name:"Bob",
show:function(){alert(this.name);}
};
var Tom= {name: "Tom"};
Bob.show(); //Bob
Bob.show.apply(); //window
Bob.show.apply(Tom); //Tom
当然call()也差不多类似
6.下面来个特殊的例子
var name="window";
var Tom={
name:"Tom",
show:function(){alert(this.name)},
wait:function(){
var fun=this.show;
fun();
}
};
Tom.wait(); //window
上面也是赋值方法后,再调用,可是执行的对象却改成了window对象
解释:
在函数体内把方法赋值给变量再调用会导致对象更改为Window对象
执行fun时,可以看做是一种方法调用的延迟行为,延迟调用方法会使得执行的对象
变为全局对象也就是window对象
下面我们来看看其他几种延迟方式,导致对象被更改为window的例子
7.匿名函数的延迟
var name="window";
var Tom={
name:"Tom",
show:function(){alert(this.name)},
wait:function(){!function(call){call();}(this.show)}
}
Tom.wait(); //Window
8.setTimeout、setInterval函数延迟
这里只以setTimeout为例子
var name="window";
var Tom={
name:"Tom",
show:function(){alert(this.name)},
wait:function(){setTimeout(this.show,1000)}
}
Tom.wait(); //window
9. 在延迟的环境下 尝试让Tom加班(对象也跟着延迟)
var name="window";
var Tom={ name:"Tom",
show:function(){alert(this.name)},
wait:function(){setTimeout(Tom.show,1000)} }
Tom.wait(); //window
上面中this对象改成了Tom,尝试让Tom加班,但是结果仍然为Window对象
因为Tom.show放在第一个参数里,延迟的执行使得执行的对象变为window对象
而不再是Tom对象,如何让执行对象Tom在延迟当不被变更呢?下面给你答案
10.虽然延迟会导致方法的执行对象被更改为Window 但也有办法防止执行对象更改 如下
var name="window"
var Tom ={
name : "Tom",
show : function(){alert(this.name);},
wait: function(){
var that=this;
setTimeout(function(){that.show()},1000)}
}
Tom.wait(); //Tom
如果不能理解上面的代码,你就当做Tom对象也跟着函数一起延迟就好了
而第9个例子没有成功延迟,是因为没有变量保存对象使得执行对象没有跟着延迟
11.eval函数的延迟
对于eval比较特殊
在eval环境下,执行的对象就是当前作用域的对象 如下
var name="window";
var Bob={
name:"Bob",
showName: function(){ eval("alert(this.name)"); }
};
Bob.showName(); //Bob
12.eval函数的环境下,不会受到延迟而影响函数执行的对象
之所以eval特殊是因为eval不受延迟的影响
var name="window";
var that;
var Tom={
name:"Tom",
show:function(){alert(this.name)},
wait:function(){that=this;setTimeout("that.show()",1000)}
}
Tom.wait(); //Tom
也许你会觉得上面的代码没有eval函数的身影
其实setTimeout函数的第一个参数就是eval环境
他会指向当前执行作用域的执行对象,忽略延迟方法延迟调用
如果能把上面12个例子都理解了,那么this将成为你的一把有力的刀,挥舞在你代码中
当然如果不能理解,那么像闭包一样 尽量的少用!
函数是JavaScript世界里的第一公民,换句话来说,就是我们如果可以精通JavaScript函数的使用,那么对JavaScript的运用可以更游刃有余了。熟悉JavaScript的人应该都知道,同样的函数,以不同的方式调用的话,受影响最大的应该是 this 。下面我们来说说JavaScript函数的各种调用模式。
一、普通函数的调用模式
所谓普通函数的调用模式,也是JavaScript函数的最简单的一种调用模式,直接就是函数名后接一个 () 实现调用,看下面代码:
function func(){
console.log(this === window); //true
}
func();
上面代码,我们用function关键字声明了一个 func 函数,并且在函数体内打印 this===window,然后我们直接调用函数func,我们可以看到控制台是直接打印出 true ,也就是说,函数的这种普通调用模式,函数体内的 this 是指向全局环境 window 的。不清楚这点的同学,可以能会遇到这样的一个bug:
var color = 'gg';
var obj = {
color : 'red',
show : function(){
function func1(){
console.log(this.color); //gg
}
func1();
}
}
obj.show();
我们在全局环境下声明了一个变量 color 和一个对象 obj ,在对象 obj 里面我们还声明了一个 color 属性 为 ‘red’,一个 show 方法。而且在 show 方法里面呢,我们还声明了一个函数 func1 并且调用了 func1,func1 的作用是打印 this.color。最后我们运行代码 obj.show(); 调用obj里面的show方法。不清楚函数的普通调用模式的特点的同学可能会认为此时在控制台答应出来的会是 ‘red’ 。实际上此时在控制台答应出来的应该是 gg 。因为函数 func1 的调用模式是 普通函数调用模式(即使它是在 obj 的 show 方法里面调用的),所以此时函数体内的 this 是指向 全局环境window 的,所以就打印了全局环境下的变量 color 。
可能有些同学会问:如果我们希望 func1 函数打印出来的是 ‘red’ 呢,应该怎么改?其实很简单,因为 obj.color 才是 ‘red’ ,所以我们只需要把 指向 obj 的 this 引入到函数 func1 里面就行了:
var color = 'gg';
var obj = {
color : 'red',
show : function(){
var that = this;
function func1(){
console.log(that.color); //red
}
func1();
}
}
obj.show();
var color = 'gg';
var obj = {
color : 'red',
show : function(){
var func1=function(){
console.log(this.color); //red
}.bind(this);
func1();
}
}
obj.show();
在上面的代码中,因为 show 里面的 this 指向 obj 的,所以我们在 show 里面声明一个变量 that = this;用来把指向 obj 的 this 引入到 func1 中,然后再把 func1 函数体内的 this.color 改为 that.color ,此时在控制台打印出来的就是我们想要的 ‘red’ 了。
可能现在又有同学会问:为什么 show 里面的 this 是指向 obj 的呢?这就是我们要说的JavaScript函数的第二种调用模式:方法调用模式
二、方法调用模式
方法调用模式,简单来说就是把一个 JavaScript函数作为一个对象的方法来调用,当一个函数被保存为一个对象的属性是,我们就把它称为方法,例如上文的 obj 对象里的 show ,当一个方法被调用时,函数体里面的 this 就会绑定到这个对象,例如上文的 show 里面的 this 。方法调用模式也很容易辨别:obj.show(),对象名 . 属性名 () ;代码的话可以参考上文的 obj 代码 ,博主就不多写了。记住:方法的调用是可以在函数体内通过 this 访问自己所属的那个对象的。
三、构造器调用模式
博主认为构造器调用模式是相对于其他模式来说较为复杂点的调用模式了。通过关键字 new 可以把一个函数作为构造器来调用。关键字 new 可以改变函数的返回值:
function func2(name){
this.name = name;
}
name; //undefined
//普通函数调用模式
var foo = func2('afei');
foo; //undefined
name; //afei
//构造器调用模式
var bar = new func2('lizefei');
bar.__proto__ === func2.prototype; //true
bar; //{name:'lizefei'}
bar.name; //'lizefei'
在上示代码中我们声明了一个函数 func2 ,分别用两种不同的调用模式去调用它。因为函数 func2 并没有显式返回值,所以作为普通函数去调用时,它什么也没有返回,所以 foo 的值是 undefined 。因为普通调用模式的 this 是指向 全局环境 window 的,所以 func2(‘afei’); 后,全局环境下就多了一个 name 变量且等于 ‘afei’。
func2 作为构造器调用时,我们可以看到,它返回的是一个对象,因为关键字 new 使得函数在调用是发生了如下的特殊变化:
- 创建了一个新对象,而且这个新对象是链接到 func2 的 prototype 属性的
- 把函数里的 this 指向了这个新对象
- 如果没有显式的返回值,新对象作为构造器func2的返回值进行返回(所以bar 是 {name:’lizefei’})
这样子我们就可以看出构造器的作用:通过函数的调用来初始化新创建出来的对象。在JavaScript的面向对象编程里面,这个可是相当重要的。
因为在函数的声明上,在未来作为构造器调用的函数和普通函数的声明没什么区别,所以导致后来的开发者很容易因为调用模式的错误导致程序出问题。所以开发者们都默契地约定,用来做构造器调用的函数的函数名的第一个字符应该大写,例如:Person,People。这样子后来的开发者一看到函数名就知道要用构造器调用模式调用此函数了。
四、使用apply()和call()方法调用
这种调用的模式是为了更灵活控制函数运行的上下文环境而诞生的。简单的说就是为了灵活控制函数体内 this 的值。
apply 和 call这两个方法的第一个参数都是要传递被函数上下文的对象(简单点说就是要绑定给函数 this 的对象)。其他参数就有所不同了:
apply方法的第二个参数是一个数组,数组里面的值将作为函数调用的参数;
call方法,从第二个参数起(包括第二个参数),剩下的参数都是作为函数调用的参数;
让我们看看栗子:
var obj = {
name :'afei'
}
function say(ag1,ag2){
console.log(ag1+':'+ag2+" "+ this.name);
}
say.apply(obj,['apply方法','hello']); //apply方法:hello afei
say.call(obj,'call方法','hi'); //call方法:hi afei
正如栗子所示,我们把对象 obj 作为函数 say 的上下文来调用函数 say ,所以函数里的 this 是指向 对象 obj 的。在apply方法里,我们通过数组 [‘apply方法’,’hello’] 给 say 方法传递了两个参数(’apply方法’ 和 ‘hello’),所以打印出来是: apply方法:hello afei。
同理 call 也是一样,而且函数传递的方式通过上面的代码也一目了然我,博主就不多做解释了。
另外,博主还听说apply和call这两个方法除了传递参数的方式不一样,执行的速度还是apply 比 call 要快呢。不过博主就没有实验过。
五、总结
在JavaScript里面,函数只要的调用模式就是这几种了(在ES6里面还有一种很奇怪很特殊的函数调用模式,叫做’标签模板‘,在这里博主也不多说了,有空另更),只要掌握了这几种主要的调用模式,那么日后再也不用担心 this 的值变来变去了。
上文如果有漏的、有错误的地方,望各位小伙伴指出,小弟虚心向学。