前言
花了三天时间粗略看完了《你不知道的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 | var num = 0; |
没错,它们的结果都是0。
回归正题,所以粗略的来说,函数被调用的对象即为他的this指向(先不谈硬绑定),最简单的例子:
1 | var obj = { |
执行结果是什么呢?
没错,是0;
因为这里的func函数由obj调用,所以this指向obj。
OK,那么再看一下下面这段代码:
1 | var num = 0; |
执行结果又是什么呢?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不是一门预编译语言,而是一门解释执行的语言(即执行一句,编译一句,当然这是不完全正确的,从变量提升即可以看出来))
- 查询是否存在func这个变量—否
- 声明这个变量
- 定义函数
function(){console.log(123)}
- 将该函数存储于内存中,并取得其地址
- 将该地址赋值给func变量
所以func变量实际存储的是该函数的地址。
所以函数实际上是没有存储作用域链中的任何信息,它总是一个独立存在的个体。
这也就解释了为什么func虽然定义在obj中,但是通过某种方式提取出来后直接调用其this就指向了window。
所以你大概已经明白了了吧,思考一下下面的代码:
1 | var num = 0; |
所以,结果是什么呢?
我猜你肯定答对了,是0;
同样的道理,inner函数虽然在outter函数中被调用,但任然是直接调用,没有任何前缀对象,所以其this指向任然是window。
最后一个例子:
1 | var name = "clever coder"; |
这里内层函数this指向的任然是window,有人认为是JavaScript的设计错误,但是从上面内存的角度去分析,会发现这是正确的。
why & where
说了这么多this,那么为什么要用this呢?又在哪里用呢?
Q:why?
A:this的使用使调用上下文对象变得更加简洁,否则,每次调用函数必须传递上下文对象,编码将及其复杂。
Q:where?
A:相信接触过OOP(面向对象编程)的同学应该熟悉这个结构(伪代码):
1 | Class Example{ |
没错,这是一个基本类的构造,只包括了一个constructor构造器方法,其中的this指向的即是这个被实例化的对象(instan),则instan的name属性为’tom’,age属性为23。试想没有this的话,构造器该如何为实例属性赋值呢?只有显式的将instan传递给constructor函数,这将变得无比繁杂。
在js中,不仅是在类,即使是用行为委托方式编码,任然离不开this,因为总是存在对上下文对象的应用。
值得注意的是:JavaScript语言基础中并没有class的概念,即使ES6推出了class关键字,但它任然是使用原型链对类的模拟,任然是ES5部分框架class实现的语法糖
how
说了那么多,还是要详细讲一下this的绑定问题:
- 默认绑定
独立函数调用执行默认绑定。
1 | var a = 0; |
如上面我们所说,这里的func是直接的函数调用,所以执行默认绑定,this指向了window对象。
值得注意的是:在strict模式下,默认绑定this为undefined
1 | var a = 0; |
- 隐式绑定
这就是我们之前熟悉的用对象来调用函数:
1 | var obj = { |
当含有多层对象引用的时候,只有距函数最近的一个对象为上下文对象
1 | var obj0 = { |
正如我们之前所说的,函数的存储与上下文对象毫无关系,所以,当我们将对象中的函数通过某种方法提取出来时,它就与原来的对象毫无关系了,其this指向则为window了(这种现象一般被称为隐式丢失)。具体可以看上面那个例子。
- 显示绑定
所谓显示绑定,即通过call(),apply(),以及ES6的bind()函数直接指定this的指向。
1 | var a = 0; |
值得注意的是:call(),apply()函数的绑定是软绑定,即只在绑定这一次起作用,下一次调用时this任然执行原有绑定规则。
所以就衍生出了硬绑定,ES6之前需要手动封装硬绑定方法:
1 | function bind(fn,obj) |
由于这个方法需求太广泛了,所以ES6推出了官方的bind()方法,直接调用即可。
- new绑定
与其他语言中构造函数的特殊性不同,在JavaScript中,构造函数是一个普通的函数,唯一的特殊点是它在执行new操作符后自动调用,并且开始执行一系列操作:
- 创建一个新的对象
- 这个新对象会被执行[[prototype]]连接(即将
__proto__
指定为函数的prototype
)。 - 这个对象将会被绑定到对应的函数的this。
- 如果函数没有返回其他对象,那么new表达式中函数调用会自动返回这个新对象。
第四步解释:
1 | function Fun() |
优先级
- 如果是new绑定,则this按上面的规则绑定对象。
- 如果是显示绑定,则this指向显示绑定的对象。
- 如果有隐式绑定,则this绑定在调用对象上。
- 否则执行默认绑定,非严格模式下为window,严格模式下位undefined。
箭头函数 =>
在ES6中,新加了一种声明函数的方式,箭头函数(=>)
1 | ()=>{} 等价于 function(){} |
关于箭头函数的特性就不具体细讲,他与this相关的就是:
箭头函数的this决定于定义函数时的外层作用域来决定:
1 | //arrayFunc |
由于fun的this指向obj,而箭头函数的this根据外围函数的this决定,所以arrayFunc的this也指向obj,则a为1。
1 | //normal |
这里普通函数的this根据调用的对象来确定,由于它是单独调用的,所以this指向window,则a为0。
Last
this的用法相当重要,不管是自己原生开发,或是用框架,特别是使用框架时,由于一般框架会有一个App实例,我们的操作都在这个实例之中进行,所以会无数次用到this,所以我们必须学通。下一期写一下Protype原型链,也是JavaScript中相当重要的一个内容。