JS 基础进阶(一)

本节通过了解JS中变量在计算机中的内存中存储方式来解释JS中的一些难题,按值传递和引用传递…

栈与堆

JS中没有严格区分栈内存与堆内存。

栈的存取方式就像是往乒乓球盒子里面存放乒乓球。先进后出,后进先出。

堆内存的存取方式就像是从书架上拿书。我们只需要知道某本书的名字就可以从书架上准确地拿到该书。

JS中的基础数据类型与引用数据类型的存储

变量对象(变量对象其实也是存在堆内存中的,但它比较特殊,所以把它单独提出来)

  • 基础数据类型是统一存储在一个变量对象上的。var a = 1;相当于:变量对象.a = 1;
  • 复制变量对象中的变量时会为新变量分配一个新值,这个值与之前的值没有关系

引用数据类型

  • 引用数据都是存在堆内存中的
  • 一个引用数据的内存由三个部分构成:变量对象中存储变量名与内存地址(内存指针)、堆内存中存储键值对、内存地址指向堆内存中的一个引用类型数据
  • 复制一个引用类型的数据其实是给了新变量一个新的内存地址(这个过程发生在变量对象中)。这个新的内存地址与原来的内存地址指向的是同一个引用类型数据

内存空间管理

JS的内存生命周期

1
2
3
var a = 20;  // 在内存中给数值变量分配空间
alert(a + 100); // 使用内存
a = null; // 使用完毕之后,释放内存空间
给a赋值null的目的是让a原本对应的值失去引用,脱离执行环境,这个值会在下一次垃圾收集器执行操作时被找到并释放。
在局部作用域中,当函数执行完毕,局部变量也就没有存在的必要了。因此垃圾收集器很容易做出判断并回收。但是全局变量什么时候需要自动释放内存空间则很难判断,因此在我们的开发中,需要尽量避免使用全局变量,以确保性能问题。

执行上下文

what

执行上下文可以理解为当前代码的执行环境,它会形成一个作用域。

types

  • 全局环境:JS代码执行首先进入全局环境。这个环境最先入栈,最后出栈。
  • 函数环境:当函数被调用执行时,会进入当前函数中执行代码(不是定义时,是被调用时)。
  • eval环境:现在基本不用,所以不用考虑。

characteristic

  • 单线程
  • 同步执行,只有栈顶的上下文处于执行中,其他上下文需要等待。
  • 全局的上下文只有一个,它在浏览器窗口关闭时出栈。
  • 函数的执行上下文没有个数限制。
  • 每次某个函数被调用,就会用一个新的执行上下文为其创建。即使是调用的它自身。

执行上下文与闭包

它的执行过程是:全局环境入栈-->外层函数入栈-->外层函数出栈-->内层函数入栈-->内层函数出栈-->全局环境出栈
内层函数会记住外层函数的变量对象,从而实现闭包的效果。

变量对象

当一个函数被调用时,就会创建一个新的执行上下文。
一个的执行上下文的生命周期分为创建阶段和执行阶段。
创建阶段:生成变量对象-->建立作用域链-->确定this的指向;
执行阶段:变量赋值-->函数引用-->执行其他代码

变量对象(Variable Object)的创建过程

  1. 建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。
  2. 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
  3. 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。

注:未进入执行阶段之前,变量对象中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。变量对象和活动对象其实是同一个对象,只是处于执行上下文的不同生命周期。

作用域链

按照我的理解,作用域链是由多个变量对象所组成的一个数组。
数组的第一个元素是当前执行上下文的变量对象,最后一个元素是全局环境的变量对象(window对象下的变量)。
作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。

JS中的this

this的指向,是在函数被调用的时候确定的。也就是执行上下文被创建时确定的。

函数中的this

在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。

使用call,apply显示指定this

JavaScript内部提供了一种机制,让我们可以自行手动设置this的指向。它们就是call与apply。所有的函数都具有着两个方法。它们除了参数略有不同,其功能完全一样。它们的第一个参数都为this将要指向的对象。
下面举一个利用call,apply实现继承的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义父级的构造函数
var Person = function(name, age) {
this.name = name;
this.age = age;
this.gender = ['man', 'woman'];
}
// 定义子类的构造函数
var Student = function(name, age, high) {
// use call
Person.call(this, name, age);
this.high = high;
}
// Student的构造函数等同于下
var Student = function(name, age, high) {
this.name = name;
this.age = age;
this.gender = ['man', 'woman'];
// Person.call(this, name, age); 这一句话,相当于上面三句话,因此实现了继承
this.high = high;
}

注:匿名函数经常会导致this丢失,可以用bind方法来解决。

构造函数与原型方法上的this

new一个对象的过程:

  1. 创建一个新的对象。
  2. 将构造函数的this指向这个新对象。
  3. 指向构造函数的代码,为这个对象添加属性,方法等。返回新对象。

    原型方法上的this

    原型方法上的this就好理解多了。谁调用的,this就指向谁。
叶思玄 wechat
如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!