本文不讨论let和const关键次
Context上下文
Context这个概念猛看起来比较晦涩,其实是很好理解的。你可以将其理解为代码“社交”的场所和范围。一个函数执行时可能要使用到一些变量,或者调用其他函数,这就是“社交”,既然是“社交”就跟生活中的社交一样,有场所,有范围。代码是机器语言,有严格的规范和标准,如果没有允许,是不能随意互相调用任何数据的,最明显的限制就是局部作用域和全局作用域。
那这个代码“社交”的场所是以什么方式体现的?就是对象。JS中代码的执行都依托于对象,每个对象都是其内部声明的数据的上下文环境。
当一个HTML文件运行在浏览器中的时候,浏览器会为这个运行的HTML添加一个全局的window对象(如果还有frame或者iframe标签,会额外再创建对应的window对象)。这个全局的window对象,就是当前这个HTML文件的最大的运行环境。这个运行环境为当前页面的数据沟通提供大背景。
JS的预解析
我们一直都强调JS没有编译这个阶段,它是直接在浏览器上执行的。但是我们在写代码时,会发现一些问题,比如:
<script>
console.log(a); // undefined
var a = 0;
</script>
输出变量a,结果是undefined,但问题是,如果JS没有编译这个阶段,按照我们的理解,除了流程控制及异步操作之外,js代码是逐行执行的,a的输出在前,声明在后,这个代码应该报错 a is not defined,怎么会出现一个undefined呢。
虽然JS代码没有编译,但是有预解析。
当一个HTML文件被运行在浏览器中时,这个HTML文件中引用的所有JS代码,会走两个流程:
(1)浏览器的JS引擎对于JS代码的预解析(创建auguments,创建作用域链,确定context上下文);
(2)逐行执行预解析之后的JS代码,这个逐行执行才是我们所认为的逐行执行。
在JS执行流程的第一步,预解析过程确定了上下文context,其实是将你当前HTML文件中引入的所有的JS文件中的数据,都添加到当前HTMl文件运行时的window对象中去了,那么window对象相当于所有数据的父级对象,即顶层对象。
现在这个window对象就是当前HTML文件中所有JS代码的顶层环境,所谓的上下文,可以简单理解为,同在一个父级对象中的其他键值对们。根据JS的语言规律,大家都在一个父级对象中了,就可以互相访问了。
所以上面的代码其实是,在预解析时,由于var关键词的声明作用,在代码预解析时,将a变量绑定给了window对象,作为其属性。预解析结束,代码开始执行,第一行输出a,因为已经在预解析阶段将a绑定给了window,所以可以访问到a变量,但是绑定时仅仅绑定了变量,并未赋值,所以输出undefined。
变量和函数声明的真正意义
再举个例子:
<script>
var a = 1;
function fn() {
console.log(a); // 1
}
fn();
</script>
fn函数中可以直接访问a,因为预解析时a和fn都被绑定给了window对象,他们在同一个上下文环境中,可以直接互相访问。访问形式是this.a,this是window,所以this被省略了。
其他数据和window一样。JS中所有的数据都是以对象的形式呈现的(一个DOM节点,一个函数,一个数组)。
像下面的代码:
<script>
const obj = {
name: 'tom',
fn () {
console.log(this.name);
}
};
</script>
name和fn都在对象obj中,想要在fn中访问name,需要借助于对象this。fn在被调用时,是obj.fn(); 这个时候fn运行的环境就是obj这个对象,name就是fn的上下文。所以this指obj。
或者再往简单了说,谁调函数,谁就是函数运行的环境(函数在哪儿调,哪儿就是函数运行的环境),当然特殊的new关键词除外。
普通函数中的this
如果代码是这样的:
<script>
const _body = document.getElementByTagName('body')[0];
function fn () {
console.log(this);
}
fn(); // window对象
_body.onclick = fn; // body
</script>
同样的fn函数,以不同的形式调用,this的输出值为什么不一样。因为JS在调用变量名时,拿到的是变量的值。但是在调函数名时,拿到的是函数代码在内存中的地址。这样,一个函数在不同的环境下调用的结果可能完全不一样。为了响应函数的这种特点,JS规定,函数中的this,在函数被调用时确定,它指函数当前运行的环境,我们可以使用this获取当前函数本次调用的上下文数据。
那么上面的代码就好解释了,当fn直接调用时,是window.fn(); fn这个时候所在的上下文环境指window,所以this是window。
若将fn的地址传递给了_body这个DOM节点对象的onclick键,那么点击_body时,会根据_body对象的onclick键存储的地址,找到fn代码并执行,onclick键的父级对象是_body,所以fn函数本次的执行环境就是_body对象,所以this指_body对象。
构造函数中的this
首先要明确,JS是没有类这个概念,但是又要使用面向对象的编程思想,所有就有了构造函数的说法(ES6中有class类)。构造函数首先是个普通的函数。可以将它视作任何一个你平时声明的函数。
特殊的不是“构造函数”这个称呼,或者首字母大写的函数名,特殊的是new关键词。
<script>
function oop() {
this.name = 'tom';
}
const o = new oop(); // {name: 'tom'}
</script>
new关键词的作用是调用某个函数并拿到其中的返回值,只是调用过程稍特殊。在上面的代码实例中。oop函数被new关键词调用时,内部依次执行了以下步骤:
(1)创建一个空对象。
(2)将这个空对象的原型,指向这个构造函数的prototype。
(3)将空对象的值赋给函数内部的this(this就是个空对象了)。
(4)执行函数体代码,为this这个对象绑定键值对。
(5)返回this,将其作为new关键词调用oop函数的返回值。
所以构造函数中的this,依旧是在构造函数被new关键词调用时确定其指向,指向的是当前被实例化的那个对象。
注:如果在封装构造函数时,你通过代码为这个函数自动设置了返回值且这个值的类型是一个对象,那么new关键词再调这个构造函数时,返回值就不再是this,而是你return的这个值。当然如果你没有设置return值,或者你设置的值不是个对象,依旧返回this。