js基础梳理-关于this常见指向问题的分析
首先,依然回顾《js基础梳理-究竟什么是执行上下文栈(执行栈),执行上下文(可执行代码)?》中的
3.执行上下文的生命周期
3.1 创建阶段
- 生成变量对象(Variable object, VO)
 - 建立作用域链(Scope chain)
 - 确定this指向
 
3.2 执行阶段
- 变量赋值
 - 函数引用
 - 执行其他代码
 
我们已经梳理了在执行上下文中的变量对象是如何生成的以及作用域链是如何建立的。本篇文章就继续梳理下,执行上下文的this指向是如何确定的。
首先,执行上下文分全局执行上下文和函数执行上下文,在浏览器环境的全局执行上下文中,this指向全局对象,也就是window(接下来本篇文章都仅只浏览器环境)。这也相对简单,没有那么多复杂的情况需要考虑。
而在函数执行上下文中,确定this指向发生在函数执行上下文的创建阶段,而函数执行上下文又是在函数被调用后才产生的。因此,不难理解:this的指向,是在函数被调用的时候确定的。而不是函数声明的时候确定的。而确定this的指向难就难在函数被调用的方式是多种多样的,所以我们就需要从函数执行的各种方式分别去分析this的指向。
1. this与普通函数执行(默认绑定)
- 当一个函数执行不带任何修饰时,使用默认绑定规则。
 - 默认绑定:函数体如果在非严格模式下,this绑定到window,严格模式下绑定到undefined。
 
// 1.1 函数体在非严格模式下的全局函数执行
function fn () {
    console.log(this)
}
fn1()   // => window
// 1.2 函数体在严格模式下的全局函数执行
'use strict'
function fn () {
    console.log(this)
}
fn()    // => undefined
// 1.3 函数体在非严格模式下的函数中的函数执行
function fn1 () {
    function fn2 () {
        console.log(this)
    }
    fn2()
}
fn1()   // => window
// 1.4 函数体在严格模式下的函数中的函数执行
'use strict'
function fn1 () {
    function fn2 () {
        console.log(this)
    }
    fn2()
}
fn1()   // => undefined
// 1.5 函数体在非严格模式下,而函数调用在严格模式下时, this依然指向window
function fn () {
    console.log(this)
}
(function () {
    'use strict'
    fn()    // => window
})()
2. this与对象中的方法执行(隐式绑定)
2.1 无论是否是严格模式,当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。
// 2.1.1函数直接在对象中声明
var obj = {
    a: 1,
    test: function () {
        console.log(this.a)
    }
}
obj.test();   // => 1
// 2.1.2 函数先声明,再由对象引用
function test () {
    console.log(this.a)
}
var obj = {
    a: 1,
    test: test
}
obj.test();    // => 1
2.2 对象属性引用链中只有最顶层或者说只有最后一层会影响调用位置
// 2.2.1 多层对象引用,this指向离函数调用最近的对象
function test () {
    console.log(this.a)
}
var obj2 = {
    a: 2,
    test: test
}
var obj1 = {
    a: 1,
    obj2: obj2
}
obj1.obj2.test()    // => 2
2.3 隐式丢失:被隐式绑定的函数可能会丢失绑定对象。
// 2.3.1 将obj.foo当作函数别名赋值给一个变量
function foo () {
    console.log(this.a)
}
var obj = {
    a: 2,
    foo: foo
}
var bar = obj.foo   // 函数别名
var a = '全局属性'
bar()   // => 全局属性
在2.3.1中,虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar() 其实是一个不带任何修饰的普通函数调用。因此也使用默认绑定规则。
// 2.3.2 将obj.foo当作bar的回调函数。
function foo () {
    console.log(this.a)
}
function bar (fn) {
    fn()
}
var obj = {
    a: 2,
    foo: foo
}
var a = '全局属性'
bar(obj.foo)    // => 全局属性
大家都知道所有的函数的参数都是按值传递的,(都是栈内数据的拷贝)。
基本类型传的是值本身(因为直接把值存在栈内),引用类型传的是对象在内存里面的地址(因为复杂对象在堆内,所以在栈里存对象所在的堆地址)。
因此 bar(obj.foo) 执行时,参数fn实际上引用的是foo。而foo函数执行其实就是一个不带任何修饰的普通函数调用。所以它也使用默认绑定规则。
2.3.4
由此可扩展到 setInterval, setTimeout,以及匿名函数中的this也是使用的默认绑定规则。即非严格模式下,this指向window,严格模式下,this指向undefined。
3.this与call,apply,bind(显示绑定)
显示绑定规则:this指向第一个参数。
3.1 call
// 3.1.1
var xw = {
    name : "小王",
    gender : "男",
    age : 24,
    say : function(school,grade) {
        console.log(this.name + " , " + this.gender + " ,今年" + this.age + " ,在" + school + "上" + grade);
    }
}
var xh = {
    name : "小红",
    gender : "女",
    age : 12
}
xw.say.call(xh, "实验小学", "六年级")   // => 小红 , 女 ,今年12 ,在实验小学上六年级
在3.1.1代码示例中,当调用say时强制把它的this绑定到了xh上。
3.2 apply
// 3.2.1
var xw = {
    name : "小王",
    gender : "男",
    age : 24,
    say : function(school,grade) {
        console.log(this.name + " , " + this.gender + " ,今年" + this.age + " ,在" + school + "上" + grade);
    }
}
var xh = {
    name : "小红",
    gender : "女",
    age : 12
}
xw.say.apply(xh,["实验小学","六年级"])  // => 小红 , 女 ,今年12 ,在实验小学上六年级
3.3 bind
// 3.3.1
var xw = {
    name : "小王",
    gender : "男",
    age : 24,
    say : function(school,grade) {
        alert(this.name + " , " + this.gender + " ,今年" + this.age + " ,在" + school + "上" + grade);
    }
}
var xh = {
    name : "小红",
    gender : "女",
    age : 12
}
xw.say.bind(xh)("实验小学","六年级")  // => 小红 , 女 ,今年12 ,在实验小学上六年级
通过以上这些例子,其实也可以明显的看到call,apply,bind的区别。
3.4 显示绑定的变种-硬绑定
之前说到隐式丢失的问题,而显示绑定的一个变种可以解决隐式丢失的问题,这种方式被称之为硬绑定。
// 2.3.2 将obj.foo当作bar的回调函数。
function foo () {
    console.log(this.a)
}
function bar (fn) {
    fn()
}
var obj = {
    a: 2,
    foo: foo
}
var a = '全局属性'
bar(obj.foo)    // => 全局属性
将其修改成
// 3.4.1 利用call方法解决隐式丢失的问题
function foo(){
     console.log(this.a);
 }
 function bar(fn){
     fn.call(obj);
 }
 var obj = {
     a:2,
     foo:foo
 }
 var a = "全局属性";
 bar(obj.foo);  // => 2
这里依旧是创建了 bar()这个函数,但是在其内部手动调用了obj.foo.call(obj),把foo强制绑定到了obj对象,之后无论如何调用bar(),它总会手动在obj上调用foo.
再看看隐式丢失的代码示例2.3.1
// 2.3.1 将obj.foo当作函数别名赋值给一个变量
function foo () {
    console.log(this.a)
}
var obj = {
    a: 2,
    foo: foo
}
var bar = obj.foo   // 函数别名
var a = '全局属性'
bar()   // => 全局属性
将其修改成
function foo(){
     console.log(this.a);
 }
 var obj = {
     a:2,
     foo:foo
 };
 var bar = obj.foo.bind(obj);
 var a = "global";
 bar(); // => 2
总结下“显示绑定三人组”
共同点:
1、都用于控制this指向;
2、第一个参数都是this需要指向的对象,也就是上下文;
3、都可以后续参数传递;
4、没有任何参数时或者第一个参数是null时,this都指向全局对象window >
区别:
1、call后面的参数与say方法中的参数是一一对应的,而apply的第二个参数是一个数组,数组中的元素是和say方法中一一对应的。所以当传入的参数数目不确定时,多使用apply。
2、call、apply绑定后立刻执行,bind是延迟执行。换言之,当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,就使用bind()方法吧
扩散性思考:
call,apply,bind还有什么实际使用场景?它们的原理是什么?如何自己模拟实现这三个方法?
4. this与构造函数调用(new 绑定)
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:
- 创建(或者说构造)一个全新的对象
 - 这个新对象会被执行[[原型]]链接。
 - 这个新对象会绑定到函数调用的this.
 - 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
 
在这里主要关心第1,3,4步。
function Foo(a) {
    this.a = a;
}
var bar = new Foo(2);
console.log(bar.a)  // 2
使用new来调用Foo(...)时,会构造一个新对象并把它绑定到foo(...)调用中的this上,new是最后一种可以影响到函数调用时this绑定行为的方法,称之为new绑定。
5.this与箭头函数
5.1箭头函数中的this指向包裹它的第一个普通函数中的this
// 5.1 箭头函数外部是普通函数调用时
function fn () {
  return () => {
    return () => {
      console.log(this)
    }
  }
}
fn()()()    // => window
在这个例子中,因为包裹箭头函数的第一个普通函数是 fn,而fn的this指向window, 所以箭头函数中的 this也是 window。
以上的例子,多数为单个规则。
这里推荐一种方法:
- 先看看它定义时,是不是在箭头函数内。如果是,那么this就指向包裹它的第一个普通函数中的this。
 - 不管它是不是箭头函数,我们都需要继续寻找它的this指向,再看它的调用方式。如果它是被 new 调用,那么this指向实例。
 - 如果调用的时候看到了bind,call,apply。那么大部分时候this都指向第一个参数。
 - 如果调用时既没有看到new,也没有看到bind,call,apply,那么大部分时候this指向window(非严格模式下)。
 
在写这篇文章的时候,本来标题是关于this指向的全面分析,后来想想,其实也并不全面。真正的宗师级别,会是多个规则夹在一起去分析this,那样的话,篇幅肯定太长了,而且一时半会其实也记不了那么多。而且就这篇文章所讲的,其实也够大家去理解分析大部分基础的this指向问题了。有兴趣的话,可以自己去翻阅一下《你不知道javascritp·上卷》这本书,里面还有一些我个人觉得比较少见而没有列出来的this指向问题,比如我的文章中提到了硬绑定,那其实也会有软绑定。
在这里也贴一篇蚂蚁金服前端的博文,是一个多个规则综合应用判断this指向的题。很多时候,当我们以为自己懂了,而总是会有人更深入的去挖掘我们之前没有想到的知识点,学无止境。
js基础梳理-关于this常见指向问题的分析的更多相关文章
- js基础梳理-如何理解作用域和作用域链?
		
本文重点是要梳理执行上下文的生命周期中的建立作用域链,在此之前,先回顾下关于作用域的一些知识. 1.什么是作用域(scope)? 在<JavaScritp高级程序设计>中并没有找到确切的关 ...
 - js基础梳理-究竟什么是执行上下文栈(执行栈),执行上下文(可执行代码)?
		
日常在群里讨论一些概念性的问题,比如变量提升,作用域和闭包相关问题的时候,经常会听一些大佬们给别人解释的时候说执行上下文,调用上下文巴拉巴拉,总有点似懂非懂,不明觉厉的感觉.今天,就对这两个概念梳理一 ...
 - js基础梳理-究竟什么是变量对象,什么是活动对象?
		
首先,回顾下上篇博文中js基础梳理-究竟什么是执行上下文栈(执行栈),执行上下文(可执行代码)?的执行上下文的生命周期: 3.执行上下文的生命周期 3.1 创建阶段 生成变量对象(Variable o ...
 - js基础梳理-内存空间
		
我估计有很多像我这样非计算机专业的人进入到前端之后,总是在写业务代码,思考什么什么效果如何实现,导致很多基础概念型的东西都理解得并不太清楚.经常一碰到群里讨论的些笔试题什么的,总觉得自己像是一个假前端 ...
 - js异步梳理:1.从浏览器的多进程到JS的单线程,理解JS运行机制
		
大家很早就知道JS是一门单线程的语言.但是也时不时的会看到进程这个词.首先简单区分下线程和进程的概念 1. 简单理解进程 - 进程是一个工厂,工厂有它的独立资源 - 工厂之间相互独立 - 线程是工厂中 ...
 - 前端工程师面试问题归纳(一、问答类html/css/js基础)
		
一.参考资源 1.前端面试题及答案整理(一) 2.2017年前端面试题整理汇总100题 3.2018最新Web前端经典面试试题及答案 4.[javascript常见面试题]常见前端面试题及答案 5.W ...
 - JS基础-全方面掌握继承
		
前言 上篇文章详细解析了原型.原型链的相关知识点,这篇文章讲的是和原型链有密切关联的继承,它是前端基础中很重要的一个知识点,它对于代码复用来说非常有用,本篇将详细解析JS中的各种继承方式和优缺点进行, ...
 - js 基础篇(点击事件轮播图的实现)
		
轮播图在以后的应用中还是比较常见的,不需要多少行代码就能实现.但是在只掌握了js基础知识的情况下,怎么来用较少的而且逻辑又简单的方法来实现呢?下面来分析下几种不同的做法: 1.利用位移的方法来实现 首 ...
 - 【 js 基础 】Javascript “继承”
		
是时候写一写 "继承"了,为什么加引号,因为当你阅读完这篇文章,你会知道,说是 继承 其实是不准确的. 一.类1.传统的面向类的语言中的类:类/继承 描述了一种代码的组织结构形式. ...
 
随机推荐
- Confluence 6 workbox 配置查询间隔
			
查询间隔在Confluence 服务器中的 workbox 被用来显示应用内通知和任务. 激活的查询间隔(Active polling interval) Confluence 将会等待多少时间(秒) ...
 - django----多对多数据添加
			
class UserInfo(models.Model): """ 用户表:既有班主任也有老师 """ username = models. ...
 - 从认识面向对象到构造函数的标准写法(构造函数的继承、多态、ECMA6中新代替语法class) - 下
			
笔记一个包含:认识面向对象.构造函数的封装.继承.多态.ECMA6中新代替语法class 下:包括构造函数的继承.多态.ECMA6中新代替语法class 构造函数的继承 从父一级延续下来的属性和功能( ...
 - Python使用正则表达式分割字符串
			
re.split(pattern, string, [maxsplit], [flags]) pattern:表示模式字符串,由要匹配的正则表达式转换而来. string:表示要匹配的字符串. max ...
 - Python字典(Dictionary)
			
Python中字典与类表类似,也是可变序列,不过与列表不同,他是无序的可变序列,保存的内容是以键 - 值对的形式存放的.类似我们的新华字典,他可以把拼音和汉字关联起来,通过音节表可以快速的找到想要的字 ...
 - 论文阅读笔记十四:Decoupled Deep Neural Network for Semi-supervised Semantic Segmentation(CVPR2015)
			
论文链接:https://arxiv.org/abs/1506.04924 摘要 该文提出了基于混合标签的半监督分割网络.与当前基于区域分类的单任务的分割方法不同,Decoupled 网络将分割与分类 ...
 - ubuntu16.04安装skype
			
ubuntu16.04安装skype 一句命令搞定 wget https://repo.skype.com/latest/skypeforlinux-64.deb && sudo dp ...
 - iptables命令
			
iptables命令是Linux上常用的防火墙软件,是netfilter项目的一部分.可以直接配置,也可以通过许多前端和图形界面配置. 语法 iptables(选项)(参数) 选项 -t<表&g ...
 - Nancy 自寄宿
			
一:简介 Self Hosting 顾名思义,就是自己Host自己.也就是不需要依赖别的应用,而让应用本身就是服务.一个Console程序或者一个Winform程序都是一个应用,Self Hostin ...
 - java去除html代码中含有的html、js、css标签,获取文字内容
			
https://blog.csdn.net/u010882234/article/details/80585175