JavaScript夯实基础系列(三):this
在JavaScript中,函数的每次调用都会拥有一个执行上下文,通过this关键字指向该上下文。函数中的代码在函数定义时不会执行,只有在函数被调用时才执行。函数调用的方式有四种:作为函数调用、作为方法调用、作为构造函数调用以及间接调用,判定this指向的规则跟函数调用的方式有关。
一、作为函数的调用
作为函数调用是指函数独立执行,函数没有人为指定的执行上下文。在有些情况下,作为函数调用的形式具有迷惑性,不仅仅是简单的函数名后面加括号来执行。
1、明确的作为函数调用
明确的作为函数调用是指形如func(para)形式的函数调用。作为函数调用的情况下this在严格模式下为undefined,在非严格模式下指向全局对象(在浏览器环境下为Window对象)如下代码所示:
var a = 1;
function test1 () {
var a = 2
return this.a
}
test1() // 1
'use strict'
var a = 1;
function test1 () {
var a = 2
return this.a
}
test1() // Uncaught TypeError
以函数调用形式的函数通常不使用this,但是可以根据this来判断当前是否是严格模式。如下代码所示,在严格模式下,this为undefined,strict为true;在非严格模式下,this为全局对象,strict为false。
var strict = (function () {
return !this
})()
2、对象作为桥梁找到方法
通过对象调用的函数称为方法,但是通过对象找到方法并不执行属于作为函数调用的情况。如下代码所示:
var a = 1;
function test() {
console.log( this.a );
}
var obj = {
a: 2,
test: test
};
var func = obj.test;
func(); // 1
上述代码中,obj.test是通过obj对象找到函数test,并未执行,找到函数之后将变量func指向该函数。obj对象在这个过程中只是起到一个找到test地址的桥梁作用,并不固定为函数test的执行上下文。因此var func = obj.test;执行的结果仅仅是变量func和变量test指向共同的函数体而已,因此func()仍然是作为函数调用,和直接调用test一样。
当传递回调函数时,本质也是作为函数调用。如下代码所示:
var a = 1
function func() {
console.log( this.a );
}
function test(fn) {
fn();
}
var obj = {
a: 2,
func: func
};
test( obj.func ); // 1
函数参数是以值传递的形式进行的,obj.func作为参数传递进test函数时会被复制,复制的仅仅是指向函数func的地址,obj在这个过程中起到找到函数func的桥梁作用,因此test函数执行时,里面的fn是作为函数调用的。
接收回调的函数是自己写的还是语言内建的没有什么区别,比如:
var a = 1;
function test() {
console.log( this.a );
}
var obj = {
a: 2,
test: test
};
setTimeout( obj.test, 1000 ); // 1
setTimeout的第一个参数是通过obj对象找到的函数test,本质上obj依然是起到找到test函数的桥梁作用,因此test依然是作为函数调用的。
3、间接调用传递null或undefined作为执行上下文
函数的间接调用是指通过call、apply或bind函数明确指定函数的执行上下文,当我们指定null或者undefined作为间接调用的上下文时,函数实际是作为函数调用的。但是有一点需要注意:call()和apply()在严格模式下传入空值则上下文为空值,并不是因为遵循作为函数调用在严格模式下执行上下文为全局对象的规则,而是因为在严格模式下call()和apply()的第一个实参都会变成this的值,哪怕传入的实参是原始值甚至是null或undefined。
var a = 1;
function test() {
console.log( this.a );
}
test.call( null ); // 1
间接调用的目的是为了指定函数的执行上下文,那么为什么要传null或undefined使其作为函数调用呢?这是因为我们会用到这些方法的其他性质:函数call中一般不传入空值(null或undefined);函数apply传入空值可以起到将数组散开作为函数参数的效果;函数bind可以用来进行函数柯里化。在ES6中,新增了扩展运算符‘...’,将一个数组转为用逗号分隔的参数序列,可以替代往apply函数传空值的情况。但是ES6中没有增加函数柯里化的方法,因此往函数bind中传空值的情况将继续使用。
在使用apply或bind传入空值的情况,一般是不关心this值。但是如果函数中使用了this,在非严格模式下能够访问到全局变量,有时会违背代码编写的本意。因此,使用一个真正空的值传入其中能够避免这类情况,如下代码所示:
var empty = Object.create( null );
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
foo.apply( empty, [1, 2] ); // a:1, b:2
二、作为方法调用
当函数挂载到一个对象上,作为对象的属性,则称该函数为对象的方法。如果通过对象来调用函数时,该对象就是本次调用的上下文,被调用函数的this也就是该对象。如下代码所示:
var obj = {
a: 1,
test: test
};
function test() {
console.log( this.a );
}
obj.test(); // 1
在JavaScript中,对象可以拥有对象属性,对象属性有又可以拥有对象或者方法。函数作为方法调用时,this指向直接调用该方法的对象,其他对象仅仅是为了找到this指向的这个对象而已。如下代码所示:
function test() {
console.log( this.a );
}
var obj2 = {
a: 2,
test: test
};
var obj1 = {
a: 1,
obj2: obj2
};
obj1.obj2.test(); // 2
当方法的返回值时一个对象时,这个对象还可以再调用它的方法。当方法不需要返回值时,最好直接返回this,如果一个对象中的所有方法都返回this,就可以采用链式调用对象中的方法。如下代码所示:
function add () {
this.a++;
return this;
}
function minus () {
this.a--;
return this;
}
function print() {
console.log( this.a );
return this;
}
var obj = {
a: 1,
print: print,
minus: minus,
add: add
};
obj.add().minus().add().print(); // 2
三、作为构造函数调用
在JavaScript中,构造函数没有任何特殊的地方,任何函数只要是被new关键字调用该函数就是构造函数,任何不被new关键字调用的都不是构造函数。
当使用new关键字来调用函数时,会经历以下四步:
1、创建一个新的空对象。
2、这个空对象继承构造函数的prototype属性。
3、构造函数将新创建的对象作为执行上下文来进行初始化。
4、如果构造函数有返回值并且是对象,则返回构造函数的返回值,否则返回新创建的对象。
约定俗成的是:在编写构造函数时函数名首字母大写,且构造函数不写返回值。因此一般来说,new关键字调用构造函数创建的新对象作为构造函数的this。如下代码所示:
function foo() {
this.a = 1;
}
var bar = new foo();
console.log( bar.a ); // 1
四、间接调用
在JavaScript中,对象中的方法属性仅仅存储的是一个函数的地址,函数与对象的耦合度没有想象中的高。通过对象来调用函数,函数的执行上下文(this指向)就是该对象。如果通过对象来找到函数的地址,就能指定函数的执行上下文,可以使用call()、apply()和bind()方法来实现。换而言之,任何函数可以作为任何对象的方法来调用,哪怕函数并不是那个对象的方法。
1、call()和apply()
每个函数都call()和apply()方法,函数调用这两个方法是可以明确指定执行上下文。从绑定上下文的角度来说这两个方法是一样的,第一个参数传递的都是指定的执行上下文。所不同的在于call()方法剩余的参数将会作为函数的实参来使用,可以有多个;apply()则最多只接收两个参数,第一个是执行上下文,第二个是一个数组,数组中的每个元素都将作为函数的实参。如下代码所示:
var a = 1
function test(b,c) {
console.log(`a:${this.a},b:${b},c:${c}`)
}
var obj = {
a:2
}
test.call(obj,3,4) // a:2,b:3,c:4
var d = 11
function test2(b,c) {
console.log(`b:${b},c:${c},d:${this.d}`)
}
var obj2 = {
d:12
}
test2.apply(obj2,[13,14]) // b:13,c:14,d:12
在非严格模式下,call()、apply()的第一个参数传入null或者undefined时,函数的执行上下文被替代为全局对象,如果传入的是基础类型,则为替代为相应的包装对象。在严格模式下,遵循的规则是传入的值即为执行上下文,不替换,不自动装箱。如下代码所示:
var a = 1
function test1 () {
console.log(this.a)
}
test1.call(null) // 1
test1.call(undefined) // 1
test1.apply(null) // 1
test1.apply(undefined) // 1
'use strict'
function test1 () {
console.log(this)
}
test1.call(null) // null
test1.call(undefined) // undefined
test1.call(1) // 1
test1.apply(null) // null
test1.apply(undefined) // undefined
test1.apply(1) // 1
apply()有一个较为常见的用法:将数组转化成函数的参数序列。ES6中增加了扩展运算符“...”来实现该功能。如下代码所示:
var arr = [1,19,4,54,69,9]
var a = Math.max.apply(null,arr)
console.log(a) // 69
var b = Math.max(...arr)
console.log(b) // 69
2、bind()
bind()函数可以接收多个参数,返回一个功能相同、执行上下文确定、参数经过初始化的函数。其中第一个参数为要绑定的执行上下文,剩余参数为返回函数的预定义值。bind()函数的作用有两点:1、为函数绑定执行上下文;2、进行函数柯里化。如下代码所示:
var a = 1
function func(b,c) {
console.log(`a:${this.a},b:${b},c:${c}`)
}
var obj = {
a: 2
}
var test = func.bind(obj,3)
test(4) // a:2,b:3,c:4
bind()方法是ES5加入的,但是我们可以很轻易的在ES3中通过apply()模拟出来,下面代码是MDN上的bind()的polyfill。
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== "function") {
// 可能的与 ECMAScript 5 内部的 IsCallable 函数最接近的东西,
throw new TypeError( "Function.prototype.bind - what " +
"is trying to be bound is not callable"
);
}
var aArgs = Array.prototype.slice.call( arguments, 1 ),
fToBind = this,
fNOP = function(){},
fBound = function(){
return fToBind.apply(
(this instanceof fNOP &&oThis ? this : oThis),
aArgs.concat( Array.prototype.slice.call( arguments ) )
);
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
五、规则的优先级
函数的调用有时不只一种,那么不同调用方式的规则的优先级就最终决定了this的指向。那就让我们来比较不同调用方式的规则优先级。如下代码所示,当函数作为方法调用的时候,this指向调用方法的对象,当作为函数调用时,this指向在非严格模式下指向全局对象,在严格模式下指向undefined。因此,方法调用的优先级高于函数调用。
var a = 1
var obj = {
a:2,
test:test
}
function test () {
console.log(this.a)
}
var b = obj.test
obj.test() // 2
b() // 1
如下代码所示是函数作为方法调用分别和间接调用、构造函数调用作对比。由代码可知:函数作为方法调用优先级分别小于间接调用和构造函数调用。
function test(para) {
this.a = para
}
var obj1 = {
test: test
}
var obj2 = {}
obj1.test( 2 )
console.log( obj1.a ) // 2
obj1.test.call( obj2, 3 )
console.log( obj2.a ) // 3
var bar = new obj1.test( 4 )
console.log( obj1.a ) // 2
console.log( bar.a ) // 4
new关键字后面是一个函数,而call()和apply()并不是返回一个函数,而是依照传入参数来执行函数,因此形如new foo.call(obj)的代码是不被允许的。ES5中的bind()返回的是一个函数,可以与new关键字同时使用。如下代码所示,bind()返回的函数用作构造函数,将忽略传入bind()的this值,原始函数会以构造函数的形式调用,传入的参数也会原封不动的传入原始函数。
function test(something) {
this.a = something;
}
var obj = {};
var bar = test.bind( obj );
bar( 2 );
console.log( obj.a ); // 2
var baz = new bar( 3 );
console.log( obj.a ); // 2
console.log( baz.a ); // 3
总之,构造函数的优先级大于间接调用,间接调用的优先级大于方法调用,方法调用的优先级大于函数调用。
六、词法this
this关键字没有作用域限制,函数的this指向调用该函数的对象,在嵌套函数汇中,如果想访问外层函数的this值,可以将外层函数的this赋值给一个变量,用词法作用域来代替传统的this机制。如下代码所示:
function foo() {
var self = this // 词法上捕获`this`
setTimeout( function(){
console.log( self.a )
}, 1000 )
}
var obj = {
a: 2
};
foo.call( obj ) // 2
ES6新增了箭头函数,箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。如下代码所示,箭头函数能够将this固化,箭头函数内部没有绑定this的机制,其内部的this就是外层代码块的this。传统的this机制让很多人与词法作用域混淆,因此有了将this赋值给变量的行为,ES6只是将这种行为加以标准化而已。
var a = 21
function test() {
setTimeout(() => {
console.log('a:', this.a)
}, 1000)
}
test.call({ a: 42 }) // 42
七、总结
JavaScript中的this机制跟词法作用域没有关系,根据函数调用的方式不同,确定this指向的规则也不相同。在确定this指向时可以遵循以下步骤:
1、函数是否为构造函数调用,即函数跟在new关键字后面,如果是,this就是新构建的对象。
2、函数是否为间接调用,即通过call()、apply()或者bind()调用,如果是,this就是明确指定的对象。
3、函数是否为作为方法调用,即通过对象来调用函数,如果是,this就是该对象。
4、否则,即为作为函数的调用,在非严格模式下,this指向全局对象,在严格模式下,this为undefined。
可以将外层函数的this赋值给一个变量,使得内层函数以词法作用域的规则来访问该this。ES6新增的箭头函数便是使用词法作用域来决定this绑定的。
如需转载,烦请注明出处:https://www.cnblogs.com/lidengfeng/p/9198569.html
JavaScript夯实基础系列(三):this的更多相关文章
- JavaScript夯实基础系列(四):原型
在JavaScript中有六种数据类型:number.string.boolean.null.undefined以及对象,ES6加入了一种新的数据类型symbol.其中对象称为引用类型,其他数据类 ...
- JavaScript夯实基础系列(二):闭包
在JavaScript中函数是一等公民.所谓一等公民是指函数跟其他对象一样,很普通,可以进行把函数存在数组中.作为参数传递.赋值给变量等操作.当函数作为另一个函数的返回值在外部调用时,跟该函数在函 ...
- JavaScript夯实基础系列(五):类
JavaScript中没有类,是通过使用构造函数和原型模式的组合来实现类似其它面向对象编程语言中"类"的功能.ES6引入的关键字class,形式上向其它面向对象编程语言靠拢,其 ...
- JavaScript夯实基础系列(一):词法作用域
作用域是一组规则,规定了引擎如何通过标识符名称来查询一个变量.作用域模型有两种:词法作用域和动态作用域.词法作用域是在编写时就已经确定的:通过阅读包含变量定义的数行源码就能知道变量的作用域.Jav ...
- 夯实基础系列四:Linux 知识总结
前言 前三节内容传送门: 夯实基础系列一:Java 基础总结 夯实基础系列二:网络知识总结 夯实基础系列三:数据库知识总结 现在很多公司项目部署都使用的是 Linux 服务器,互联网公司更是如此.对于 ...
- 【C++自我精讲】基础系列三 重载
[C++自我精讲]基础系列三 重载 0 前言 分二部分:函数重载,操作符重载. 1 函数重载 函数重载:指在同一名字空间中,函数名称相同,参数类型.顺序或数量不同的一类函数,同一函数名的函数能完成不同 ...
- java基础系列(三)---HashMap
java基础系列(三)---HashMap java基础系列 java基础系列(一)---String.StringBuffer.StringBuilder java基础系列(二)---Integer ...
- 快速掌握JavaScript面试基础知识(三)
译者按: 总结了大量JavaScript基本知识点,很有用! 原文: The Definitive JavaScript Handbook for your next developer interv ...
- JavaScript笔记基础篇(三)
针对前段JS获取当前时间或者对时间数据处理方法汇总: javascript 字符串转化为日期 Java代码 var s="2010-5-18 12:30:20"; var t= ...
随机推荐
- bzoj5342 CTSC2018 Day1T3 青蕈领主
首先显然的是,题中所给出的n个区间要么互相包含,要么相离,否则一定不合法. 然后我们可以对于直接包含的关系建出一棵树,于是现在的问题就是给n个节点分配权值,使其去掉最后一个点后不存在非平凡(长度大于1 ...
- noip前集训
10.18 关网了,2333 上午考试,130 rank16 一直在刚T2的割点,却直接弃了一道第一眼看上去不可做但实际并没那么难想的小模拟 但是T2没搞出来是不是也要反思一下,先是割点板子忘了,之后 ...
- 【刷水】之USACO2008资格赛(Bzoj1599-1603)
做之前真是没想到有这么水>.< 但做了还是发上来吧>.< 就当是刷一刷AC量&1A率什么的>.< Bzoj1599: [Usaco2008 Oct]笨重的石 ...
- SM干货篇:你应该具备的提问技巧!
在成为Scrum Master(SM)之前,我曾担任过许多团队的技术负责人.工作内容之一就是做决定,而且我认为自己做得挺好:坚定果断是我性格的一部分. 然而,当我成为Scrum Master之后,这样 ...
- Robot Framework 源码解析(1) - java入口点
一直很好奇Robot Framework 是如何通过关键字驱动进行测试的,好奇它是如何支持那么多库的,好奇它是如何完成截图的.所以就打算研究一下它的源码. 这是官方给出的Robot framework ...
- 循环神经网络之LSTM和GRU
看了一些LSTM的博客,都推荐看colah写的博客<Understanding LSTM Networks> 来学习LSTM,我也找来看了,写得还是比较好懂的,它把LSTM的工作流程从输入 ...
- Scala 开发遇到的坑
1. x.purchaseIntax.getOrElse(BigDecimal(0.00)) 可以直接写成 x.purchaseIntax.getOrElse(0)自动转换的 2. srcDataL ...
- 【工具篇】Selenium 学习实践(一)环境搭建
一.环境搭建 (1)初学者最佳环境: Python 2.7 + Selenium 2+ Firefox 46 (2)喜欢尝新的环境: Python 3.6 + Selenium 3+ Firefox ...
- kolla 多节点部署 openstack
kolla 介绍 简介 kolla 的使命是为 openstack 云平台提供生产级别的.开箱即用的交付能力.kolla 的基本思想是一切皆容器,将所有服务基于 Docker 运行,并且保证一个容器只 ...
- Netty基础系列(2) --彻底理解阻塞非阻塞与同步异步的区别
引言 在进行I/O学习的时候,阻塞和非阻塞,同步和异步这几个概念常常被提及,但是很多人对这几个概念一直很模糊.要想学好Netty,这几个概念必须要掌握清楚. 同步和异步 同步与异步的区别在于,异步基于 ...