JS高级使用1.0——this的使用以及函数apply()和call()的解释
创作场景
因公司中大多数使用的是原生的js或者jquery写法,使用的UI框架大多数只是一些结构和一些组件(业务比较复杂),所以学习了js的高级教程,再根据公司现有的代码进行举证分析,准备对JS高级部分写一些博客做一下加深。
阅读前提
最好是有一定工作经验(没有也不要紧,肯定会讲清楚),同时对js基础有一定了解,包括函数的定义和调用
、函数的作用域
、什么是window对象
等。如果对上述不了解的伙伴,此博客也会进行简单说明,帮助阅读者理解内容,并附上帮助内容的详细博客链接,以便后续了解。
关于this的介绍可能会有些许繁杂和抽象,在apply()和call()函数是需要了解this的属性的,如果不懂可以适当跳过或只看一下总结内容。
开始正题
1. 什么是this,函数内的this和函数外的this有什么区别
定义:JavaScript的this指的是调用某个函数的对象
,官方语言:函数上下文
!
解释:只要是一个函数,都有其调用者
,函数无法脱离调用者主动执行,像setTimeOut
执行函数和闭包
执行函数只是调用方式不同,并不是说没有调用者。
先用一个例子看一下怎么个事。
<!-- 定义一个全局的函数-->
function Fn() {
console.log("函数内部的this", this)
}
// 执行全局函数
Fn()
// 输出外部的this
console.log("函数外部的this", this)
先看一下输出结果
可以看到两个的输出都是window对象。
1.1 window对象
在JavaScript中,一个浏览器窗口就是一个window对象。ECMAScript
规定window对象是BOM对象
,也就是浏览器对象,我们可以根据window进行浏览器的交互,比如说跳转页面:window.location.href = ‘跳转的路径’,而一般我们使用document.getElementById是用来获取页面上的某个标签,而标签<div></div>
就是我们常说的DOM
元素。
如果详细了解window对象可以点击下面的链接。
OK,回归我们的this探索,先来看一下window对象中有什么?
可以看到,我们当时定义的Fn函数已经挂载到了window对象上,这也是为什么函数内部的this是window对象的原因,因为就是window调用的这个函数,众所周知,函数的调用一定是对象点方法
,也就是object.fun
,一般我们在全局上下文中定义的函数直接就可以调用,是因为函数已经挂载到了window对象中,调用的时候会自动在window对象上找这个方法并且调用,最终就是这样执行的:
window.Fn()
我们执行一下上面这个方法,其实输出结果和直接执行是一样的,这样就可以直观的看到this就是调用的对象
这个定义了。
2. 对象内部的this怎么使用
对象内部的this简单点来说就是指的对象本身,如下:
var obj = {
name: "张三",
age: 18,
// 等价于obj.name
fun: function () {
console.log("name:", this.name)
console.log("age:", this.age)
}
}
obj.fun()
输出结果如下:
稍作解释:这里用对象调用了对象内部的一个方法,所以函数fun中的this指向的就是obj对象,obj对象上有name和age两个属性,所以输出的就是对象上的属性值,接下来用两个稍微复杂的例子更进一步说明一下this。
例1:
console.log("-----------------------------例1中的this---------------------------")
var obj1 = {
name: "张三",
age: 18,
fun: function () {
console.log("例1name:", name)
console.log("例1age:", age)
}
}
obj1.fun()
输出结果:
可以看到name输出了一个空值,而age显示的是未定义,这是怎么回事呢?
解释一下,js在执行函数的时候,会在内存中开辟一块空间用于执行函数,执行结束后释放空间,那么这个空间就可以称之为函数的执行空间
,一般函数中所用到的变量会优先在当前的执行空间中寻找,如果找不到,则继续向上找(但不包括对象,对象的属性都可以看为私有变量,只能通过对象访问
),所以在这个例子中无法直接找到obj对象的name和age值。问题是name怎么是一个空值呢?
按照刚才所说的,继续向上找的话只能是window,也就是全局上下文
,这是最大也是最后一个上下文,先看下图:
发现了吗?window上有个name属性是空值,这也是为什么没有报name未找到的错误,在全局上下文中这是一个变量,所以可以直接访问到,同时也可以看到两个obj对象也在window中。
console.log("-----------------------------例2中的this---------------------------")
var obj2 = {
name: "张三",
age: 18,
fun: function () {
var name = "李四";
var age = 20;
var height = 180;
var objFn = {
job: "程序猿",
fun1: function (){
console.log("fun1函数中的this", this)
}
}
function fun2() {
var name = "王五";
var age = 30;
objFn.fun1()
console.log("fun2函数中的this", this)
console.log("name:", name)
console.log("age:", age)
console.log("height:", height)
console.log("this.name", this.name)
console.log("this.age", this.age)
console.log("this.height", this.height)
}
console.log("fun函数中的this", this)
fun2()
}
}
obj2.fun()
这段代码其实涉及到了另一个闭包
的知识点,这里不对闭包说明,解释一下这段程序:
obj2对象中有一个函数是fun,fun函数中又定义了一个函数fun2,在fun2函数中分别使用this和不使用this输入了几个值,发现不用this的话,就是我们说的,首先在本身找,没有会依次向上找,直到找到为止,因为在fun函数中定义了name、age和height,所以输出的其实就是fun函数中的,那用this为啥就不行了呢?
注意看哈,我们分别对fun2函数中的this和fun函数的this进行了打印,fun函数的this好理解,因为是obj2调用的,所以this就是obj2,那fun2函数中的this为啥是window对象,说白了,像这种定义的函数,因为没有直接调用的对象,obj2对象没有调用吧,那谁来调用呢?只能window老大哥了呀,你想啊,你是老三(fun2),你家老二(fun)不管你,那只能老大(window)管了啊,老大不管谁管,无论怎么嵌套定义,你就是在fun2函数在在定义一个fun3,它的this还是window,因为没人管呀。你看老二中有一个函数fun1,在对象objFn中,人家的this指向的就是自己objFn,因为就是人家自己调用的呀(可以这么理解,都在家里待着,你姐没钱了人家花自己的,你没钱了你就向你妈要,因为你姐不给你,虽然你两都在家里,都是兄妹,好惨
)。
总结:上面这段文字其实理解起来有些抽象,大家只需要记住,函数中的this不是指向的这个函数,这是很多人的误区,这个函数严格来说只是一个上下文
,执行完就销毁了,this一定是一个可观测、持久存在的对象
,如果函数执行前没有指定对象,那this指向的就是window。
场景 | this |
---|---|
obj.fun() | obj对象 |
fun() | window对象 |
![]() | 数组本身(数组中用法,一般正常人不这么用,了解即可) |
setInterval、setTimeout | window对象 |
DOM元素的click或者其它事件 | DOM元素 |
IIFE(自执行函数) | window对象 |
2.1 IIFE
关于IIFE(自执行函数)可以看一下下面这篇博客,总结还是可以的
3. 实际和工作中this应该怎样使用
3.1 对象中定义多个函数,函数中出现互相调用的情况
var funP = {
fun1: function () {
console.log("老大说:我出门上学了妈妈")
},
fun2: function () {
console.log("老二说:我出门上学了妈妈")
},
fun3: function () {
console.log("老三说:我出门上学了妈妈")
},
fun: function () {
console.log("孩儿他爸说:我送三个孩子出门上学了老婆")
this.fun1()
this.fun2()
this.fun3()
}
}
// 老大自己去上学
funP.fun1()
// 老二自己去
funP.fun2()
// 老三自己去
funP.fun3()
// 她妈看不惯,决定让孩儿他爸出手
funP.fun()
3.2 将this存储到变量,模拟修改this指向,达到使用this的目的
console.log("------------------------------------------------")
var obj1111111111111111 = {
name: "张三",
age: 18,
fun1: function () {
var that = this;
function fun2() {
console.log("that.name", that.name)
}
fun2()
}
}
obj1111111111111111.fun1() // 输出“张三”
这种比较常见,因为实际中对象的定义都是见名知意
的,一般都比较长,所以这种写法还是比较实用。
3.3 DOM对象调用函数中使用this
这种可能看起来this的优势没有那么明显,如果页面上有多个DOM对象都需要绑定一个函数,函数中需要获取自己的文本,总不能给所有对象都像上面一样绑死一个函数,就是下面这样。
$("#h1").bind('click', function () {
console.log($("#h1").text())
})
$("#h2").bind('click', function () {
console.log($("#h2").text())
})
。。。
如果使用this,就可以改造为
var domFun = function () {
console.log(this.innerText)
}
$("#h1").bind('click', domFun)
$("#h2").bind('click', domFun)
这样就达到了函数复用的效果,维护起来也更好维护。
4. 为啥花精力研究this
有的人就说了,我只需要记住这东西简单使用就行了,了解this这么深干嘛,因为apply和call
要用啊,重头戏在这两函数,这两函数有大用啊!!!
apply()和call()各有优势,最明显的就是可以修改this的指向
,且听我简单唠一下,为什么要执意修改this的指向,有的人可能就说了,我传个参数不就行了吗,没毛病的,这么做在参数少的情况下可以,但是如果参数过多呢,开发中我们不建议将一个函数的参数定义过多
,太多了不好维护,同时还有一个就是代码量压缩
的概念,你看哈,搞个参数userPostInfo对象,那之后使用这个参数中的属性必须要用userPostInfo来访问,久而久之,代码量不就上去了吗,this是不是相对少一些呢?不要小瞧这几个字符串,积少成多也不少,程序讲究的是效率
,如果两个产品实现了同样的功能,一个执行速度快,一个慢,肯定优先使用快的。
其实这也是变向要求代码的精简度,也是一个好的习惯,毕竟代码要写的越来越少不是吗。
5. apply介绍及使用
首先看一下官方的语法:
apply(thisArg)
apply(thisArg, argsArray)
参数介绍:
thisArg: 需要改变this的值,可以是null和undefined,如果是null和undefined则默认会赋值window对象
argsArray:一个类数组对象,用于指定调用 func 时的参数,或者如果不需要向函数提供参数,则为 null 或 undefined。
返回值:
函数处理的结果
解释一下,因为函数中的this不是你调用函数的对象就是window,如果你想改变this的值,我们在上面用到过一种假的实现,就是用一个变量存起来,但是那种写法还是有些浪费内存,而apply函数就可以很好的解决这个问题。
而第二个参数也是比较重要的,了解函数的知道,每个函数都有一个arguments对象,也就是参数数组,而argsArray实际上就是把你需要传递的参数放到一个数组给arguments,然后函数就可以接收到,不过稍微有点限制条件,之后说明一下。
用两个例子说明一下
例1:
console.log("apply例1------------------------------------")
function testApply1(name, age, height) {
console.log("我的姓名是", name, ",我的年龄是", age, "我的身高是", height)
}
testApply1.apply(null, ["张三", 18, 180])
例2:
console.log("apply例2------------------------------------")
function testApply2(name, age, height) {
var obj = {
name: name,
age: age,
height: height
}
return obj
}
var apply2Res = testApply2.apply(null, ["张三", 18, 180])
console.log("apply例2的返回值", apply2Res)
例3:
console.log("apply例3------------------------------------")
var array = ["a", "b"];
var elements = [0, 1, 2];
// 等价于:array.push(... elements),但是这种写法只支持ES6
array.push.apply(array, elements);
console.info("apply例3", array);
例5:
console.log("apply例5------------------------------------")
// 数组中的最小/最大值
var numbers = [5, 6, 2, 3, 7];
// 用 apply 调用 Math.min/Math.max
var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);
console.log("apply5最大值", max, "apply5最小值", min)
上面几个例子分别是几种用法,说实在的,目前我使用过的和最实用的也就是第三种方式,拼接数组,因为apply最大的好处就是可以将数组中的参数一个一个传递给函数
,类似循环的效果,而且不用担心浏览器兼容性(因为有的公司确实是要兼容部分IE浏览器,ES6不能用,比如我们公司)。
6. call介绍及使用
语法:
call(thisArg)
call(thisArg, arg1)
call(thisArg, arg1, arg2)
参数介绍
thisArg: 改变this的指向对象,和apply一致(不传递则默认为window)
arg1等等,依次传递的参数(可选,可以不传递参数)
返回值
使用指定的 this 值和参数调用函数后的结果。和apply保持一致
继续搞几个例子看一下
例1:
console.log("call例1------------------------------------")
function testCall1(name, age, height) {
console.log("我的姓名是", name, ",我的年龄是", age, "我的身高是", height)
}
testCall1.call(null, "张三", 18, 180)
例2:
console.log("call例2------------------------------------")
function testCall2() {
console.log("我的姓名是", this.name, ",我的年龄是", this.age, "我的身高是", this.height)
}
testCall2.call({
name: "张三",
age: 18,
height: 180
})
例3:
console.log("call例3------------------------------------")
var obj111 = {
name: "李四",
age: 68,
money: "100亿"
}
var obj222 = {
name: "王五",
age: 18,
money: "1块",
getMoney: function () {
return this.money
},
objFun: function () {
// 如果只是this.getMoney,那this就是obj222这个对象,所以最后的钱就是1块
var money = this.getMoney.call(obj111)
console.log("我叫", this.name, "今年", this.age, "岁,拥有了李四的", money)
}
}
obj222.objFun()
call函数目前使用的我觉得较apply多一些,上面的例3中是比较常用的,改变this的指向,让我的方法中可以访问别的对象的属性,那么有人就说了,何必这么费劲呢,直接访问对象的属性不香吗?其实来说这是一种比较少见的概念,继承,A对象继承了B对象的属性,但是这会造成一定的内存损耗,因为B对象的属性是有的,没必要复制一份,造成浪费,而call函数可以有效的避免这个问题,我直接使用即可,不用继承你这个属性,然后挂载到我的对象上(这部分解释稍微有点抽象,且为个人理解,还需一定举证
)。
7. apply和call的相同点和不同点
相同点
:都可以改变this的指向,官方语言就是:扩充函数赖以运行的作用域。
最大不同点
:
- apply传递参数是以
数组
传递的,使用时必须将所有参数包装为一个数组,但是调用时会依次进行调用,类似for循环。 - call传递参数只能
一个一个
传递。
同时我们需要注意一点,call和apply传递参数都不能超过函数定义的数量,即使你传多了,也是没用滴,如果你传递少了呢,那就是undefined喽。上面说过和arguments
有点相似,但是arguments包含你所有的参数,也就是说你给多少,我就有多少,这是一个注意点。
function test111(name) {
console.log("arguments", arguments)
console.log("name", name)
}
test111(1, 2, 3)
OK,本次就讲到这里,希望对大家有帮助,如果后续发现了更好的用处也会继续修正此文章。
如果有大佬发现问题,麻烦评论说一下,可能会存在部分描述不正确。
还是建议大家先面向官方学习,了解定义后在根据博客学习怎么使用或者不懂的地方。