1

[TOC]

本部分主要是针对于JavaScript中常用的数据结构类型进行分析说明。

变量与常量

在JavaScript中,基本的变量声明可以用var方式。JavaScript允许省略var,直接对未声明的变量赋值。也就是说,var a = 1 与 a = 1,这两条语句的效果相同。但是由于这样的做法很容易不知不觉地创建全局变量(尤其是在函数内部),所以建议总是使用var命令声明变量。

在ES6中,对于变量声明的方式进行了扩展。引入了let方式,let即是定义块级别的变量,不会以闭包方式扩展到块外面。另一种引入了可以用于定义常量的const声明。

function f() {
  {
    let x;
    {
      // okay, block scoped name
      const x = "sneaky";
      // error, const
      x = "foo";
    }
    // error, already declared in block
    let x = "inner";
  }
}

变量作用域

变量提升

JavaScript是解释执行的语言, JavaScript引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。

console.log(a);
var a = 1;

上面代码首先使用console.log方法,在控制台(console)显示变量a的值。这时变量a还没有声明和赋值,所以这是一种错误的做法,但是实际上不会报错。因为存在变量提升,真正运行的是下面的代码:

var a;
console.log(a);
a = 1;

最后的结果是显示undefined,表示变量a已声明,但还未赋值。请注意,变量提升只对var命令声明的变量有效,如果一个变量不是用var命令声明的,就不会发生变量提升。

console.log(b);
b = 1;

上面的语句将会报错,提示“ReferenceError: b is not defined”,即变量b未声明,这是因为b不是用var命令声明的,JavaScript引擎不会将其提升,而只是视为对顶层对象的b属性的赋值。

避免全局变量

在计算机编程中,全局变量指的是在所有作用域中都能访问的变量。全局变量是一种不好的实践,因为它会导致一些问题,比如一个已经存在的方法和全局变量的覆盖,当我们不知道变量在哪里被定义的时候,代码就变得很难理解和维护了。在ES6中可以利用let关键字来声明本地变量,好的 JavaScript 代码就是没有定义全局变量的。有一些技术可以帮助你让所有的事情都保持在本地:

为了避免全局变量,第一件事情就是要确保所有的代码都被包在函数中。最简单的办法就是把所有的代码都直接放到一个函数中去:

函数包裹
(function(win) {
    "use strict"; // 进一步避免创建全局变量
    var doc = window.document;
    // 在这里声明你的变量
    // 一些其他的代码
}(window));
声明命名空间
var MyApp = {
    namespace: function(ns) {
        var parts = ns.split("."),
            object = this, i, len;
        for(i = 0, len = parts.lenght; i < len; i ++) {
            if(!object[parts[i]]) {
                object[parts[i]] = {};
            }
            object = object[parts[i]];
        }
    return object;
    }
};

// 定义命名空间
MyApp.namespace("Helpers.Parsing");

// 你现在可以使用该命名空间了
MyApp.Helpers.Parsing.DateParser = function() {
    //做一些事情
};
模块化

另一项开发者用来避免全局变量的技术就是封装到模块 Module 中。一个模块就是不需要创建新的全局变量或者命名空间的通用的功能。不要将所有的代码都放一个负责执行任务或者发布接口的函数中。最常见的 JavaScript 模块类型就是异步模块定义 Asynchronous Module Definition (AMD)

//定义
define( "parsing", //模块名字
        [ "dependency1", "dependency2" ], // 模块依赖
        function( dependency1, dependency2) { //工厂方法

            // Instead of creating a namespace AMD modules
            // are expected to return their public interface
            var Parsing = {};
            Parsing.DateParser = function() {
              //do something
            };
            return Parsing;
        }
);

// 通过 Require.js 加载模块
require(["parsing"], function(Parsing) {
    Parsing.DateParser(); // 使用模块
});

解构赋值

解构赋值允许你使用类似数组或对象字面量的语法将数组和对象的属性赋给各种变量。这种赋值语法极度简洁,同时还比传统的属性访问方法更为清晰。

传统的访问数组前三个元素的方式为:

    var first = someArray[0];
    var second = someArray[1];
    var third = someArray[2];

而通过解构赋值的特性,可以变为:

    var [first, second, third] = someArray;

数组与迭代器

以上是数组解构赋值的一个简单示例,其语法的一般形式为:

    [ variable1, variable2, ..., variableN ] = array;

这将为variable1到variableN的变量赋予数组中相应元素项的值。如果你想在赋值的同时声明变量,可在赋值语句前加入varletconst关键字,例如:

    var [ variable1, variable2, ..., variableN ] = array;
    let [ variable1, variable2, ..., variableN ] = array;
    const [ variable1, variable2, ..., variableN ] = array;

事实上,用变量来描述并不恰当,因为你可以对任意深度的嵌套数组进行解构:

    var [foo, [[bar], baz]] = [1, [[2], 3]];
    console.log(foo);
    // 1
    console.log(bar);
    // 2
    console.log(baz);
    // 3

此外,你可以在对应位留空来跳过被解构数组中的某些元素:

    var [,,third] = ["foo", "bar", "baz"];
    console.log(third);
    // "baz"

而且你还可以通过“不定参数”模式捕获数组中的所有尾随元素:

    var [head, ...tail] = [1, 2, 3, 4];
    console.log(tail);
    // [2, 3, 4]

当访问空数组或越界访问数组时,对其解构与对其索引的行为一致,最终得到的结果都是:undefined

    console.log([][0]);
    // undefined
    var [missing] = [];
    console.log(missing);
    // undefined

请注意,数组解构赋值的模式同样适用于任意迭代器:

    function* fibs() {
      var a = 0;
      var b = 1;
      while (true) {
        yield a;
        [a, b] = [b, a + b];
      }
    }
    var [first, second, third, fourth, fifth, sixth] = fibs();
    console.log(sixth);
    // 5

对象

通过解构对象,你可以把它的每个属性与不同的变量绑定,首先指定被绑定的属性,然后紧跟一个要解构的变量。

    var robotA = { name: "Bender" };
    var robotB = { name: "Flexo" };
    var { name: nameA } = robotA;
    var { name: nameB } = robotB;
    console.log(nameA);
    // "Bender"
    console.log(nameB);
    // "Flexo"

当属性名与变量名一致时,可以通过一种实用的句法简写:

    var { foo, bar } = { foo: "lorem", bar: "ipsum" };
    console.log(foo);
    // "lorem"
    console.log(bar);
    // "ipsum"

与数组解构一样,你可以随意嵌套并进一步组合对象解构:

    var complicatedObj = {
      arrayProp: [
        "Zapp",
        { second: "Brannigan" }
      ]
    };
    var { arrayProp: [first, { second }] } = complicatedObj;
    console.log(first);
    // "Zapp"
    console.log(second);
    // "Brannigan"

当你解构一个未定义的属性时,得到的值为undefined

    var { missing } = {};
    console.log(missing);
    // undefined

请注意,当你解构对象并赋值给变量时,如果你已经声明或不打算声明这些变量(亦即赋值语句前没有letconstvar关键字),你应该注意这样一个潜在的语法错误:

    { blowUp } = { blowUp: 10 };
    // Syntax error 语法错误

为什么会出错?这是因为JavaScript语法通知解析引擎将任何以{开始的语句解析为一个块语句(例如,{console}是一个合法块语句)。解决方案是将整个表达式用一对小括号包裹:

    ({ safe } = {});
    // No errors 没有语法错误

默认值

当你要解构的属性未定义时你可以提供一个默认值:

    var [missing = true] = [];
    console.log(missing);
    // true
    var { message: msg = "Something went wrong" } = {};
    console.log(msg);
    // "Something went wrong"
    var { x = 3 } = {};
    console.log(x);
    // 3

由于解构中允许对对象进行解构,并且还支持默认值,那么完全可以将解构应用在函数参数以及参数的默认值中。

function removeBreakpoint({ url, line, column }) {
      // ...
    }

当我们构造一个提供配置的对象,并且需要这个对象的属性携带默认值时,解构特性就派上用场了。举个例子,jQuery的ajax函数使用一个配置对象作为它的第二参数,我们可以这样重写函数定义:

jQuery.ajax = function (url, {
      async = true,
      beforeSend = noop,
      cache = true,
      complete = noop,
      crossDomain = false,
      global = true,
      // ... 更多配置
    }) {
      // ... do stuff
    };

同样,解构也可以应用在函数的多重返回值中,可以类似于其他语言中的元组的特性:

function returnMultipleValues() {
      return [1, 2];
    }
var [foo, bar] = returnMultipleValues();

Spread Operator(… 扩展操作符)

function myFunction(x, y, z) { }
var args = [0, 1, 2];
myFunction(...args);

还有另外一个好处就是可以用来替换Object.assign来方便地从旧有的对象中创建新的对象,并且能够修改部分值。譬如

var obj = {a:1,b:2}
var obj_new_1 = Object.assign({},obj,{a:3});
var obj_new_2 = {
  ...obj,
  a:3
}

类型/格式判断与转换

typeof

typeof运算符可以返回一个值的数据类型,可能有以下结果。

(1)原始类型

数值、字符串、布尔值分别返回number、string、boolean。

typeof 123 // "number"
typeof "123" // "string"
typeof false // "boolean"

(2)函数

函数返回function。

// 定义一个空函数
function f(){}

typeof f
// "function"

(3)undefined

undefined返回undefined。

typeof undefined
// "undefined"

利用这一点,typeof可以用来检查一个没有声明的变量,而不报错。

v
// ReferenceError: v is not defined

typeof v
// "undefined"

实际编程中,这个特点通常用在判断语句。

// 错误的写法
if (v){
    // ...
}
// ReferenceError: v is not defined

// 正确的写法
if (typeof v === "undefined"){
    // ...
}

(4)其他

除此以外,都返回object。

typeof window // "object"
typeof {} // "object"
typeof [] // "object"
typeof null // "object"

从上面代码可以看到,空数组([])的类型也是object,这表示在JavaScript内部,数组本质上只是一种特殊的对象。另外,null的类型也是object,这是由于历史原因造成的,为了兼容以前的代码,后来就没法修改了,并不是说null就属于对象,本质上null是一个类似于undefined的特殊值。

instanceof

typeof对数组(array)和对象(object)的显示结果都是object,那么怎么区分它们呢?instanceof运算符可以做到。

var o = {};
var a = [];

o instanceof Array // false
a instanceof Array // true

类型的自动转换

当遇到以下几种情况,JavaScript会自动转换数据类型:

  • 不同类型的数据进行互相运算;

  • 对非布尔值类型的数据求布尔值;

  • 对非数值类型的数据使用一元运算符(即“+”和“-”)。

基本类型(Basic)

数值类型

科学计算

随机数

random() 方法可返回介于 0 ~ 1 之间的一个随机数。

<script type="text/javascript">
document.write(Math.random())
</script>

类型转换

使用Number函数,可以将任意类型的值转化成数字。

  • 数值:转换后还是原来的值。

  • 字符串:如果可以被解析为数值,则转换为相应的数值,否则得到NaN。空字符串转为0。

  • 布尔值:true转成1,false转成0。

  • undefined:转成NaN。

  • null:转成0。

Number函数将字符串转为数值,要比parseInt函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为NaN。

parseInt('011') // 9
parseInt('42 cats') // 42
parseInt('0xcafebabe') // 3405691582

Number('011') // 11
Number('42 cats') // NaN
Number('0xcafebabe') // 3405691582

如果Number传入的参数是一个对象,那么转换规则会相对复杂一点,具体而言描述如下:

  1. 先调用对象自身的valueOf方法,如果该方法返回原始类型的值(数值、字符串和布尔值),则直接对该值使用Number方法,不再进行后续步骤。

  2. 如果valueOf方法返回复合类型的值,再调用对象自身的toString方法,如果toString方法返回原始类型的值,则对该值使用Number方法,不再进行后续步骤。

  3. 如果toString方法返回的是复合类型的值,则报错。

格式化显示

这里用的是numeraljs,格式化显示的效果如下所示:

  • Number

Number Format String
10000 '0,0.0000' 10,000.0000
10000.23 '0,0' 10,000
10000.23 '+0,0' +10,000
  • Currency

Number Format String
1000.234 '$0,0.00' $1,000.23
1000.2 '0,0[.]00 $' 1,000.20 $
  • Bytes

Number Format String
100 '0b' 100B
2048 '0 b' 2 KB
  • Percentages

Number Format String
1 '0%' 100%
0.974878234 '0.000%' 97.488%
  • Time

Number Format String
25 '00:00:00' 0:00:25
238 '00:00:00' 0:03:58

布尔类型

布尔值代表“真”和“假”两个状态。“真”用关键字true表示,“假”用关键字false表示。布尔值只有这两个值。如果JavaScript预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为false,其他值都视为true。

  • undefined

  • null

  • false

  • 0

  • NaN

  • ""(空字符串)

类型转换

所有对象的布尔值都是true,甚至连false对应的布尔对象也是true。

Boolean(new Boolean(false))
// true

空类型

JavaScript中常见的空类型为undefined与null,不过typeof undefined === ‘undefined’typeof null === ‘object’

null表示"没有对象",即该处不应该有值。典型用法是:

  • 作为函数的参数,表示该函数的参数是对象。

  • 作为对象原型链的终点。

undefined表示"缺少值",就是此处应该有一个值,但是还未定义。典型用法是:

  • 变量被声明了,但没有赋值时,就等于undefined。

  • 调用函数时,应该提供的参数没有提供,该参数等于undefined。

  • 对象没有赋值的属性,该属性的值为undefined。

  • 函数没有返回值时,默认返回undefined。

Symbols

Symbols是JavaScript的第七种原始类型,它代指一个全局唯一的不可变对象。如果需要创建一个Symbol对象,则需要调用Symbol函数:

var sym1 = Symbol();
var sym2 = Symbol("foo");
var sym3 = Symbol("foo");

如上的代码会创建三个新的符号,注意,虽然Symbol使用了”foo”这个字符串作为输入对象,但是每次会创建一个新的符号:

Symbol("foo") === Symbol("foo"); // false

确切地说,symbol与其它类型并不完全相像。symbol被创建后就不可变更,你不能为它设置属性(在严格模式下尝试设置属性会得到TypeError的错误)。他们可以用作属性名称,这些性质与字符串类似。

另一方面,每一个symbol都独一无二,不与其它symbol等同,即使二者有相同的描述也不相等;你可以轻松地创建一个新的symbol。这些性质与对象类似。

ES6中的symbol与Lisp和Ruby这些语言中更传统的symbol类似,但不像它们集成得那么紧密。在Lisp中,所有的标识符都是symbol;在JS中,标识符和大多数的属性键仍然是字符串,symbol只是一个额外的选项。

关于symbol的忠告:symbol不能被自动转换为字符串,这和语言中的其它类型不同。尝试拼接symbol与字符串将得到TypeError错误。

> var sym = Symbol("<3");
> "your symbol is " + sym
// TypeError: can't convert symbol to string
> `your symbol is ${sym}`
// TypeError: can't convert symbol to string

有三种获取symbol的方法。

  • 调用Symbol()。正如我们上文中所讨论的,这种方式每次调用都会返回一个新的唯一symbol。

  • 调用Symbol.for(string)。这种方式会访问symbol注册表,其中存储了已经存在的一系列symbol。这种方式与通过Symbol()定义的独立symbol不同,symbol注册表中的symbol是共享的。如果你连续三十次调用Symbol.for("cat"),每次都会返回相同的symbol。注册表非常有用,在多个web页面或同一个web页面的多个模块中经常需要共享一个symbol。

  • 使用标准定义的symbol,例如:Symbol.iterator。标准根据一些特殊用途定义了少许的几个symbol。

字符串类型

创建增删

插值

ES6中开始支持较为复杂的模板字符串方式:

// Basic literal string creation
`In JavaScript '\n' is a line-feed.`

// Multiline strings
`In JavaScript this is
 not legal.`

// String interpolation
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

// Construct an HTTP request prefix is used to interpret the replacements and construction
GET`http://foo.org/bar?a=${a}&b=${b}
    Content-Type: application/json
    X-Credentials: ${credentials}
    { "foo": ${foo},
      "bar": ${bar}}`(myOnReadyStateChangeHandler);

随机字符串

UUID

类型编码

类型转换

使用String函数,可以将任意类型的值转化成字符串。规则如下:

  • 数值:转为相应的字符串。

  • 字符串:转换后还是原来的值。

  • 布尔值:true转为“true”,false转为“false”。

  • undefined:转为“undefined”。

  • null:转为“null”。

而对于较复杂一点的对象类型,转换规则如下:

  1. 先调用toString方法,如果toString方法返回的是原始类型的值,则对该值使用String方法,不再进行以下步骤。

  2. 如果toString方法返回的是复合类型的值,再调用valueOf方法,如果valueOf方法返回的是原始类型的值,则对该值使用String方法,不再进行以下步骤。

  3. 如果valueOf方法返回的是复合类型的值,则报错。

HTML编码

function html_encode(str)   
{   
  var s = "";   
  if (str.length == 0) return "";   
  s = str.replace(/&/g, "&gt;");   
  s = s.replace(/</g, "&lt;");   
  s = s.replace(/>/g, "&gt;");   
  s = s.replace(/ /g, "&nbsp;");   
  s = s.replace(/\'/g, "&#39;");   
  s = s.replace(/\"/g, "&quot;");   
  s = s.replace(/\n/g, "<br>");   
  return s;   
}   

function html_decode(str)   
{   
  var s = "";   
  if (str.length == 0) return "";   
  s = str.replace(/&gt;/g, "&");   
  s = s.replace(/&lt;/g, "<");   
  s = s.replace(/&gt;/g, ">");   
  s = s.replace(/&nbsp;/g, " ");   
  s = s.replace(/&#39;/g, "\'");   
  s = s.replace(/&quot;/g, "\"");   
  s = s.replace(/<br>/g, "\n");   
  return s;   
}

其他操作

Reverse

str.split('').reverse().join('');

Date Time

Built-in:Date

  • 根据毫秒计算对应的天/时/分/秒

    var left_time = <!--{$left_time}-->;
        function GetRTime(){
         //var NowTime = new Date();
         <!--获取当前时间,并使用当前时间减去开始时间-->
         //var nMS = startTime.getTime() - NowTime.getTime() 
         left_time = left_time - 1000;
         var nMS = left_time;
         var nD = Math.floor(nMS/(1000 * 60 * 60 * 24));
         var nH = Math.floor(nMS/(1000*60*60)) % 24;
         var nM = Math.floor(nMS/(1000*60)) % 60;
         var nS = Math.floor(nMS/1000) % 60;
         if (nMS < 0){
         }else{
         }
        }

Moment:JS的第三方时间库

Format
moment().format('MMMM Do YYYY, h:mm:ss a'); // August 26th 2015, 9:13:04 pm
moment().format('dddd');                    // Wednesday
moment().format("MMM Do YY");               // Aug 26th 15
moment().format('YYYY [escaped] YYYY');     // 2015 escaped 2015
moment().format();                          // 2015-08-26T21:13:04+08:00
var dateString = moment.unix(value).format("MM/DD/YYYY");

如果需要显示本地化时间,需要引入locale目录下对应文件,并且使用moment.locale(String);进行设置。另外注意,Unix时间戳指的是

Difference&&Duration:时间差
var a = moment([2007, 0, 29]);
var b = moment([2007, 0, 28]);
a.diff(b) // 86400000 milliseconds
a.diff(b, 'days') // 1

如果需要将duration格式化为较好的显示,可以参考moment-duration-format

TimeZone(时间本地化)

Indexed Collection

Array

在JavaScript中,Array是一个全局的对象可以用来创建数组,是一个高级别的有点类似于列表的集合。可以直接使用Array.length来获取某个数组的长度。

创建增删

JavaScript创建新的数组,可以采用如下方式:

arr = []
arr = new Array()
arr = new Array([PreSize])

arr = []
arr[1] = 1 // arr = [undefined , 1] 对于不存在的自动赋值为undefined
复制

slice() 方法把数组中一部分的浅复制(shallow copy)存入一个新的数组对象中,并返回这个新的数组。array.slice(begin[, end]).
slice会提取原数组中索引从 begin 到 end 的所有元素(包含begin,但不包含end)。
slice(1,4) 提取原数组中的第二个元素开始直到第四个元素的所有元素 (索引为 1, 2, 3的元素)。
例子:返回数组中的一部分

// Our good friend the citrus from fruits example
var fruits = ["Banana", "Orange", "Lemon", "Apple", "Mango"];
var citrus = fruits.slice(1, 3);

// puts --> ["Orange","Lemon"]

例子:使用 slice

在下例中, slice从`myCar中创建了一个新数组`newCar.两个数组都包含了一个myHonda对象的引用. 当myHonda的color属性改变为purple, 则两个数组中的对应元素都会随之改变.

// 使用slice方法从myCar中创建一个newCar.
var myHonda = { color: "red", wheels: 4, engine: { cylinders: 4, size: 2.2 } };
var myCar = [myHonda, 2, "cherry condition", "purchased 1997"];
var newCar = myCar.slice(0, 2);

// 输出myCar, newCar,以及各自的myHonda对象引用的color属性.
print("myCar = " + myCar.toSource());
print("newCar = " + newCar.toSource());
print("myCar[0].color = " + myCar[0].color);
print("newCar[0].color = " + newCar[0].color);

// 改变myHonda对象的color属性.
myHonda.color = "purple";
print("The new color of my Honda is " + myHonda.color);

//输出myCar, newCar中各自的myHonda对象引用的color属性.
print("myCar[0].color = " + myCar[0].color);
print("newCar[0].color = " + newCar[0].color);

上述代码输出:

myCar = [{color:"red", wheels:4, engine:{cylinders:4, size:2.2}}, 2, "cherry condition", "purchased 1997"]
newCar = [{color:"red", wheels:4, engine:{cylinders:4, size:2.2}}, 2]
myCar[0].color = red 
newCar[0].color = red
The new color of my Honda is purple
myCar[0].color = purple
newCar[0].color = purple

类数组(Array-like)对象

slice 方法可以用来将一个类数组(Array-like)对象/集合转换成一个数组。你只需将该方法绑定到这个对象上。下述代码中 list 函数中的 arguments 就是一个类数组对象。

function list() {
  return Array.prototype.slice.call(arguments);
}

var list1 = list(1, 2, 3); // [1, 2, 3]

除了使用 Array.prototype.slice.call(arguments),你也可以简单的使用 [].slice.call(arguments) 来代替。另外,你可以使用 bind 来简化该过程。

var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.call.bind(unboundSlice);

function list() {
  return slice(arguments);
}

var list1 = list(1, 2, 3); // [1, 2, 3]
插入删除

JavaScript的Array支持从头部插入移除,从尾部插入移除等多种方式:

var fruits = ["Apple", "Banana"];

var newLength = fruits.push("Orange");//从尾部插入,返回的数组长度
// ["Apple", "Banana", "Orange"]

var last = fruits.pop(); // remove Orange (from the end),返回删除的对象
// ["Apple", "Banana"];

var first = fruits.shift(); // remove Apple from the front
// ["Banana"];

var newLength = fruits.unshift("Strawberry") // add to the front
// ["Strawberry", "Banana"];

遍历索引

存在性判断


  var array = [1, 2, 3];
  array.includes(1);
  // → true
反向索引

如果在数组中需要反向索引某个元素的位置,可以使用indexOf方法

fruits.push("Mango");
// ["Strawberry", "Banana", "Mango"]

var pos = fruits.indexOf("Banana");
// 1
  var array = [2, 9, 9, 4, 3, 6];
  var result = array.lastIndexOf(9);    
  console.log(result); 
  // output: 2

如果存储的是复杂的对象,则可以使用find方法,返回数组中满足测试条件的一个元素,如果没有满足条件的元素,则返回 undefined。

  _.find(users, function(o) { return o.age < 40; });
  // output: object for 'barney'

  // Native
  var users = [
    { 'user': 'barney',  'age': 36, 'active': true },
    { 'user': 'fred',    'age': 40, 'active': false },
    { 'user': 'pebbles', 'age': 1,  'active': true }
  ];

  users.find(function(o) { return o.age < 40; });
  // output: object for 'barney'
  
  var index =  users.findIndex(function(o) { return o.age >= 40; });
  console.log(index);
  // output: 1
遍历
fruits.forEach(function (item, index, array) {
  console.log(item, index);
});
// Apple 0
// Banana 1

Set:集合

集合也是一种常见的数据结构,在ES6中添加了对于集合的支持,其基本用法如下:

// Sets
var s = new Set();
s.add("hello").add("goodbye").add("hello");
s.size === 2;
s.has("hello") === true;

WeakSet

WeakSet提供了一种自回收的存储方式:

// Weak Sets
var ws = new WeakSet();
ws.add({ data: 42 });
// Because the added object has no other references, it will not be held in the set

序列操作

map

将某个列表中的元素映射到新的列表中。

  // Native
  var array1 = [1, 2, 3];
  var array2 = array1.map(function(value, index) {
    return value*2;
  });
  console.log(array2);
  // output: [2, 4, 6]

reduce

  var array = [0, 1, 2, 3, 4];
  var result = array.reduce(function (previousValue, currentValue, currentIndex, array) {
    return previousValue + currentValue;
  });
  console.log(result);
  // output: 10
  
  //reduceRight,正好方向相反
  var array = [0, 1, 2, 3, 4];
  var result = array.reduceRight(function (previousValue, currentValue, currentIndex, array) {
    return previousValue - currentValue;
  });
  console.log(result);
  // output: -2

filter

  // Native
  function isBigEnough(value) {
    return value >= 10;
  } 
  var array = [12, 5, 8, 130, 44];
  var filtered = array.filter(isBigEnough);
  console.log(filtered);
  // output: [12, 130, 44]

Keyed Collections

Object

JavaScript中Object是一个混合了类似于Dictionary与Class的用法,基本上来说也是一种键值类型。其中键的类型主要包含三种:

  • Identifier:包含任何有效地标识符,包括了ES的保留关键字。

  • 字符串:single (') or double (") quotes. 'foo', "bar",'qu\'ux', "" (the empty string), and 'Ich \u2665 B\xFCcher' are all valid string literals.

  • 数字:decimal literal (e.g. 0, 123, 123., .123, 1.23, 1e23, 1E-23, 1e+23, 12, but not 01, +123 or -123) or a hex integer literal (0[xX][0-9a-fA-F]+ in regex, e.g. 0xFFFF, 0X123,0xaBcD).

var object = {
  // `abc` is a valid identifier; no quotes are needed
  abc: 1,
  // `123` is a numeric literal; no quotes are needed
  123: 2,
  // `012` is an octal literal with value `10` and thus isn’t allowed in strict mode; but if you insist on using it, quotes aren’t needed
  012: 3,
  // `π` is a valid identifier; no quotes are needed
  π: Math.PI,
  // `var` is a valid identifier name (although it’s a reserved word); no quotes are needed
  var: 4,
  // `foo bar` is not a valid identifier name; quotes are required
  'foo bar': 5,
  // `foo-bar` is not a valid identifier name; quotes are required
  'foo-bar': 6,
  // the empty string is not a valid identifier name; quotes are required
  '': 7
};

注意,与object不同的是,JSON 只允许用双引号 (") 包裹的字符串作为键名。而如果要根据键名进行索引的话,可以使用方括号,这种方式对于三种键值皆有效:

object['abc']; // 1

有时候也可以使用点操作符,不过这种方式只可以被用于键为有效地Identifier情况:

object.abc; // 1

如果需要获取所有的键名的话,可以使用Object.keys方法:

注意,所有的Object的方法只能用Object.methodName方式调用



  // Native
  var result2 = Object.keys({one: 1, two: 2, three: 3});
  console.log(result2); 
  // output: ["one", "two", "three"]
    //也可以等效获取大小

  var result2 = Object.keys({one: 1, two: 2, three: 3}).length;
  console.log(result2); 
  // output: 3

创建添加

Object.create() :创建一个拥有指定原型和若干个指定属性的对象。

其基本语法为:

Object.create(proto, { propertiesObject })

这里需要注意的是,propertiesObject不是一个简单的键值类型,而是有固定格式的object。

var o;

// 创建一个原型为null的空对象
o = Object.create(null);

o = {};
// 以字面量方式创建的空对象就相当于:
o = Object.create(Object.prototype);

o = Object.create(Object.prototype, {
  // foo会成为所创建对象的数据属性
  foo: { writable:true, configurable:true, value: "hello" },
  // bar会成为所创建对象的访问器属性
  bar: {
    configurable: false,
    get: function() { return 10 },
    set: function(value) { console.log("Setting `o.bar` to", value) }
}})


function Constructor(){}
o = new Constructor();
// 上面的一句就相当于:
o = Object.create(Constructor.prototype);
// 当然,如果在Constructor函数中有一些初始化代码,Object.create不能执行那些代码


// 创建一个以另一个空对象为原型,且拥有一个属性p的对象
o = Object.create({}, { p: { value: 42 } })

// 省略了的属性特性默认为false,所以属性p是不可写,不可枚举,不可配置的:
o.p = 24
o.p
//42

o.q = 12
for (var prop in o) {
   console.log(prop)
}
//"q"

delete o.p
//false

//创建一个可写的,可枚举的,可配置的属性p
o2 = Object.create({}, { p: { value: 42, writable: true, enumerable: true, configurable: true } });
Object.assign:一层浅复制

Object.assign() 方法可以把任意多个的源对象所拥有的自身可枚举属性拷贝给目标对象,然后返回目标对象。Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象身上。注意,对于访问器属性,该方法会执行那个访问器属性的 getter 函数,然后把得到的值拷贝给目标对象,如果你想拷贝访问器属性本身,请使用 Object.getOwnPropertyDescriptor()Object.defineProperties() 方法。

注意,字符串类型和 symbol 类型的属性都会被拷贝。

注意,在属性拷贝过程中可能会产生异常,比如目标对象的某个只读属性和源对象的某个属性同名,这时该方法会抛出一个 TypeError 异常,拷贝过程中断,已经拷贝成功的属性不会受到影响,还未拷贝的属性将不会再被拷贝。

注意, Object.assign 会跳过那些值为 nullundefined 的源对象。

Object.assign(target, ...sources)
  • 例子:浅拷贝一个对象

var obj = { a: 1 };
var copy = Object.assign({}, obj);
console.log(copy); // { a: 1 }
  • 例子:合并若干个对象

var o1 = { a: 1 };
var o2 = { b: 2 };
var o3 = { c: 3 };

var obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(o1);  // { a: 1, b: 2, c: 3 }, 注意目标对象自身也会改变。
  • 例子:拷贝 symbol 类型的属性

var o1 = { a: 1 };
var o2 = { [Symbol("foo")]: 2 };

var obj = Object.assign({}, o1, o2);
console.log(obj); // { a: 1, [Symbol("foo")]: 2 }
  • 例子:继承属性和不可枚举属性是不能拷贝的

var obj = Object.create({foo: 1}, { // foo 是个继承属性。
    bar: {
        value: 2  // bar 是个不可枚举属性。
    },
    baz: {
        value: 3,
        enumerable: true  // baz 是个自身可枚举属性。
    }
});

var copy = Object.assign({}, obj);
console.log(copy); // { baz: 3 }
  • 例子:原始值会被隐式转换成其包装对象

var v1 = "123";
var v2 = true;
var v3 = 10;
var v4 = Symbol("foo")

var obj = Object.assign({}, v1, null, v2, undefined, v3, v4); 
// 源对象如果是原始值,会被自动转换成它们的包装对象,
// 而 null 和 undefined 这两种原始值会被完全忽略。
// 注意,只有字符串的包装对象才有可能有自身可枚举属性。
console.log(obj); // { "0": "1", "1": "2", "2": "3" }
  • 例子:拷贝属性过程中发生异常

var target = Object.defineProperty({}, "foo", {
    value: 1,
    writeable: false
}); // target 的 foo 属性是个只读属性。

Object.assign(target, {bar: 2}, {foo2: 3, foo: 3, foo3: 3}, {baz: 4});
// TypeError: "foo" is read-only
// 注意这个异常是在拷贝第二个源对象的第二个属性时发生的。

console.log(target.bar);  // 2,说明第一个源对象拷贝成功了。
console.log(target.foo2); // 3,说明第二个源对象的第一个属性也拷贝成功了。
console.log(target.foo);  // 1,只读属性不能被覆盖,所以第二个源对象的第二个属性拷贝失败了。
console.log(target.foo3); // undefined,异常之后 assign 方法就退出了,第三个属性是不会被拷贝到的。
console.log(target.baz);  // undefined,第三个源对象更是不会被拷贝到的。

不过需要注意的是,assign是浅拷贝,或者说,它是一级深拷贝,举两个例子说明:

const defaultOpt = {
    title: {
        text: 'hello world',
        subtext: 'It\'s my world.'
    }
};

const opt = Object.assign({}, defaultOpt, {
    title: {
        subtext: 'Yes, your world.'
    }
});

console.log(opt);

// 预期结果
{
    title: {
        text: 'hello world',
        subtext: 'Yes, your world.'
    }
}
// 实际结果
{
    title: {
        subtext: 'Yes, your world.'
    }
}

上面这个例子中,对于对象的一级子元素而言,只会替换引用,而不会动态的添加内容。那么,其实assign并没有解决对象的引用混乱问题,参考下下面这个例子:

const defaultOpt = {
    title: {
        text: 'hello world',
        subtext: 'It\'s my world.'
    } 
};

const opt1 = Object.assign({}, defaultOpt);
const opt2 = Object.assign({}, defaultOpt);
opt2.title.subtext = 'Yes, your world.';

console.log('opt1:');
console.log(opt1);
console.log('opt2:');
console.log(opt2);

// 结果
opt1:
{
    title: {
        text: 'hello world',
        subtext: 'Yes, your world.'
    }
}
opt2:
{
    title: {
        text: 'hello world',
        subtext: 'Yes, your world.'
    }
}

Map

创建增删

var map = new Map();
map.set("foo", "bar");
console.log(map.get("foo")); //logs "bar"
var animalSounds = new Map();

animalSounds.set("dog", "woof");
animalSounds.set("cat", "meow");
animalSounds.set("frog", "ribbit");

console.log(animalSounds.size); //logs 3
console.log(animalSounds.has("dog")); //logs true

animalSounds.delete("dog");

console.log(animalSounds.size); //logs 2
console.log(animalSounds.has("dog")); //logs false

animalSounds.clear();
console.log(animalSounds.size); //logs 0

索引遍历

usersMap = new Map();
usersMap.set(1, "sally");
usersMap.set(2, "bob");
usersMap.set(3, "jane");

console.log(usersMap.get(1)); //logs "sally"
usersMap.forEach(function (username, userId) {
  console.log(userId, typeof userId); //logs 1..3, "number"
  if (userId === 1) {
     console.log("We found sally.");
  }
});

//如果用for...of方式遍历,每次返回的是一个Array
for (data of usersMap) {
    console.log(data);//Array [1,"sally"]
}

Map的键的类型可以是object、NaN等等。

var obj, map;
map = new Map();
obj = {foo: "bar"};
map.set(obj, "foobar");
obj.newProp = "stuff";
console.log(map.has(obj)); //logs true
console.log(map.get(obj)); //logs "foobar"

Immutable.js

FaceBook-Immutable
Immutable 详解及 React 中实践

Immutable 对象一旦被创建之后即不可再更改,这样可以使得应用开发工作变得简化,不再需要大量的保护性拷贝,使用简单的逻辑控制即可以保证内存控制与变化检测。Immutable.js虽然和React同期出现且跟React配合很爽,但它可不是React工具集里的(它的光芒被掩盖了),它是一个完全独立的库,无论基于什么框架都可以用它。意义在于它弥补了Javascript没有不可变数据结构的问题。不可变数据结构是函数式编程中必备的。前端工程师被OOP洗脑太久了,组件根本上就是函数用法,FP的特点更适用于前端开发。

Javascript中对象都是参考类型,也就是a={a:1}; b=a; b.a=10;你发现a.a也变成10了。可变的好处是节省内存或是利用可变性做一些事情,但是,在复杂的开发中它的副作用远比好处大的多。于是才有了浅copy和深copy,就是为了解决这个问题。举个常见例子:

var  defaultConfig = { /* 默认值 */};

var config = $.extend({}, defaultConfig, initConfig); // jQuery用法。initConfig是自定义值

var config = $.extend(true, {}, defaultConfig, initConfig); // 如果对象是多层的,就用到deep-copy了

var stateV1 = Immutable.fromJS({  
users: [
{ name: 'Foo' },
{ name: 'Bar' }
]
});
var stateV2 = stateV1.updateIn(['users', 1], function () {  
return Immutable.fromJS({
name: 'Barbar'
});
});
stateV1 === stateV2; // false  
stateV1.getIn(['users', 0]) === stateV2.getIn(['users', 0]); // true  
stateV1.getIn(['users', 1]) === stateV2.getIn(['users', 1]); // false

如上,我们可以使用===来通过引用来比较对象,这意味着我们能够方便快速的进行对象比较,并且它能够和React中的PureRenderMixin 兼容。基于此,我们可以在整个应用构建中使用Immutable.js。也就是说,我们的Flux Store应该是一个具有不变性的对象,并且我们通过 将具有不变性的数据作为属性传递给我们的应用程序。

创建与判断

如果要创建Immutable对象,使用fromJS方法既可以将简单的JS中的objects与arrays转化为不可变的Maps与Lists

fromJS(json: any, reviver?: (k: any, v: Iterable<any, any>) => any): any

如果reviver这个属性被提供了,那么它会传入一个Seq对象并且被循环调用,对于顶层对象,它的默认的键为""

Immutable.fromJS({a: {b: [10, 20, 30]}, c: 40}, function (key, value) {
  var isIndexed = Immutable.Iterable.isIndexed(value);
  return isIndexed ? value.toList() : value.toOrderedMap();
});

// true, "b", {b: [10, 20, 30]}
// false, "a", {a: {b: [10, 20, 30]}, c: 40}
// false, "", {"": {a: {b: [10, 20, 30]}, c: 40}}

对于转化而来的Immutable对象,可以通过Iterable.is*方法来判断其是列表还是映射或者其他数据类型。

List

创建增删

更新
删除

遍历索引

Map

创建增删

更新

要更新Map中的某个元素值,需要调用updateIn方法,该方法会根据传入的keyPath寻找到该值,然后进行安全修正并返回一个新的对象。如果keyPath值并不存在,那么返回的Map对象会自动创建该键,如果KeyPath没有提供指定值,那么会自动调用notSetValue或者赋值为undefined

updateIn(keyPath: Array<any>, updater: (value: any) => any): Map<K, V>
updateIn(
keyPath: Array<any>,
notSetValue: any,
updater: (value: any) => any
): Map<K, V>
updateIn(keyPath: Iterable<any, any>, updater: (value: any) => any): Map<K, V>
updateIn(
keyPath: Iterable<any, any>,
notSetValue: any,
updater: (value: any) => any

): Map<K, V>

var data = Immutable.fromJS({ a: { b: { c: 10 } } });
data = data.updateIn(['a', 'b', 'c'], val => val * 2);
// { a: { b: { c: 20 } } }
删除

王下邀月熊_Chevalier
22.5k 声望8.5k 粉丝

爱代码 爱生活 希望成为全栈整合师