javaScript深入解析1-this关键字

前言

花了三天时间粗略看完了《你不知道的JavaScript》下部,这一部分介绍了ES6的部分内容(本书出版时ES7,8)的部分特性还未推出,因为之前已经学过ES6,所以这一遍只是温习和关注细节,感觉除了新增的特征,迭代器应该是最重要的部分,因为ES6的很多特性都用到了迭代器,包括 扩展运算符(spread)‘[…]’,泛数组的迭代(for..of循环,keys(),values(),entries()等等),其次就是Promise,这个特性用的也很多,特别是在异步操作上,配合生成器(function */),迭代器(iterator),就是async异步函数的底层实现。

关于this

this关键字作为JavaScript开发者必须理解的概念之一,其实并没有那么高深。

what

Q:什么是this?
A:this代表的是函数运行的上下文环境。
Detail:

在所有语言中,都有一个调用栈的概念,即函数被哪个对象所调用(注意JavaScript中对象的概念,几乎所有的方法,参数都有一个宿主对象,其中顶层对象在浏览器中window,在node中则为global)。所以我们再执行一个函数或调用一个变量时,默认是带有一个顶层对象前缀的,不过由于我们所有的代码都在该对象中,则可以省略,不信可以试一下:

1
2
3
var num = 0;
console.log(num);
console.log(window.num);

没错,它们的结果都是0。

回归正题,所以粗略的来说,函数被调用的对象即为他的this指向(先不谈硬绑定),最简单的例子:

1
2
3
4
5
6
7
8
var obj = {
num : 0,
func: function(){
console.log(this.num);
}
}

obj.func();

执行结果是什么呢?
没错,是0;
因为这里的func函数由obj调用,所以this指向obj。
OK,那么再看一下下面这段代码:

1
2
3
4
5
6
7
8
9
10
var num = 0;
var obj = {
num : 1,
func: function(){
console.log(this.num);
}
}

var outterFunc = obj.func;
outterFunc();

执行结果又是什么呢?have a try!
可能出乎你的意料,结果是0.
So,why?思考一下我们上面的解释。

没错,也许你想通了,因为这时候的func函数并不是通过obj来调用的,所以this默认指向window,但window中num变量为0,所以结果是0.

在这之中,我们需要了解的是:在JavaScript中,函数,对象,数组,或者说所有对象即对象的子类(因为包括函数,数组皆为对象的子类)都是通过地址的形式存储,类似与C语言中的指针形式存储

思考下面代码:

1
var func = function(){console.log(123)}

在JavaScript引擎中时如何运行完这条语句的呢?
(你需要了解的是JavaScript不是一门预编译语言,而是一门解释执行的语言(即执行一句,编译一句,当然这是不完全正确的,从变量提升即可以看出来))

  1. 查询是否存在func这个变量—否
  2. 声明这个变量
  3. 定义函数function(){console.log(123)}
  4. 将该函数存储于内存中,并取得其地址
  5. 将该地址赋值给func变量

所以func变量实际存储的是该函数的地址。
所以函数实际上是没有存储作用域链中的任何信息,它总是一个独立存在的个体。
这也就解释了为什么func虽然定义在obj中,但是通过某种方式提取出来后直接调用其this就指向了window。

所以你大概已经明白了了吧,思考一下下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
var num = 0;
function inner()
{
console.log(this.num)
}

function outter()
{
var num = 1;
inner();
}

outter();

所以,结果是什么呢?

我猜你肯定答对了,是0;

同样的道理,inner函数虽然在outter函数中被调用,但任然是直接调用,没有任何前缀对象,所以其this指向任然是window。

最后一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
var name = "clever coder";  
var person = {
name : "foocoder",
hello : function(sth){
var that = this;
var sayhello = function(sth) {
console.log(that.name + " says " + sth);
};
sayhello(sth);
}
}
person.hello("hello world");//foocoder says hello world

这里内层函数this指向的任然是window,有人认为是JavaScript的设计错误,但是从上面内存的角度去分析,会发现这是正确的。

why & where

说了这么多this,那么为什么要用this呢?又在哪里用呢?

Q:why?

A:this的使用使调用上下文对象变得更加简洁,否则,每次调用函数必须传递上下文对象,编码将及其复杂。

Q:where?

A:相信接触过OOP(面向对象编程)的同学应该熟悉这个结构(伪代码):

1
2
3
4
5
6
7
8
9
Class Example{
constructor(name,age)
{
this.name = name;
this.age = age;
}
}

declare instan = new Example('tom',23);

没错,这是一个基本类的构造,只包括了一个constructor构造器方法,其中的this指向的即是这个被实例化的对象(instan),则instan的name属性为’tom’,age属性为23。试想没有this的话,构造器该如何为实例属性赋值呢?只有显式的将instan传递给constructor函数,这将变得无比繁杂。

在js中,不仅是在类,即使是用行为委托方式编码,任然离不开this,因为总是存在对上下文对象的应用。

值得注意的是:JavaScript语言基础中并没有class的概念,即使ES6推出了class关键字,但它任然是使用原型链对类的模拟,任然是ES5部分框架class实现的语法糖

how

说了那么多,还是要详细讲一下this的绑定问题:

  1. 默认绑定

独立函数调用执行默认绑定

1
2
3
4
5
6
7
8
9
var a = 0;
function func()
{
console.log('a:'this.a);
}

func();

//a:0

如上面我们所说,这里的func是直接的函数调用,所以执行默认绑定,this指向了window对象。

值得注意的是:在strict模式下,默认绑定this为undefined

1
2
3
4
5
6
7
8
9
10
var a = 0;
function func()
{
"use strict"
console.log('a:'+this.a);
}

func();

//Uncaught TypeError: Cannot read property 'a' of undefined
  1. 隐式绑定

这就是我们之前熟悉的用对象来调用函数:

1
2
3
4
5
6
7
8
9
10
var obj = {
num : 0,
func: function(){
console.log('num:'+this.num);
}
}

obj.func();

//num:0

当含有多层对象引用的时候,只有距函数最近的一个对象为上下文对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var obj0 = {
num : 0,
func: function(){
console.log('num:'+this.num);
}
}

var obj1 = {
num:1,
obj0:obj0
}

obj1.obj0.func()

//num:0

正如我们之前所说的,函数的存储与上下文对象毫无关系,所以,当我们将对象中的函数通过某种方法提取出来时,它就与原来的对象毫无关系了,其this指向则为window了(这种现象一般被称为隐式丢失)。具体可以看上面那个例子。

  1. 显示绑定

所谓显示绑定,即通过call(),apply(),以及ES6的bind()函数直接指定this的指向。

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 0;
var obj = {
a: 1
}

function func()
{
console.log(this.a);
}

func.call(obj);

//1

值得注意的是:call(),apply()函数的绑定是软绑定,即只在绑定这一次起作用,下一次调用时this任然执行原有绑定规则。

所以就衍生出了硬绑定,ES6之前需要手动封装硬绑定方法:

1
2
3
4
5
6
7
8
function bind(fn,obj)
{
return function(){
return fn.apply(obj,arguments)
}
}

//执行此方法后,函数的this指向将被永久绑定在指定对象上,无法修改。

由于这个方法需求太广泛了,所以ES6推出了官方的bind()方法,直接调用即可。

  1. new绑定

与其他语言中构造函数的特殊性不同,在JavaScript中,构造函数是一个普通的函数,唯一的特殊点是它在执行new操作符后自动调用,并且开始执行一系列操作:

  1. 创建一个新的对象
  2. 这个新对象会被执行[[prototype]]连接(即将__proto__指定为函数的prototype)。
  3. 这个对象将会被绑定到对应的函数的this。
  4. 如果函数没有返回其他对象,那么new表达式中函数调用会自动返回这个新对象。

第四步解释:

1
2
3
4
5
6
7
8
9
10
function Fun()
{
return {
b:2
}
}

var instan = new Fun()

//{b:2}

优先级

  1. 如果是new绑定,则this按上面的规则绑定对象。
  2. 如果是显示绑定,则this指向显示绑定的对象。
  3. 如果有隐式绑定,则this绑定在调用对象上。
  4. 否则执行默认绑定,非严格模式下为window,严格模式下位undefined。

箭头函数 =>

在ES6中,新加了一种声明函数的方式,箭头函数(=>)

1
()=>{} 等价于 function(){}

关于箭头函数的特性就不具体细讲,他与this相关的就是:
箭头函数的this决定于定义函数时的外层作用域来决定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//arrayFunc
var a = 0;
//定义全局变量a
function fun() {
return (arrayFunc = ()=>{
console.log(this.a);
})()//这是一个立即执行函数,也可以在外围多调用一次
}

var obj = {
a : 1,
func: fun()
}

fun.call(obj);//将fun的this指向obj
//1

由于fun的this指向obj,而箭头函数的this根据外围函数的this决定,所以arrayFunc的this也指向obj,则a为1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//normal

var a = 0;
function fun() {
return (function normalFunc(){
console.log(this.a);
})()
}

var obj = {
a : 1,
func: fun()
}
fun.call(obj);
//0

这里普通函数的this根据调用的对象来确定,由于它是单独调用的,所以this指向window,则a为0。

Last

this的用法相当重要,不管是自己原生开发,或是用框架,特别是使用框架时,由于一般框架会有一个App实例,我们的操作都在这个实例之中进行,所以会无数次用到this,所以我们必须学通。下一期写一下Protype原型链,也是JavaScript中相当重要的一个内容。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2024 My Wonderland All Rights Reserved.

UV : | PV :