javascript-类型、值和变量

筅森魡賤 提交于 2019-11-28 12:23:22

基本类型和引用类型

MDN-JavaScript 数据类型和数据结构

ECMAScript 变量可能包含两种不同数据类型的值:基本类型值和引用类型值。基本类型值指的是 简单的数据段,而引用类型值指那些可能由多个值构成的对象。 其中基本类型是按值访问的,可以操作保存在变量中的实际的值:undefinednullnumberbooleanstringsymbol(es6新增)、BigInt(提案阶段); 引用类型是保存在内存中的对象,引用类型的值是按引用访问的:FunctionObject(ArrayDateRegExp)

!> 除 Object 以外的所有类型都是不可变的(值本身无法被改变),我们称这些类型的值为“原始值”。

graph LR reference_type --> Object subgraph 栈区 reference_type[Object类型引用地址] Undefined Null Number Boolean String Symbol BigInt end subgraph 堆区 Object[引用类型值] end

类型检测

typeof

类型 举例
Null typeof(null) => object
Number typeof(1) => number
Boolean typeof(true) => boolean
String typeof('test') => string
Symbol typeof(Symbol()) => symbol
BigInt typeof(BigInt("9007199254740995")) => bigint
Object typeof({}) => object
Function typeof (()=>{}) => function
Array typeof([]) => object

!> 使用typeof操作符检测函数时,该操作符会返回"function"。在Safari 5及 之前版本和 Chrome 7 及之前版本中使用 typeof 检测正则表达式时,由于规范的原 因,这个操作符也返回"function"。ECMA-262 规定任何在内部实现[[Call]]方法 的对象都应该在应用 typeof 操作符时返回"function"。由于上述浏览器中的正则 表达式也实现了这个方法,因此对正则表达式应用 typeof 会返回"function"。在 IE 和 Firefox 中,对正则表达式应用 typeof 会返回"object"。

检测基本数据类型时 typeof表现没有问题,但在检测引用类型的值时无法检测它是什么类型的对象。为此,ECMAScript 提供了 instanceof 操作符

instanceof

instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。详见MDN:instanceof

语法

object instanceof constructor

根据规定,所有引用类型的值都是 Object 的实例。因此,在检测一个引用类型值和 Object 构造 函数时,instanceof 操作符始终会返回 true。当然,如果使用 instanceof 操作符检测基本类型的 值,则该操作符始终会返回 false,因为基本类型不是对象。

(new Object) instanceof Object // true
true instanceof Object // true
[] instanceof Array // true
null instanceof Object // false

instanceof是根据它的原型链来识别类型的,若原型链被修改(当然一般也不会这么做)则:

let a = [];
a.__proto__ = Object;
a instanceof Array // false

Object.prototype.toString

Object.prototype.toString表示该对象的字符串。每个对象都有一个 `toString()`` 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中 type 是对象的类型。 还是上边的例子

let a = [];
a.__proto__ = Object;
Object.prototype.toString.call(a) // "[object Array]"

Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(null) // "[object Null]"
(function (){console.log(Object.prototype.toString.call(arguments))})() // "[object Arguments]"

封装一个获取传入变量类型的函数:

function getType (t) {
    return Object.prototype.toString.call(t).match(/^\[object\s{1}(.*)]$/)[1].toLowerCase();
}
类型 举例
Null getType(null) => null
Number getType(1) => number
Boolean getType(true) => boolean
String getType('test') => string
Symbol getType(Symbol()) => symbol
BigInt getType(BigInt("9007199254740995")) => bigint
Object getType({}) => object
Function getType(()=>{}) => function
Array getType([]) => array
Arguments (function() {console.log(getType(arguments))})() => arguments
Math getType(Math) => Math
Date getType(new Date) => date
NodeList getType(document.querySelectorAll('div')) => nodelist

因此我们可以封装一个函数,判断当前的变量是否是我们想要的类型

function isType (t, type) {
    return Object.prototype.toString.call(t) === `[object ${type}]`;
}
isType([], 'Array'); // true

不可变的原始值和可变的对象引用

JavaScript的值可分为:

  • 原始值(undefinednullbooleannumberstring,symbolbigint):存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。(js中的valueOf())。原始值是不可更改的:任何方法都无法更改一个原始值。原始值的比较是值的比较:只有他们的值相等时它们才相等。
  • 引用值(数组、对象等):存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存处。引用值可以变更:通过修改其属性;引用值的比较均是引用的比较:当且仅当它们引用同一个基对象时才相等。

变量声明

var a;
var b;

var a, b;

var a = 1, b = 2;
var c = 3;

es6中的letconst 用法类似

  • var声明变量可重复用var声明, 但letconst 不可重复定义
  • 声明一个全局变量,实际上是定义了全局对象的一个属性,不可以通过delete删除
  • es5严格模式下,给一个没有声明的变量赋值会报错,在非严格模式下,给一个未声明的变量赋值,JavaScript 实际上会给全局对象创建一个同名属性,并且像一个正确声明全局变量工作(但并不完全一样,这个变量是全局对象的可配值属性,可以通过delete删除)。
aa = 1;
var bb = 2;
delete aa; // => true
delete bb; // => false

执行环境及作用域

执行环境(execution context)定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个 与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。虽然我们 编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

  • 全局变量:在全局定义的变量,执行环境是全局
  • 局部变量:在函数体內定义的变量,执行环境是局部性的

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是 保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。作用域链中 的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延 续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

函数作用域的创建步骤:

  1. 函数形参的声明。
  2. 函数变量的声明,函数变量会覆盖以前声明过的同名声明。
  3. 普通变量的声明。
  4. 函数内部的this指针赋值
  5. 函数内部代码开始执行!

javascript中作用域有:全局作用域、函数作用域、块级作用域(ES6新增)

!> 1. 作用域是一个函数在执行时期的执行环境,每一个函数在执行的时候都有着其特有的执行环境。 2. 变量的作用域遵循就近原则,即局部变量优先级高于全局变量;局部变量的会优先选取最近的函数作用域;若在整个作用域链都未找到变量,则抛出引用错误(ReferenceError)异常。

声明提前(变量提升)

JavaScript的函数作用域是指在函数內声明的所有变量(使用var声明的变量)在函数体內始终是可见的。这意味着变量在声明之前甚至已经可用。 这个特性被称为声明提前(hoisting),即JavaScript函数里声明的所有变量(但不涉及赋值)都被"提前"至函数体的顶部 !> 声明提前,这步操作是在JavaScript引擎"预编译"时进行的,是在代码开始运行之前

作用域链(scope chain)

因为作用域是一个函数在执行时期的执行环境,每一个函数在执行的时候都有着其特有的执行环境。而在JS中,函数的可以允许嵌套的。即,在一个函数的内部声明另一个函数。 这种函数作用域的嵌套就组成了所谓的函数作用域链。当在自身作用域内找不到该变量的时候,会沿着作用域链逐步向上查找,若在全局作用域内部仍找不到该变量,则会抛出异常。

延长作用域链

有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。在两种情况下会发生这种现象:

  1. try-catch 语句的 catch 块;
  2. with 语句。 这两个语句都会在作用域链的前端添加一个变量对象。对with语句来说,会将指定的对象添加到作用域链中。对 catch 语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。 下面看一个例子。
function buildUrl() {
    var qs = "?debug=true";
    with(location){ // 接收的是 location 对象,因此其变量对象中就包含了 location 对象的所有属 性和方法,而这个变量对象被添加到了作用域链的前端。
        var url = href + qs; // 实际引用的是 location.href
    }
    return url;
}

!> IE8即在 catch 语句中捕获的错误对象会被添加到执行环境的变量对象,而不是 catch 语句,即使是在 catch 块的外部也可以访问到错误对象。IE9修复了这个问题。

作用域链与闭包

Javascript采用词法作用域,也就是说,函数的执行以来于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。Javascript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域內,这种特性在计算机科学文献中称为"闭包"。 从技术角度讲所有的Javascript函数都是闭包:它们都是对象,它们都关联到作用域。简单的理解闭包:函数定义时的作用域链到函数执行时依然有效。

垃圾回收

Javascript具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。其原理:找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间) ,周期性的执行这一操作。通常使用的策略有:标记清除、引用计数。

标记清除

垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后被加上标记的变量将视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。 到 2008 年为止,IE、Firefox、Opera、Chrome 和 Safari 的 JavaScript 实现使用的都是标记清除式的 垃圾收集策略(或类似的策略),只不过垃圾收集的时间间隔互有不同。

引用计数

引用计数的含义是跟踪记录每 个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。 如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取 得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这 个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那 些引用次数为零的值所占用的内存。 引用计数策略有一个严重的问题:循环引用。循环引用指的是对象 A 中包含一个指向对象 B 的指针,而对象 B 中也包含一个指向对象 A 的 引用。例:

function problem(){
    var objectA = new Object();
    var objectB = new Object();
    objectA.someOtherObject = objectB;
    objectB.anotherObject = objectA;
}

objectA 和 objectB 通过各自的属性相互引用;也就是说,这两个对象的引用次 数都是 2。在采用标记清除策略的实现中,由于函数执行之后,这两个对象都离开了作用域,因此这种 相互引用不是个问题。但在采用引用计数策略的实现中,当函数执行完毕后,objectA 和 objectB 还 将继续存在,因为它们的引用次数永远不会是 0。假如这个函数被重复多次调用,就会导致大量内存得 不到回收。

内存优化

确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行 3 中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为null来释放其引用——这个 做法叫做解除引用(dereferencing)。这一做法适用于大多数全局变量和全局对象的属性。局部变量只在函数执行的过程中存在。而在 这个过程中,会为局部变量在栈(或堆)内存上分配相应的空间,以便存储它们的值。然后在函数中使用这些变量,直至函数执行结束。此时,局部变量就没有存在的必要了,因此可以释放它们的内存以供将来使用。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!