深入理解javascript中的this


Posted in Javascript onFebruary 08, 2021

深入理解Js中的this

JavaScript作用域为静态作用域static scope,但是在Js中的this却是一个例外,this的指向问题就类似于动态作用域,其并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用,this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,当然实际上this的最终指向的是那个调用它的对象。

作用域

我们先来了解一下JavaScript的作用域,以便理解为什么说this更类似于动态作用域,通常来说,一段程序代码中所用到的名字并不总是有效或可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域scope,当一个方法或成员被声明,他就拥有当前的执行上下文context环境,在有具体值的context中,表达式是可见也都能够被引用,如果一个变量或者其他表达式不在当前的作用域,则将无法使用。作用域也可以根据代码层次分层,以便子作用域可以访问父作用域,通常是指沿着链式的作用域链查找,而不能从父作用域引用子作用域中的变量和引用。
JavaScript作用域为静态作用域static scope,也可以称为词法作用域lexical scope,其主要特征在于,函数作用域中遇到既不是参数也不是函数内部定义的局部变量时,去函数定义时上下文中查,而与之相对应的是动态作用域dynamic scope则不同,其函数作用域中遇到既不是参数也不是函数内部定义的局部变量时,到函数调用时的上下文中去查。

var a = 1;
var s = function(){
 console.log(a);
};
(function(){
 var a = 2;
 s(); // 1
})();

调用s()是打印的a为1,此为静态作用域,也就是声明时即规定作用域,而假如是动态作用域的话在此处会打印2。现在大部分语言都采用静态作用域,比如CC++JavaPHPPython等等,具有动态作用域的语言有Emacs LispCommon LispPerl等。

全局作用域

直接声明在顶层的变量或方法就运行在全局作用域,借用函数的[[Scopes]]属性来查看作用域,[[Scopes]]是保存函数作用域链的对象,是函数的内部属性无法直接访问但是可以打印来查看。

function s(){}
console.dir(s);
/*
 ...
 [[Scopes]]: Scopes[1]
 0: Global ...
*/
// 可以看见声明的s函数运行的上下文环境是全局作用域

函数作用域

当声明一个函数后,在函数内部声明的方法或者成员的运行环境就是此函数的函数作用域

(function localContext(){
 var a = 1;
 function s(){ return a; }
 console.dir(s);
})();
/*
 ...
 [[Scopes]]: Scopes[2]
 0: Closure (localContext) {a: 1}
 1: Global ...
*/
// 可以看见声明的s函数运行的上下文环境是函数localContext的作用域,也可以称为局部作用域

块级作用域

代码块内如果存在let或者const,代码块会对这些命令声明的变量从块的开始就形成一个封闭作用域。

{
 let a = 1;
 function s(){return a;}
 console.dir(s);
 /*
 ...
 [[Scopes]]: Scopes[2]
 0: Block {a: 1}
 1: Global ...
 */
}
// 可以看见声明的s函数运行的上下文环境是Block块级作用域,也是局部作用域

分析

我们在使用this之前有必要了解为什么在JavaScript中要有this这个设计,在这之前我们先举个小例子,通常我们使用this时可能会遇到的典型问题就类似于下面这样,虽然我们运行的都是同一个函数,但是执行的结果可能会不同。

var obj = {
 name: 1,
 say: function() {
 return this.name;
 }
};
window.name = 2;
window.say = obj.say;

console.log(obj.say()); // 1
console.log(window.say()); // 2

产生这样的结果的原因就是因为使用了this关键字,前文已经提到了this必须要在运行时才能确定,在这里,对于obj.say()来说,say()运行的环境是obj对象,对于window.say()来说,say()运行的环境是window对象,所以两者运行的结果不同。
此时我们就来了解一下,为什么JavaScript会有this这样一个设计,我们首先来了解一下JavaScript的内存结构中的堆栈,堆heap是动态分配的内存,大小不定也不会自动释放,栈stack为自动分配的内存空间,在代码执行过程中自动释放。JavaScript在栈内存中提供一个供Js代码执行的环境,关于作用域以及函数的调用都是栈内存中执行的。Js中基本数据类型StringNumberBooleanNullUndefinedSymbol,占用空间小且大小固定,值直接保存在栈内存中,是按值访问,对于Object引用类型,其指针放置于栈内存中,指向堆内存的实际地址,是通过引用访问。
那么此时我们来看一下上边的示例,在内存中对于obj对象是存放在堆内存的,如果在对象中的属性值是个基本数据类型,那么其会跟这个对象存储在同一块内存区域,但是这个属性值同样可能是一个引用类型,那么对于say这个函数也是存在于堆内存中的,实际上在此处我们可以将其理解为这个函数的实际定义在一个内存区域(以一个匿名函数的形式存在),而obj这个对象同样在其他的一个内存区域,obj通过say这个属性指向了这个匿名函数的内存地址,obj --say--> funtion,那么此时问题来了,由于这种内存结构,我们可以使任何变量对象等指向这个函数,所以在JavaScript的函数中是需要允许我们取得运行环境的值以供使用的,我们必须要有一种机制,能够在函数体内部获得当前的运行环境context,所以this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。

使用

我们需要记住,this是在运行时进行绑定的,并不是在定义时绑定,它的context取决于函数调用时的各种条件,简单来说this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式,再简单来说this永远指向调用者,但箭头函数除外,接下来我们介绍一下五种this的使用情况。

默认绑定

最常用的函数调用类型即独立函数调用,这个也是优先级最低的一个,此时this指向全局对象,注意如果使用严格模式strict mode,那么全局对象将无法使用默认绑定,因此this会变为undefined

var a = 1; // 变量声明到全局对象中
function f1() {
 return this.a;
}

function f2() {
 "use strict";
 return this;
}

console.log(f1()); // 1 // 实际上是调用window.f1()而this永远指向调用者即window
console.log(f2()); // undefined // 实际上是调用 window.f2() 此时由于严格模式use strict所以在函数内部this为undefined

隐式绑定

对象属性引用链中只有最顶层或者说最后一层会影响this,同样也是this永远指向调用者,具体点说应该是指向最近的调用者,当然箭头函数除外,另外我们可能有意无意地创建间接引用地情况,这个情况下同样也适用于this指向调用者,在上文分析那部分使用的示例就属于间接引用的情况。

function f() {
 console.log(this.a);
}
var obj1 = {
 a: 1,
 f: f
};
var obj2 = {
 a: 11,
 obj1: obj1
};
obj2.obj1.f(); // 1 // 最后一层调用者即obj1
function f() {
 console.log(this.a);
}
var obj1 = {
 a: 1,
 f: f
};
var obj2 = {
 a: 11,
};
obj2.f = obj1.f; // 间接引用
obj2.f(); // 11 // 调用者即为obj2

显示绑定

如果我们想把某个函数强制在某个环境即对象上,那么就可以使用applycallbind强制绑定this去执行即可,每个Function对象都存在apply()call()bind()方法,其作用都是可以在特定的作用域中调用函数,等于设置函数体内this对象的值,以扩充函数赖以运行的作用域,此外需要注意使用bind绑定this的优先级是大于applycall的,即使用bind绑定this后的函数使用applycall是无法改变this指向的。

window.name = "A"; // 挂载到window对象的name
document.name = "B"; // 挂载到document对象的name
var s = { // 自定义一个对象s
 name: "C"
}

var rollCall = {
 name: "Teacher",
 sayName: function(){
 console.log(this.name);
 }
}
rollCall.sayName(); // Teacher

// apply
rollCall.sayName.apply(); // A // 不传参默认绑定window
rollCall.sayName.apply(window); // A // 绑定window对象
rollCall.sayName.apply(document); // B // 绑定document对象
rollCall.sayName.apply(s); // C // 绑定自定义对象

// call
rollCall.sayName.call(); // A // 不传参默认绑定window
rollCall.sayName.call(window); // A // 绑定window对象
rollCall.sayName.call(document); // B // 绑定document对象
rollCall.sayName.call(s); // C // 绑定自定义对象

// bind // 最后一个()是为让其执行
rollCall.sayName.bind()(); //A // 不传参默认绑定window
rollCall.sayName.bind(window)(); //A // 绑定window对象
rollCall.sayName.bind(document)(); //B // 绑定document对象
rollCall.sayName.bind(s)(); // C // 绑定自定义对象

new绑定

JavaScriptnew是一个语法糖,可以简化代码的编写,可以批量创建对象实例,在new的过程实际上进行了以下操作。

创建一个空的简单JavaScript对象即{}。链接该对象(即设置该对象的构造函数)到另一个对象。将步骤1新创建的对象作为this的上下文context。如果该函数没有返回对象,则返回步骤1创建的对象。

function _new(base,...args){
 var obj = {};
 obj.__proto__ = base.prototype;
 base.apply(obj, args);
 return obj;
}

function Funct(a) {
 this.a = a;
}
var f1 = new Funct(1);
console.log(f1.a); // 1

var f2 = _new(Funct, 1);
console.log(f2.a); // 1

箭头函数

箭头函数没有单独的this,在箭头函数的函数体中使用this时,会取得其上下文context环境中的this。箭头函数调用时并不会生成自身作用域下的this,它只会从自己的作用域链的上一层继承this。由于箭头函数没有自己的this指针,使用applycallbind仅能传递参数而不能动态改变箭头函数的this指向,另外箭头函数不能用作构造器,使用new实例化时会抛出异常。

window.name = 1;
var obj = {
 name: 11,
 say: function(){
 const f1 = () => {
 return this.name;
 }
 console.log(f1()); // 11 // 直接调用者为window 但是由于箭头函数不绑定this所以取得context中的this即obj对象
 const f2 = function(){
 return this.name;
 }
 console.log(f2()); // 1 // 直接调用者为window 普通函数所以
 return this.name;
 }
}

console.log(obj.say()); // 11 // 直接调用者为obj 执行过程中的函数内context的this为obj对象

示例

function s(){
 console.log(this);
}

// window中直接调用 // 非 use strict
s(); // Window // 等同于window.s(),调用者为window
// window是Window的一个实例 // window instanceof Window //true

// 新建对象s1
var s1 = {
 t1: function(){ // 测试this指向调用者
 console.log(this); // s1
 s(); // Window // 此次调用仍然相当 window.s(),调用者为window
 },
 t2: () => { // 测试箭头函数,this并未指向调用者
 console.log(this);
 },
 t3: { // 测试对象中的对象
 tt1: function() {
 console.log(this);
 } 
 },
 t4: { // 测试箭头函数以及非函数调用this并未指向调用者
 tt1: () => {
 console.log(this);
 } 
 },
 t5: function(){ // 测试函数调用时箭头函数的this的指向,其指向了上一层对象的调用者
 return {
 tt1: () => {
 console.log(this);
 }
 }
 }
}
s1.t1(); // s1对象 // 此处的调用者为 s1 所以打印对象为 s1
s1.t2(); // Window
s1.t3.tt1(); // s1.t3对象
s1.t4.tt1(); // Window
s1.t5().tt1(); // s1对象

到此这篇关于深入理解Js中的this的文章就介绍到这了,更多相关深入理解Js中的this内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
JS操作图片(增,删,改) 例子
Apr 17 Javascript
js格式化时间和js格式化时间戳示例
Feb 10 Javascript
jQuery构造函数init参数分析续
May 13 Javascript
学习JavaScript事件流和事件处理程序
Jan 25 Javascript
AngularJS在IE下取数据总是缓存问题的解决方法
Aug 05 Javascript
JS验证input输入框(字母,数字,符号,中文)
Mar 23 Javascript
详解VueJS 数据驱动和依赖追踪分析
Jul 26 Javascript
vue-resouce设置请求头的三种方法
Sep 12 Javascript
vue.js实现简单轮播图效果
Oct 10 Javascript
vue 中基于html5 drag drap的拖放效果案例分析
Nov 01 Javascript
JS中的算法与数据结构之队列(Queue)实例详解
Aug 20 Javascript
html中两种获取标签内的值的方法
Jun 16 jQuery
Vue中使用wangeditor富文本编辑的问题
Feb 07 #Vue.js
vue使用lodop打印控件实现浏览器兼容打印的方法
Feb 07 #Vue.js
js基于canvas实现时钟组件
Feb 07 #Javascript
nestjs中异常过滤器Exceptionfilter的具体使用
Feb 07 #Javascript
js实现类选择器和name属性选择器的示例步骤
Feb 07 #Javascript
vue如何使用rem适配
Feb 06 #Vue.js
如何管理Vue中的缓存页面
Feb 06 #Vue.js
You might like
php框架Phpbean说明
2008/01/10 PHP
Discuz! Passport 通行证整合
2008/03/27 PHP
php之Memcache学习笔记
2013/06/17 PHP
php ckeditor上传图片文件名乱码解决方法
2013/11/15 PHP
PHP下载文件函数与用法示例
2019/09/27 PHP
Ext JS Grid在IE6 下宽度的问题解决方法
2009/02/15 Javascript
js下通过prototype扩展实现indexOf的代码
2010/12/08 Javascript
如何用JavaScript动态呼叫函数(两种方式)
2013/05/03 Javascript
jquery css 设置table的奇偶行背景色示例
2014/06/03 Javascript
JavaScript将XML转成JSON的方法
2015/03/12 Javascript
js style.display=block显示布局错乱问题的解决方法
2016/09/21 Javascript
JavaScript常用正则验证函数实例小结【年龄,数字,Email,手机,URL,日期等】
2017/01/23 Javascript
微信小程序 es6-promise.js封装请求与处理异步进程
2017/06/12 Javascript
vue的toast弹窗组件实例详解
2018/05/14 Javascript
vue form check 表单验证的实现代码
2018/12/09 Javascript
解决Vue+Electron下Vuex的Dispatch没有效果问题
2019/05/20 Javascript
JS如何实现动态添加的元素绑定事件
2019/11/12 Javascript
[03:03]DOTA2校园争霸赛 济南城市决赛欢乐发奖活动
2013/10/21 DOTA
举例讲解Python设计模式编程中的访问者与观察者模式
2016/01/26 Python
利用numpy+matplotlib绘图的基本操作教程
2017/05/03 Python
Python探索之Metaclass初步了解
2017/10/28 Python
Django实现文件上传下载
2019/10/06 Python
基于Python实现扑克牌面试题
2019/12/11 Python
python实现名片管理器的示例代码
2019/12/17 Python
pytorch进行上采样的种类实例
2020/02/18 Python
python爬虫利用代理池更换IP的方法步骤
2021/02/21 Python
html5中 media(播放器)的api使用指南
2014/12/26 HTML / CSS
中国高端家电购物商城:顺电
2018/03/04 全球购物
酒店拾金不昧表扬信
2014/01/18 职场文书
财务工作疏忽检讨书
2014/09/11 职场文书
个人公司授权委托书范本
2014/10/12 职场文书
2015新年联欢晚会开场白
2014/12/14 职场文书
集团财务总监岗位职责
2015/04/03 职场文书
门球健将观后感
2015/06/16 职场文书
2015年秋季校长开学典礼致辞
2015/07/29 职场文书
学习《中小学教师职业道德规范》心得体会
2016/01/18 职场文书