JavaScript基础(二)

JS数据类型

  ECMAScript 5(ES 5)规定了5种简单(基本)数据类型:undefined、null、boolean、number、string以及1种复杂数据类型:object。ECMAScript 6(ES 6)增加了1个基本数据类型:symbol。使用typeof可以检测变量值的类型(值得注意的是JS中的变量没有类型),typeof可以返回的结果有7种:“undefined”、”object”、”boolean”、”number”、”string”、”function”、”symbol”
  上述7种typeof返回的结果中,只有”function”找不到与之对应的基本数据类型。function其实是object的一个子类型。具体来说,function是可以被调用的对象,它有一个内部属性”call”,使其可以被调用。另外,JS中的array类型也是object的子类型,只不过array是按照数值顺序来进行元素的索引,普通object是通过字符串键值来索引。在ES 5中,typeof对没有定义的变量(typeof不会报出”Uncaught ReferenceError”错误)或者已被定义但是没有初始化的变量返回的结果均是”undefined”,因此可以用来检测变量是否已被定义或者初始化。但是ES 6引入了let命令,它会形成temporal dead zone(暂时性死区)typeof在let命令声明变量之前使用变量会报出”Uncaught ReferenceError”错误

JS变量

  按照第1节的介绍,JS变量存储的值可以分为两种类型:基本类型和引用类型(复杂类型)。基本类型对应于6种基本数据类型(undefined、null、boolean、number、string、symbol)。引用类型对应于JS中的对象(object)。基本类型和引用类型各自的特点如下:

  1. 基本类型的值占用固定大小的内存空间,其被分配在栈空间中;引用类型的值是对象,其占用的存储空间大小可以动态改变,其被分配在堆空间中
  2. 引用类型是一种数据结构,它将数据和功能组织在一起。存储引用类型的变量实际上存储的是指向对象的指针。
  3. 复制基本类型的值,会创建这个值的副本;复制引用类型的值,会创建指向对象的指针的副本;JS中所有函数的参数传递都是按值传递的。

JS作用域&作用域链

  JS变量是有其对应的作用域(或者执行环境)的。作用域决定了变量的生命周期以及哪一部分代码可以访问相应的变量。JS中的作用域有全局和局部之分,全局作用域是最外围的一个执行环境,如window,每定义一个函数就定义了一个局部作用域(函数作用域);ES 6中的let命令引入了另外一种局部作用域,块级作用域(“{ }”形成的作用域)。每个作用域都有一个与之关联的变量对象,该对象保存了该作用域中定义的所有变量和函数。
  当JS代码在某个作用域中执行时,会创建变量对象的一个作用域链,这个作用域链可以保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的起始是当前作用域,下一个是当前环境的包含环境(父作用域),依此类推直到全局作用域,由此形成了一个作用域链。作用域链的概念可由图1来刻画。

作用域链

图 1 JavaScript作用域链

  在作用域链中,子作用域(执行环境)中的代码可以访问其父作用域中的变量或函数,但是父作用域中的代码不能访问子作用域中的变量或函数。标志符的解析是沿作用域链从当前作用域向上一级一级地解析,直到解析到全局环境,如果在全局环境还是没有找到标志符的定义,则JS代码会报出”Uncaught ReferenceError”错误

JS的垃圾收集策略

  JS是一门具有内存自动分配和回收机制的语言,开发人员无法进行内存的分配和回收。垃圾回收制度深刻影响着JS的性能。
  目前JS的垃圾搜集算法主要有两种:标记清除和引用计数。标记清除算法是将当前不使用的值加上标记然后回收其内存,如当某个变量离开了当前环境则将其值标记为可以回收。引用计数算法是用来记录每个值被引用的次数,当次数为0时表示该值所占用的内存空间可以回收。引用计数算法会陷入循环引用的问题,如变量a和b互相引用,导致变量a和b所占据的内存均无法回收,消耗系统资源,严重时可极大降低系统性能。

优化内存管理

  尽管JS是自动实现内存的分配与回收,开发人员仍然可以通过养成良好的编码习惯来优化内存管理。对于不再使用的引用类型,开发人员可以手动将其赋值为null,这样做不仅便于JS的垃圾搜集,而且还可以避免循环引用的问题

创建对象的几种方式

  1. new + constructor(构造函数)

    1
    const obj = new Object();
  2. 对象字面量

    1
    const obj = {a: 1, b: 2, c: 3};
  3. 根据给定的原型和属性(可选)来构建对象,Object.create(proto[, propertiesObject])

    1
    2
    3
    4
    5
    6
    7
    // 构造对象的原型为null
    const obj = Object.create(null);
    // 同对象字面量
    const obj = Object.create(Object.prototype);
    // 构造一个原型为fn,包含属性add的对象
    const fn = {a: 1, b: 2};
    const obj = Object.create(fn, {add: {value: 12}});

定义函数的几种方式

  1. 通过函数声明的方式定义

    1
    2
    3
    function example(arg1, arg2) {
    return arg1 + arg2;
    }
  2. 通过函数表达式定义

    1
    2
    3
    const example = function(arg1, arg2) {
    return arg1 + arg2;
    };
  3. 使用构造函数

    1
    const example = new Function("arg1", "arg2", "return arg1 + arg2");

函数声明与函数表达式

  JS解析器会率先读取函数声明,并使函数在执行任何代码之前可以被访问,函数表达式必须等到解析器执行到它所在的代码时才会被执行。这就是函数声明提升的概念。

函数内部对象

  函数内部存在两个特殊的对象:argumentsthisarguments是一个类数组,包含着传入函数中的所有参数,它有一个属性callee,该属性指向拥有这个arguments对象的函数。this引用的是函数据以执行的环境对象。

函数的属性

  JS中的函数均包含两个属性:lengthprototypelength属性表示的是函数接受参数的个数;prototype属性用来保存由构造函数构造的所有对象实例的方法。

如何鉴别数组

  1. (arr instanceof Array) === true;
  2. Array.isArray(arr) === true;
  3. Object.prototype.toString.call(arr) === "[Object Array]"
  4. Object.prototype.toString.apply(arr) === "[Object Array]"