深入理解Js中的this
深入理解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
。现在大部分语言都采用静态作用域,比如C
、C++
、Java
、PHP
、Python
等等,具有动态作用域的语言有Emacs Lisp
、Common Lisp
、Perl
等。
全局作用域
直接声明在顶层的变量或方法就运行在全局作用域,借用函数的[[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
中基本数据类型String
、Number
、Boolean
、Null
、Undefined
、Symbol
,占用空间小且大小固定,值直接保存在栈内存中,是按值访问,对于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
显示绑定
如果我们想把某个函数强制在某个环境即对象上,那么就可以使用apply
、call
、bind
强制绑定this
去执行即可,每个Function
对象都存在apply()
、call()
、bind()
方法,其作用都是可以在特定的作用域中调用函数,等于设置函数体内this
对象的值,以扩充函数赖以运行的作用域,此外需要注意使用bind
绑定this
的优先级是大于apply
和call
的,即使用bind
绑定this
后的函数使用apply
和call
是无法改变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绑定
在JavaScript
中new
是一个语法糖,可以简化代码的编写,可以批量创建对象实例,在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
指针,使用apply
、call
、bind
仅能传递参数而不能动态改变箭头函数的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对象
每日一题
https://github.com/WindrunnerMax/EveryDay
参考
https://juejin.cn/post/6882527259584888845
https://www.cnblogs.com/raind/p/10767622.html
http://www.ruanyifeng.com/blog/2018/06/javascript-this.html
深入理解Js中的this的更多相关文章
- 怎么理解js中的事件委托
怎么理解js中的事件委托 时间 2015-01-15 00:59:59 SegmentFault 原文 http://segmentfault.com/blog/sunchengli/119000 ...
- 如何更好的理解js中的this,分享2段有意思的代码
关于js中this的浅析,大家可以点击[彻底理解js中this的指向,不必硬背]这篇博客了解. 今天遇到2段比较有意思的代码. ----------------第一段----------------- ...
- 图文结合深入理解 JS 中的 this 值
图文结合深入理解 JS 中的 this 值 在 JS 中最常见的莫过于函数了,在函数(方法)中 this 的出现频率特别高,那么 this 到底是什么呢,今天就和大家一起学习总结一下 JS 中的 th ...
- 深度理解js中var let const 区别
首先要理解js中作用域的概念 作用域:指的是一个变量的作用范围 1.全局作用域 直接写在script中的js代码,在js中,万物皆对象,都在全局作用域,全局作用域在页面打开时创建,在全局作用域中有一个 ...
- 如何理解js中的this和实际应用中需要避开哪些坑
this是什么 this就是函数内部的关键字 看下面例子理解js中的this // 例子1 function fnOne () { console.log(this) } 'use strict' f ...
- 深入理解JS中的对象(二):new 的工作原理
目录 序言 不同返回值的构造函数 深入 new 调用函数原理 总结 参考 1.序言 在 深入理解JS中的对象(一):原型.原型链和构造函数 中,我们分析了JS中是否一切皆对象以及对象的原型.原型链和构 ...
- 深入理解JS中的对象(三):class 的工作原理
目录 序言 class 是一个特殊的函数 class 的工作原理 class 继承的原型链关系 参考 1.序言 ECMAScript 2015(ES6) 中引入的 JavaScript 类实质上是 J ...
- 彻底理解js中this的指向,不必硬背。
首先必须要说的是,this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象(这句话有些问题,后面会解释为什么会有问题,虽然 ...
- js笔记——理解js中的call及apply
call及apply在js里经常碰得到,但一直感觉很陌生,不能熟练使用.怎样才能熟练应用呢? 为什么存在call和apply? 在javascript OOP中,我们经常会这样定义: function ...
随机推荐
- mysql使用全文索引实现大字段的模糊查询
0.场景说明 centos7 mysql5.7 InnoDB引擎 0.1创建表 DROP TABLE IF EXISTS tbl_article_content; CREATE TABLE tbl_a ...
- 第6章 未来的函数:生成器和promise
目录 1. 生成器函数 1.1 定义生成器函数 1.2 迭代器对象 1.3 对迭代器进行迭代 1.4 把执行权交给下一个生成器 2. 使用生成器 2.1 用生成器生成ID 2.2 用迭代器遍历DOM树 ...
- node.js 爬取图片
/** * _ooOoo_ * o8888888o * 88" . "88 * (| -_- |) * O\ = /O * ____/`---'\____ * . ' \\| |/ ...
- 关于软件架构中的b/s
**B/S架构 b/s只需要一个浏览器,用户就可以通过不同的网址访问不同的服务器程序. 优点:开发,安装,部署,维护简单 缺点:对硬件要求过高,用户的体验会受到影响 首先是资源分类:**可以分为静态资 ...
- .NET Core部署到linux(CentOS)最全解决方案,常规篇
本文为大家介绍使用 .NET Core部署到Linux服务器的方法,通过本文你将了解到Linux在虚拟机下的安装.Xshell,Xftp的使用方法.git在linux下的交互使用以及.net core ...
- 查看Java的汇编指令
在IDEA配置VM options,打印汇编指令 -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly windows系统 下载插件 hsdis-amd6 ...
- Nginx(七):location的使用以及nginx关闭原理
上一篇中,我们了解了如何nginx的配置原则及解析框架,以及解析location配置的具体实现,相信大家对该部分已经有了比较深刻的认识. 本篇,我们进一步来了解下,解析之后的配置,如何应用到实际中的吧 ...
- 支付宝沙箱环境使用(Alipay Easy SDK ) .Net示例
新版服务端 SDK(Alipay Easy SDK)适用于 Java.C#.PHP 编程语言,对开放能力的 API 进行了更加贴近高频场景的精心设计与裁剪,简化了服务端调用方式,让开发者享受极简编程体 ...
- 【Android初级】如何动态添加菜单项(附源码+避坑)
我们平时在开发过程中,为了灵活多变,除了使用静态的菜单,还有动态添加菜单的需求.今天要分享的功能如下: 在界面的右上角有个更多选项,点开后,有两个子菜单:关于和退出 点击"关于", ...
- Linux系统使用lvm扩展根分区
Linux系统使用lvm扩展根分区 背景:买的云主机虚拟机封装镜像是40G的系统盘,后期适用不规范或者其他需求需要扩展系统盘,而非挂载在一个盘至新建目录. 1.原本目录磁盘等信息: 2.使用vgdis ...