前言
关于this的指向问题是前端面试中常考的知识点,也是我们开发学习中较难理解的问题。作为JavaScript的基础,需要我们彻底理解这一关键词。this作为JavaScript中非常复复杂的机制,值得我们付出更大的代价来学习理解。这里分享一下我的学习笔记。
正文
1.this是什么?this指向什么?
学习this之前首先要知道this永远指向一个对象,this就是函数运行时候所在的环境,我们在学习执行上下文的时候提到,每一个执行上下文都包含三个重要对象,分别是变量对象、作用域链和 this,JavaScript支持运行环境动态切换,也就是说,this的指向是动态的,并不是固定的指向一个对象,this的指向完全取绝于函数调用的位置。接下来我们看下下面的代码:
var a=1 function foo(){ var a=3 console.log(a) } foo()//3
上面的代码执行用到函数的作用域问题,foo()函数执行的时候,函数内部定义一个变量a等于3,然后输出打印a,此时foo上下文中存在a变量,因此输出3,若函数foo()内部没有定义a这个变量,在执行打印这句代码的时候,函数内部找不到该变量,此时会沿着作用域链向上次查找,即全局作用域,此时打印结果便为1。
var a=1 function foo(){ var a=3 console.log(this.a) } foo()//1
上面的代码执行结果会打印出1,对比第一段代码你会发现就这里多了this这一个关键词,为什么加了this之后输出结果会发生改变呢?莫非此时this指向window全局对象?
var a=1 function foo(){ var a=3 console.log(this.a) } var obj={a:100,foo:foo} obj.foo()//100
再来看下上面的代码,打印结果变为100,对比前面两段代码,foo函数增加了obj对象的一层引用,输出的this.a结果再次发生改变?难道此时this指向obj对象?
this的指向为什么会发生改变,this的指向到底什么时候发生的?是因为函数在JavaScript中既可以当作值传递和返回,也可以当作对象和构造函数。所有函数在运行的时候才能确定其当前的运行环境,所以this就产生了,this会根据函数的运行环境的改变而改变,同时,函数中的this也只能在函数运行时最终确定其运行环境。
2.this不指向它自身。
function foo(num) { console.log( "foo: " + num ); // 记录 foo 被调用的次数 this.count++; } foo.count = 0; for (var i=0; i<5; i++) { foo( i ); } console.log("foo.count:"+foo.count) //foo: 0 //foo: 1 //foo: 2 //foo: 3 //foo: 4 //foo.count:0
上面的代码打印输出foo:01234,说明foo函数被调用了五次,然后打印foo.count输出还是0,说明this.count中的this并不是指向的函数foo本身,foo.count只是函数foo的一个属性,this无法指向他本身。我们可以通过arguments.callee来引用当前正在执行的函数对象,即对象本身。
3.this不指向它的作用域。
另一种常见的误解就是this指向函数的作用域。我们需要牢记,this在任何情况下都不指向函数的词法作用域,this的指向完全取决于函数调用的位置。因此不能使用this来引用一个函数词法作用域内部的东西。
4.this的绑定规则。
this是什么?当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的 其中一个属性,会在函数执行的过程中用到。
如何寻找函数的调用位置,从而判断函数在执行过程中会如何绑定 this?
首先找到调用位置:使用开发者工具得到调用栈,然后找到栈中第二个元素,这就是真正的调用位置。
通过函数调用的位置来确定函数绑定的this对象,需要遵守以下四条规则:
(1)默认绑定
function foo() { console.log( this.a ); } var a = 2; foo(); // 2
上面的代码中,foo()函数直接调用,不带任何修饰的函数引用进行调用,因此采用默认绑定,此时this指向window对象,无法应用其他规则。在非 strict mode 下时,默认绑定才能绑定到全局对象,严格模式下 this 绑定在undefined。
(2)隐式绑定–需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; obj.foo(); // 2
上面的代码中,foo函数被调用时 obj 对象 “ 拥有 ” 或者 “ 包含 ” 它。当函数引 用有上下文对象时,隐式绑定规则会把函数调用中的 this绑定到这个上下文对象,因为调 用 foo() 时 this 被绑定到 obj,因此 this.a 和 obj.a 是一样的。
function foo() { console.log(this.a); } var obj2 = { a: 2, fn: foo }; var obj1 = { a: 1, o1: obj2 }; obj1.o1.fn();//2
上面的代码需要值得注意的是:对象属性引用链中只有最顶层或者说最后一层会影响调用位置。
function foo() { console.log( this.a ); } var obj = { a: "local", foo: foo }; var bar = obj.foo; // 函数别名! var a = " global"; // a 是全局对象的属性 bar(); // "global"
上面的代码,虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
function foo(){ console.log(this.a) } var a="global" var obj={a:"local",foo:foo} var bar=obj.foo function doFnc(fn){ fn() } doFnc(bar)//global
上面的代码需要注意的是:被隐式绑定的函数会丢失绑定对象。
(3)显示绑定或者硬绑定–call,apply,bind直接指定this的绑定对象
call() apply() bind()直接指定 this 的绑定对象,返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。但是如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值 在调用时会被忽略,实际应用的是默认绑定规则。foo.call( obj );以在调用 foo 时强制把它的 this 绑定到 obj 上。
(4)new绑定
JavaScript 中 new 的机制实 际上和面向类的语言完全不同。
在 JavaScript 中,构造函数只是一些 使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上, 它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。包括内置对象函数(比如 Number(..))在内的所有函数都可 以用 new 来调用,这种函数调用被称为构造函数调用。
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
1. 创建(或者说构造)一个全新的对象。
2. 这个新对象会被执行 [[ 原型 ]] 连接。
3. 这个新对象会绑定到函数调用的 this。
4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象
new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。
5.绑定的优先和一些特殊情况。
判断优先级:
(1)函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。 var bar = new foo()
(2)函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。 var bar = foo.call(obj2)
(3)函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。 var bar = obj1.foo()
(4)如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到 全局对象。 var bar = foo()
特殊情况:
(1)间接绑定
function foo() { console.log( this.a ); } var a = 2; var o = { a: 3, foo: foo }; var p = { a: 4 }; o.foo(); // 3 (p.foo = o.foo)(); // 2 赋值表达式 p.foo = o.foo 的返回值是目标函数的引用
对比隐式绑定
function foo() { console.log( this.a ); } var a = 2; var o = { a: 3, foo: foo }; var p = { a: 4 ,fo:o.foo}; p.fo() //4 对比间接引用
(2)事件绑定中的this
1.行内绑定 <input type='button' onclick="this"> <input onclick="clickFuc"> function(){ this ...} //this指向window对象 2.事件监听与动态绑定 <input type="button" value="按钮" id="btn"> <script> var btn = document.getElementById('btn'); btn.onclick = function(){ this ; // this指向本节点对象 }</script> //动态绑定的事件本身就是对应节点的一个属性,重新赋值为一个匿名函数,this自然就指向了本节点对象
(3)箭头函数
ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。
总结
以上就是本文的全部内容,希望给读者带来些许的帮助和进步,方便的话点个关注,小白的成长之路会持续更新一些工作中常见的问题和技术点。