前言:
JavaScript这门语言是一门解释性语言,并不是编译型语言。值得注意的是,它并不是直接一行一行解释,先检查语法错误,如果没有错误则一行一行去解释执行,两者之间会有一步'预编译'的操作。
当谈及this的时候,通常会听到GO和AO的声音,心里想:wht? GO和AO是什么?
- AO: (Activation Object => 活跃对象) 存在于局部作用域,也叫全局执行期上下文
- GO: (Global Object => 全局对象) 存在于全局作用域中,也叫函数执行器上下文
总的来说整个步骤就是:
- 检查通篇代码的语法错误y
- 预编译(函数在执行前所要做的准备)
- 解释一行,执行一行
AO的理解:
预编译阶段,函数需要做的事情,首先创建一个AO对象,AO={},具体执行步骤,如下:
- 寻找函数的形参和变量声明
- 把实参赋值给形参
- 寻找函数声明,赋值函数体
- 执行函数
例子:
function fun(a, b) {
console.log(a);
c = 0;
var c;
a = 2;
b = 3;
console.log(b);
function b() {}
function d() {}
console.log(b);
}
fun(1);
// 执行结果是:1 3 3
Tip:值得注意的是:b的打印结果,并没有因为d的函数声明而改变b的值,因为
函数声明后才去执行函数,也就是先function b() {}再 b = 3。
GO的理解:
在产生函数作用域之前,会产生一个全局的作用域,首先创建一个GO对象,GO={},具体执行步骤,如下:
- 寻找变量声明
- 寻找函数声明,并赋值函数体
- 执行代码
例子:
console.log(a,b);
function a(){}
var b = function(){}
// 执行结果是:ƒ a(){} undefined
Tips:这也就是我们通常说的:函数声明(不是函数表达式)是会进行函数提升,提升至所在函数的顶部。而函数表达式没有函数提升,函数表达式执行的是变量提升。
AO和GO混合使用:
- 寻找变量声明
- 寻找函数声明,并赋值函数体
- 执行代码,并生成AO对象(即执行AO第一步)
- 实参赋值
- 找函数声明并赋值函数体
- 执行代码
例子:
function fun() {
console.log(a);
console.log(b);
if (a) {
var b = 2; //若换成 let b = 2 会存在暂时性死区,所以b值打印会报错
}
c = 3;
console.log(c);
}
var a;
fun();
a = 1;
// 执行结果是:undefined undefined 3
Tips:
- 可能你会觉得,第一次打印a的值为什么不是1?原因就是我函数内部有a这个变量,并且有值,不管是undefined还是具体的值,都是不会去全局进行查找的。
- 可能你会觉得,AO那里为什么不报错:Uncaught ReferenceError: b is not defined。因为在预编译阶段,是不管if语句的规则的,只要内部有变量就需要拿出来;执行的时候,是看if规则的,比如if的()中为true才会执行里面的代码。
上面的只是让我们了解this的预备动作,接下来切入正题了!
this:是执行上下文对象的一个属性,在JS中是一种类似于指针一样的存在,严格意义上来讲js中没有指针的概念,而浏览器环境下的全局变量就是window(并非全局变量就是window,比如在node中的全局变量不是window),一般情况下,this指向的就是函数执行的 上下文对象,全局变量也就是window。值得一提的是,函数执行才存在this指向的问题。
文章借鉴出处,# 想搞懂预编译,看这篇就够了!包括GO和AO→
this相关的 5 个绑定规则:
1.默认绑定:
即发生了函数的独立调用,没有绑定到某个对象上进行调用
例子:
console.log(this); // 默认绑定,指向window
function fun() {
console.log(this);
}
fun(); //函数的独立调用,指向window
var obj1 = {
name: "why",
foo: function() {
console.log(this); //谁调用就指向谁,指向obj1
function test() {
console.log(this);
}
test(); // 函数的独立调用,指向window
(function() {
console.log(this); // 函数的独立调用,指向window,立即执行函数的this指向始终指向window
})();
}
};
obj1.foo();
var obj2 = {
name: "why",
foo: function() {
function test() {
console.log(this); //指向window
}
return test;
}
};
obj2.foo()(); //因为这个是闭包函数,有个return所以可以看成, obj.foo()() => test()函数的独立调用,指向window
var obj3 = {
name: "why",
foo: function() {
console.log(this);
}
};
var bar = obj3.foo(); //这里的this指向obj3,函数的预编译先变量声明=>再函数声明=>执行代码,且是隐式调用
var bar = obj3.foo;
bar(); //这里的this指向window,bar持有foo的引用,且是独立调用
var car = function(){
console.log(this);
}
car(); // 这个car可以看成,上面bar的执行过程
Tips:上面的例子除了倒数第八行:var bar = obj3.foo(),其余全部都是函数的独立调用,也就是默认绑定,this的指向的也全是window。
2.隐式绑定:
也称弱绑定,即谁调用就指向谁
例子:
var obj1 = {
name: "why",
foo: function() {
console.log(this);
}
};
obj1.foo() // 谁调用就指向谁,指向obj1
// 只嵌套一层
var obj2 = {
foo(){
console.log(this);
}
}
var obj3 ={
bar:obj2.foo // bar持有obj2.foo的引用
}
obj3.bar() // 谁调用就指向谁,this指向obj3
// obj 再嵌套一层methods
let obj = {
methods: {
foo() {
console.log(this);
}
}
}
obj.methods.foo() // this指向 methods
// obj2 再嵌套一层methods
var obj2 = {
methods: {
foo() {
console.log(this);
}
}
}
var obj3 = {
bar: obj2.methods.foo // bar持有obj2.foo的引用
}
obj3.bar() // 谁调用就指向谁,this指向obj3
Tips:在隐式绑定情况下,this指向的是调用的对象。
3.显式绑定:
也称强绑定,如call、apply和bind方法
call、apply、bind的语法,如下:
call(obj,param1,param2,param3······),参数之间用逗号隔开,是一个参数列表
applay(obj,[param1、param2、param3······]),参数使用数组包含起来,是一个数组
bind(obj,param1,param2,param3······),参数之间用逗号隔开【和call使用语法一致】
使用call、apply、bind的语法的前提是它得是个函数,比如,只有函数才有call()方法,因为Function.prototype.call()
call、apply、bind的区别:
① 区别
三种方法无参的情况下,call(obj)和apply(obj)的作用是一样的。
例子:
var obj1 = {};
var fun = function() {
return this;
};
console.log(fun() == window); // true
console.log(fun.call(obj1) == obj1); // true
console.log(fun.apply(obj1) == obj1); // true
console.log(fun.bind(obj1)() == obj1); // true
var n = 123;
var obj2 = { n: 456 };
function a() {
console.log(this.n);
}
a(0); // 123
a.call(); // 123
a.call(null); // 123
a.call(undefined); // 123
a.call(window); // 123
a.call(obj2); // 456
补充说明:call、apply、bind方法的第一个参数,是一个对象。如果它们的第一个参数为空、null和undefined,则默认传入全局对象。下面换成apply和bind结果一样,只是bind后面再加一个()让其执行。可见绑定的参数,如果是null、undefined实际绑定的是全局作用域window,如果省去绑定的参数默认是绑定全局作用域。
② bind的特殊性
call和apply绑定完this会立即调用当前的函数,而bind绑定完this不会立即调用当前函数,而是将函数返回
例子:
var obj = {
user: "追梦子",
fn: function(e, ee) {
console.log(this.user);
console.log(e + ee);
}
};
var a = obj.fn;
a(1, 10); // undefined 11
// 因为这个this指向的是window, 而window.obj中才有user,所以第一个为undefined
var b = obj.fn;
b.call(obj, 1, 10); //'追梦子' 11
var c = obj.fn;
c.apply(obj,[1,10]); //'追梦子' 11
var d = obj.fn;
d.bind(obj,1,10)(); //'追梦子' 11
补充说明:通过强绑定call、apply和bind改变this的指向,this指向的是第一个参数,而这个参数一般而言是个对象。
③ 其他用法
使用call、apply、bind并不一定是为了改变this指向
// 先品味下官网说的这句话?如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg。
function add(){
console.log(this == window);
}
console.log(add()); // true , bind 函数的参数列表为空 => bind()
console.log(add(null)); // true , thisArg是null或undefined =>bind(null) 新函数的this指向的是执行作用域的 this
console.log(add(undefined)); // true
例子:
// 1.
let obj = { a: 1 };
function multiply(x, y, z) {
return x * y * z;
}
let fn = multiply.bind(undefined,10)
console.log(fn(20,30)); // 6000
// 2.
console.log(Math.max.apply(null, [10,20,30])); //30, apply可以默认将数组[10,20,30]转换为参数列表(10,20,30)
console.log(Math.max(...[10,20,30]));// 同上
4.new绑定:
使用new关键字来调用函数
回顾下这个问题,new关键字做了什么(面试常问)?过程如下
- 创建一个新对象
- 将构造函数的作用域赋值给新对象(因此this就指向了这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
例子:
function Person(){
this.a = 'lwx'
}
var lwx = new Person()
console.log(lwx.a); // lwx
Tips:通过构造函数这个工厂函数,生成一个新的对象,而构造函数的this就指向这个新对象。
注意:特别容易混淆的点,隐式绑定和独立调用,如下:
function foo() {
console.log(this);
return function() {
console.log(this);
};
}
var obj = {
foo: foo
};
obj.foo() // obj ,这是foo的隐式绑定
obj.foo()(); //obj window ,前一个'()'是函数foo()的隐式绑定执行,后一个'()'是里面return的匿名函数的独立调用执行
5.其他绑定规则:
setTimeout,setInterval、forEach、find等数组方法、箭头函数、DOM事件处理函数中的this等等规则
注意:为啥要let that = this或者通过强绑定来改变函数内部的this指向?之前总是一知半解,如下:
function fn() {
console.log(this); //指向的是obj
function test() {
console.log(this); // 指向window
}
test();
}
var obj = {
say: fn
};
obj.say();
说明:因为函数内部的this指向并不由函数本身决定,而是取决于调用这个函数的调用者,所以在子函数作用域并不绝对继承直系父级函数作用域的this指向,所以产生改变this的操作。
...
// test()
test.call(this);
...
说明:只需要改一行代码,你会发现两次打印的this结果,都是指向obj !!!
箭头函数
例子:
const foo = ()=>{} //箭头函数
var obj ={
name:3,
foo:()=>{
console.log(this);
}
}
obj.foo() // window this指向的是父级obj所在作用域的执行上下文对象,通俗的说,就是obj同级作用域的window对象
说明:箭头函数的this的指向:箭头函数本身没有this,这个this是父级所在的作用域的执行上下文对象。
默认绑定规则 对箭头函数无效
function foo() {
console.log("外层的this指向", this);
var test = () => {
console.log("里层的this指向", this);
};
return test;
}
var obj1 = {
name: 1,
foo: foo
};
obj1.foo(); // obj1 这是执行了foo这个函数,返回了一个test函数,返回了函数test,但没有执行
obj1.foo()(); // obj1 obj1 这里执行的是foo这个函数返回的test函数,返回了函数test,且执行了通过foo.test()
弱绑定规则 对箭头函数无效
var obj = {
name: 3,
foo: () => {
console.log('foo',this);
},
boo(){
console.log('boo',this);
}
};
obj.foo(); // 指向window而不是obj,可以这个规则对箭头函数无效
obj.boo(); // obj
强绑定规则 对箭头函数无效
function foo(){
console.log('外层的this指向',this);
var test = ()=>{
console.log('里层的this指向',this);
}
return test
}
var obj2 ={
name:2,
foo:foo
}
var bar = foo().call(obj2) // window window
new绑定规则 对箭头函数无效,且new不能构造箭头函数,只能构造普通函数
function Person(){
this.name = 'lwx'
console.log(this);
this.say = ()=>{
console.log(this);
}
this.eat = function(){
console.log(this);
}
}
let lwx = new Person() //Person {name: 'lwx'}
lwx.say() // Person {name: 'lwx', say: ƒ, eat: ƒ}
lwx.eat() // Person {name: 'lwx', say: ƒ, eat: ƒ}
Person() // window
var foo = ()=>{
console.log(this);
}
let lwx = new foo()
console.log(lwx); //报错:` Uncaught TypeError: foo is not a constructor`,new不能构造箭头函数,只能构造普通函数
Tips:切记,四个规则对箭头函数的完全无效!!!不能把规则乱用!!!
setTimeout、setInterval
setTimeout(() => {
console.log(this); //这里指向window,因为省去了前缀window,完整写法:window.setTimeout()
}, 500);
var obj = {
name: "setTimeout检测",
// 外层函数作用域
say() {
console.log('1',this); // obj
setTimeout(() => {
console.log("箭头函数", this); //指向obj,找到父函数setTimeout()所在的作用域上下文的this指向,即setTimeout()的同级作用域
});
},
eat() {
console.log('2',this); // obj
setTimeout(function() {
console.log("普通函数", this); //指向window,看谁调用了setTimeout函数, 即"window.setTimeout"所以是window
});
}
};
obj.say();
obj.eat();
var handler = {
init: function () {
console.log(this); // {init: ƒ, doSomething: ƒ}
document.addEventListener('click',
event => {
console.log(this); // {init: ƒ, doSomething: ƒ}
this.doSomething(event.type)
}, false);
},
doSomething: function (type) {
console.log('Handling ' + type + ' for ' + this.id);
}
};
handler.init()
Tips: 如果不是箭头函数,第2个this指向的肯定时document,使用了箭头函数后,this指向document所在作用域的上下文对象,即init的作用域内,而init()函数是handler调用的,所以init内部的this指向的是handler内部的this。这里存在一点歧义:第1个this是弱绑定规则,第2个this是箭头函数,因为箭头函数不遵循这几个规则,所以第2个this找的只是document.add...这部分代码所在作用域的同级上下文,和第1个this的绑定规则是两回事,从内向外找this的一个整体思路。
forEach、map等数组方法
仔细研究下MDN对forEach的解析:
语法::arr.forEach(callback(currentValue [, index [, array]])[, thisArg])
解释:
- callback 为数组中每个元素执行的函数,该函数接收一至三个参数:
- currentValue数组中正在处理的当前元素。
- index 可选数组中正在处理的当前元素的索引。
- array 可选forEach() 方法正在操作的数组。
- thisArg 可选可选参数。当执行回调函数 callback 时,用作 this 的值。
- 返回值 undefined。
let obj = { name: "lwx" };
let arr = [1, 2];
// 不指明第二个参数,则在window中这个this就是window,如果是Vue环境就是Vue实例
arr.forEach(function(item, index, arr) {
console.log(this); // window
});
// 普通函数指明了第二个参数,则回调函数的this指向第二个参数
arr.forEach(function(item, index, arr) {
console.log(this); // obj
},obj);
// 箭头函数指明了第二个参数,则回调函数的this指向第二个参数
arr.forEach((item, index, arr) => {
console.log(this); // window
}, obj);
说明:类似上面的其他数组迭代方法中,第二个参数thisArg就是第一个回调函数callback的this指向。
DOM事件处理函数
DOM0级: element.onclick=function(event){}
DOM2级: element.addEventListener('click',function(event){},false)冒泡
DOM3级: element.addEventListener('keyup',function(event){},false),新增的鼠标键盘事件
注意: dom1级没有涉及事件,不是没有dom1标准
...`html代码`
<div onclick="clickOne()">按钮one</div>
<div class="btn2">按钮Two</div>
<div onclick="click1(params)">按钮Three</div>
...
...`js代码`
// Dom0级事件
// function clickOne(){
// console.log(this,'按钮3的点击1'); // window
// }
const clickOne = ()=>{
console.log(this,'按钮3的点击2'); // window
}
// function click1(context){
// console.log(context,'按钮2的点击1');// btn的dom节点
// }
const click1 = (context) => {
console.log(context, "按钮2的点击2"); // btn的dom节点
};
// Dom2级事件
let btn2 = document.querySelector('.btn2')
btn2.addEventListener('click',function(){
console.log(this == btn2); //true, this指向 btn2的dom节点
})
btn2.addEventListener('click',()=>{
console.log(this == btn2); //false, this指向 window
})
...
Tips:Dom0级事件不能重复定义事件,因为JavaScript中没有重载,只有后面的方法覆盖前面的方法;而dom2级事件,相对松耦合,底层代码可以让其绑定多个事件,类似重载。其this的指向,是根据这个方法内部的封装方案决定的,所以需要看api文档如何规定this指向。
思考下,既然改变this指向有多种,那么这几种情况混用,会发生什么呢?换句话说,改变this的指向有没有优先级?答案是肯定的,存在优先级。
结论:优先级:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
例子:
// 强绑定 > 弱绑定
function fn() {
console.log(this.a);
}
var obj1 = {
a: 1
};
var obj2 = {
a: 2,
fn: fn
};
obj2.fn.call(obj1); // 1
// new > 强绑定
function Foo(b) {
this.a = b;
}
var obj3 = {};
var Bar = Foo.bind(obj3);
Bar(2);
console.log(obj3.a); // 2
var baz = new Bar(3); // Foo(不是Bar)中的this指向的是baz,之前是Foo(不是Bar)的this指向obj3,且new的优先级比bind高,而obj3和baz并没有直接关系,不影响obj3.a的值
console.log(obj3.a); // 2
console.log(baz.a); // 3
console.log(Bar.prototype); // undefined
console.log(baz.__proto__); // {constructor:f Foo(b)}
console.log(baz.__proto__ === Bar.prototype); // false , 这里Bar的this指向是Foo,而不是baz
console.log(baz.__proto__ === Foo.prototype); // true , 这里的Foo的this指向的是baz
这里有个疑问:构造函数作为一个复杂数据类型是按引用访问的,为甚么baz.proto === Bar.prototype是false? wht??? 我的理解是:this指向的是原构造函数Foo,而不是Bar的构造函数,Bar构造函数只是充当一个中转站。这个疑点,待求证!!!
function Person(){
this.sex = '女'
}
let lwx = new Person()
console.log(Person.prototype == lwx.__proto__); // true
Tips:new绑定的时候,构造函数的this指向的是这个新的实例对象。且构造函数的显式原型prototype指向的是实例对象的隐式原型__proto__。
5.Vue框架绑的this定规则:
...
data() {
return {
a: 1,
b: 2,
c: this.a,
d: this,
};
},
mounted() {
console.log(this.c, "c"); // undefined
console.log(this.d, "d"); // VueComponent实例对象
console.log(this.d.a, "a"); // 1
},
methods: {
getTime() {
console.log(this, "vue的实例对象"); //this指向 vue的实例对象
},
// 错误写法
// getTime :()=>{
// console.log(this, 'undefined'); //this指向 undefined
//}
},
computed: {
getVal({ a, b }) {
return a + b;
},
// 复杂写法
// getVal:{
// get({ a, b }){
// return a + b;
// }
// }
},
...
结论:
- data中的this不能重复引用VueComponent实例对象,mounted中的this使用则是正常的。
- methods中的箭头函数的写法,this指向undefined是因为vue默认开启了严格模式。
- computed中的参数可以进行解构,写起来更方便。
最后来看一道关于this的面试题:
例子:
var x = 20;
var a = {
x: 15,
fn: function() {
var x = 30;
return function() {
return this.x;
};
}
};
console.log(a.fn()); // function() {return this.x}
console.log(a.fn()()); // 20
console.log(a.fn()()); // 20
console.log(a.fn()() == a.fn()()); // true
console.log(a.fn().call(this)); // 20
console.log(a.fn().call(a)); // 15
这篇文章参考了阮一峰在Es6提及this的文章,有兴趣可以研究下。
读到最后,this的指向问题就算弄懂了,赶紧收藏种草吧!!!