本文主要参考 《你不知道的 js》想要了解更多建议自己去看看
什么是执行上下文
-
相当于是对数据的预处理
-
执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行
-
这里再简单说说我对执行上下文和作用于的理解吧 不一定正确希望指正
- 作用域在声明的时候就已经确定了
- 而执行上下文呢 重点还是在'执行' 二字吧 前面知道它是对数据做的预处理 那么只有在执行的时候它才会存在 比如 this 指向就是调用才确定的
对 this 的误解
指向自身
-
很容易把 this 理解成指向函数自身,这个推断从英语的语法角度来说是说得通的。
-
function foo(num) { console.log( "foo: " + num ); // 记录foo被调用的次数 this.count++; } foo.count = 0; var i; for (i=0; i<10; i++)="" {="" if="" (i=""> 5) { foo( i ); } } // foo: 6 // foo: 7 // foo: 8 // foo: 9 // foo 被调用了多少次? console.log( foo.count ); // 0- 如果指向自身 那么因该输出 4 说明并不是指向自身
它的作用域
- 认为 this 指向函数的作用域 这个问题有些复杂 因为他有时是正确的
- 需要明确的是 this 任何情况下都不指向函数的词法作用域
this 是什么
- 当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)
- 这个记录会包含函数在 哪里被调用(调用栈)、函数的调用方法、传入的参数等信息
- this就是记录的其中一个属性,会在 函数执行的过程中用到
- this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时 的各种条件
- this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式
调用位置
-
分析函数的调用位置能够更准确的确认 this 的引用
-
最重要的是 分析调用栈
-
function baz() { // 当前调用栈是:baz // 因此,当前调用位置是全局作用域 console.log( "baz" ); bar(); // <-- bar的调用位置 } function bar() { // 当前调用栈是baz -> bar // 因此,当前调用位置在baz中 console.log( "bar" ); foo(); // <-- foo的调用位置 } function foo() { // 当前调用栈是baz -> bar -> foo // 因此,当前调用位置在bar中 console.log( "foo" ); } baz(); // <-- baz的调用位置 - 可以把调用栈想象成一个函数调用链
- 如果想要分析 this 的绑定 找到栈中第二个元素 这就是真正的调用位置
-
this 的四种绑定规则
默认绑定
-
是最常用的函数调用类型:独立函数调用
-
可以把这条规则看作是无法应用其他规 则时的默认规则
-
function foo() { console.log( this.a ); } var a = 2; foo(); // 2 - 这种就是最常见的 this 默认绑定全局对象 window
- 但是如果是严格模式的话 this 指向是 undefined
-
隐式绑定
-
另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含
-
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; obj.foo(); // 2 -
这里是先声明 foo 然后被当作引用属性添加到 obj 中
-
但是无论是 直接在obj中定义还是先定义再添加为引用属性,这个函数严格来说都不属于 obj 对象
-
他保存的只是 foo 的指针
- 这里要知道 js 默认基本类型保存在栈中 引用类型在堆中 然后栈中保存一个指向堆的指针
-
隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象 obj 因此 obj.a == this.a
-
-
隐式丢失
-
一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑 定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式
-
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var bar = obj.foo; // 函数别名! var a = "oops, global"; // a是全局对象的属性 bar(); // "oops, global" - 这里的 bar 就是另一个指向 foo 对象的指针 这个变量 bar 是在全局对象中的 那么 foo 的 this 就变的和默认绑定一样了
- 虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一 个不带任何修饰的函数调用,因此应用了默认绑定
-
function foo() { console.log( this.a ); } function doFoo(fn) { // fn其实引用的是foo fn(); // <-- 调用位置! } var obj = { a: 2, foo: foo }; var a = "oops, global"; // a是全局对象的属性 doFoo( obj.foo ); // "oops, global" - 这里是以回调函数的形式传入 这里调用的其实也是 foo 本身了
- 参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子 一样
-
-
显示绑定
-
在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属 性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上
-
那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?
-
可以使用内置的几个方法来实现显示绑定
-
function foo() { console.log( this.a ); } var obj = { a:2 }; foo.call( obj ); // 2 -
对比call 、bind 、 apply 传参情况下
-
var name = '小王', age = 17 var obj = { name: '小张', objAge: this.age, myFun: function() { console.log(this.name + '年龄' + this.age, '来自' + fm + '去往' + t) } } var bd = { name: 'dlow', age: 99 } obj.myFun.call(db,'成都','上海'); // 德玛 年龄 99 来自 成都去往上海 obj.myFun.apply(db,['成都','上海']); // 德玛 年龄 99 来自 成都去往上海 obj.myFun.bind(db,'成都','上海')(); // 德玛 年龄 99 来自 成都去往上海 obj.myFun.bind(db,['成都','上海'])(); // 德玛 年龄 99 来自 成都, 上海去往 undefined -
call 、bind 、 apply 这三个函数的第一个参数都是 this 的指向对象,第二个参数差别就来了:
-
call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面 obj.myFun.call(db,'成都', ... ,'string' ) 。
-
apply 的所有参数都必须放在一个数组里面传进去 obj.myFun.apply(db,['成都', ..., 'string' ]) 。
-
bind 除了返回是函数以外,它 的参数和 call 一样。
当然,三者的参数不限定是 string 类型,允许是各种类型,包括函数 、 object 等等!
-
-
硬绑定
-
但是显示绑定无法解决隐式丢失的情况
-
可以使用变种 硬绑定
-
function foo() { console.log( this.a ); } var obj = { a:2 }; var bar = function() { foo.call( obj ); }; bar(); // 2 setTimeout( bar, 100 ); // 2 // 硬绑定的bar不可能再修改它的this bar.call( window ); // 2 -
原理是 创建了一个 bar() 并且在内部手动调用了 foo.call(obj) 强制把 foo 的 this 绑定到了 obj 上
-
无论之后如何调用 bar 他总是会手动在 obj 上调用 foo 这就是硬绑定
-
硬绑定的应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值:
-
function foo(something) { console.log( this.a, something ); return this.a + something; } var obj = { a:2 }; var bar = function() { return foo.apply( obj, arguments ); }; var b = bar( 3 ); // 2 3 console.log( b ); // 5
-
-
-
另一种使用方法是创建一个可以重复使用的辅助函数:
-
function foo(something) { console.log( this.a, something ); return this.a + something; } // 简单的辅助绑定函数 function bind(fn, obj) { return function() { return fn.apply( obj, arguments ); }; } var obj = { a:2 }; var bar = bind( foo, obj ); var b = bar( 3 ); // 2 3 console.log( b ); // 5 -
所以内置的 bind 方法就实现了这个功能
-
bind 会返回一个硬绑定的新函数 会把参数设置为 this 的上下文并调用原始函数
-
function foo(something) { console.log( this.a, something ); return this.a + something; } var obj = { a:2 }; var bar = foo.bind( obj ); var b = bar( 3 ); // 2 3 console.log( b ); // 5
-
-
new 绑定
-
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作
-
1.创建一个新对象
-
2.将新对象的 proto 指向构造函数的 prototype 属性
-
3.将构造函数的 this 指向新对象
-
4.执行构造函数内部的代码
-
5.将新对象返回
-
let objA = (function { let obj = {} obj._proto_ = CreateObj.prototype CreateObj.call(obj, 'A') return obj })() console.log(objA.name)
-
-
优先级
- 判断this 可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的顺序来 进行判断:
-
-
函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
var bar = new foo()
-
-
-
函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
var bar = foo.call(obj2)
-
-
-
函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
var bar = obj1.foo()
-
-
-
如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
var bar = foo()
-
补充
-
箭头的函数没有自己的this,this是靠包住 箭头函数的普通函数给的
- 1.找普通函数
- 2.这个普通函数要包住箭头函数
- 3.一个误区就是很多人认为 {} 内部的就是作用域了 但是 对象的 {} 不属于块级作用域 那么他会继续向上层查找 很可能根据作用域链查找到 window