1.学习TypeScript——使用VS Code搭建TypeScript开发环境

前言

TypeScript和VSCode已经成为了2016年前端开发最流行的开发语言和开发工具之一,更多请看2016年JavaScript流行详情。使用TypeScript语言开发的类库和框架也越来越多,鼎鼎大名的Google的Angular 2中文网)项目就是使用TypeScript作为主要的开发语言的(还有JS版本和Dart版本)。

现在浏览器显然已经不能满足了JS了,很多App也开始使用JS开发了,使用基于TypeScript的IonicNativeScript能够快速高效地帮助你开发APP。

TypeScript是JavaScript的超集,TypeScript只是增强了JavaScript而非改变了JavaScript,TypeScript同样也是基于ECMAScript标准的编程语言。因此非常流行的Vue和React及我们常用Jquery等类库都可以使用TypeScript来编码,TypeScript强大的智能类型分析系统,能够使你的代码更加强壮。

说了这么多TypeScript的优点,下面让我们来学习如果在VScode中使用TypeScript开发项目吧。

下载TypeScript

TypeScript官网中文网)提供了多种下载方式,我们使用NPM来下载TypeScript,如果你没有安装NPM,请到Nodejs官网进行安装。

在CMD(Windows系统)或者终端(macOS系统)中输入一下命令:

npm install -g typescript

安装完毕后我们输入tsc -v可以查看当前安装的TypeScript版本号。当前最新的版本是2.1.5。

下载VSCode

VSCode是我使用过最棒的编辑器没有之一,比Sublime Text还有优秀。

VSCode官网就可以下载相应系统的VSCode安装包,最新的VSCode版本是1.9。

VSCode下载图

创建项目

创建目录

首先我们创建一个项目文件夹,比如叫ts_vscode,然后创建一些主要目录,在VSCode中打开项目,在macOS上我们可以直接使用命令来操作:

# 创建ts_vscode及主要文件夹
mkdir -p ts_vscode/src/{css,images,fonts}

# 使用VSCode打开
code ts_vscode

目录结构:

ts_vscode/
    └─src/
        ├─ css/
        ├─ fonts/
        └─ images/

创建package.json

使用命令npm init来创建package.json文件,一般默认就可以,具体详情可以看这里

创建

目录结构:

ts_vscode/
    ├─ src/
    │    ├─ css/
    │    ├─ fonts/
    │    └─ images/
    └─ package.json

创建tsconfig.json

使用tsc --init命令就可以快速创建一个tsconfig.json文件,关于tsconfig.json的属性描述请访问这里

目录结构:

ts_vscode/
    ├─ src/
    │    ├─ css/
    │    ├─ fonts/
    │    └─ images/
    ├─ package.json
    └─ tsconfig.json

安装项目依赖和开发依赖

我们的这个项目使用jQuery和Bootstrap来做,因此我们使用npm来安装:

npm install jquery bootstrap --save

我们的项目使用了TypeScript来开发,因此我们需要下载相对应的声明文件,关于声明文件请访问这里。安装声明文件命令:

npm install @types/jquery @types/bootstrap -save-dev

创建index.html

ts_vscode目录下创建index.htmlindex.html文件中放入下面的代码:

<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>TypeScript with VSCode</title>
  <!-- Bootstrap -->
  <link rel="stylesheet" href="./node_modules/bootstrap/dist/css/bootstrap.min.css">
  <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
  <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
  <!--[if lt IE 9]>
    <script src="https://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script>
    <script src="https://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
  <![endif]-->
  <link rel="stylesheet" href="./src/css/index.css">
</head>

<body>
  <h1 class="text-center"></h1>
  <div class="container">
    <div class="row show-grid">
      <div class="col-md-1">.col-md-1</div>
      <div class="col-md-1">.col-md-1</div>
      <div class="col-md-1">.col-md-1</div>
      <div class="col-md-1">.col-md-1</div>
      <div class="col-md-1">.col-md-1</div>
      <div class="col-md-1">.col-md-1</div>
      <div class="col-md-1">.col-md-1</div>
      <div class="col-md-1">.col-md-1</div>
      <div class="col-md-1">.col-md-1</div>
      <div class="col-md-1">.col-md-1</div>
      <div class="col-md-1">.col-md-1</div>
      <div class="col-md-1">.col-md-1</div>
    </div>
    <div class="row show-grid">
      <div class="col-md-8">.col-md-8</div>
      <div class="col-md-4">.col-md-4</div>
    </div>
    <div class="row show-grid">
      <div class="col-md-4">.col-md-4</div>
      <div class="col-md-4">.col-md-4</div>
      <div class="col-md-4">.col-md-4</div>
    </div>
    <div class="row show-grid">
      <div class="col-md-6">
        <button type="button" class="btn btn-default" aria-label="Left Align">
          <span class="glyphicon glyphicon-align-left" aria-hidden="true"></span>
        </button>
      </div>
      <div class="col-md-6">
        <button type="button" class="btn btn-default btn-lg">
          <span class="glyphicon glyphicon-star" aria-hidden="true"></span> Star
        </button>
      </div>
    </div>
  </div>
  <!-- /.modal -->
  <!-- js -->
  <script src="./node_modules/jquery/dist/jquery.min.js"></script>
  <script src="./node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
  <script src="./src/index.js"></script>
</body>

</html>

目录结构:

ts_vscode/
    ├─ src/
    │    ├─ css/
    │    ├─ fonts/
    │    └─ images/
    ├─ index.html
    ├─ package.json
    └─ tsconfig.json

创建index.ts文件并编写TS代码

src目录下创建index.ts文件。我们使用TS编写一个类,并使用这个类的方法向index.htmlh1标签中插入一句话,代码如下:

class Main {
    constructor() {

    }

    name: string;

    show(text: string): void {
        let $ele: JQuery = $("h1");
        $ele.text(text);
    }
}

let main = new Main();
main.name = "Hello TypeScript";
main.show(main.name);

目录结构:

ts_vscode/
    ├─ src/
    │    ├─ css/
    │    ├─ fonts/
    │    ├─ images/
    │    └─ index.ts
    ├─ index.html
    ├─ package.json
    └─ tsconfig.json

编译和启动项目

我们使用package.json中的scripts来编译和启动项目。

编译比较简单,tsc命令就可以编译项目,tsc -w命令监控并自动编译,编译会使用tsconfig.json中的配置项。

启动项目我们安装live-server,来帮助我们启动一个服务器环境,live-server非常轻便且带有自动刷新功能,我们使用npm全局安装即可:

npm install -g live-server

安装完毕后,我们修改package.json中的scripts如下:

"scripts": {
    "test": "tsc -w & live-server"
}

最终目录结构:

最终目录结构

最后我们在终端中输入npm t就可以启动项目了,启动成功后如下图:

启动项目成功

尝试改变项目文件,你可以发现浏览器会自动编译和自动刷新哦。

总结

本文章我们认识了VSCode和TypeScript,学会了基本的安装和编译及简单的TypeScript代码编写。本篇文章的示例代码请访这里

下篇文章我们将学习如何构建打包TypeScript项目。

[译]TypeScript 2.1 候选版:更好的类型推断、异步函数和其它更多特性

今天我们非常高兴地宣布发布TypeScript 2.1 候选版!如果您不熟悉它,TypeScriptTypeScript中文网)是一种向JavaScript添加可选静态类型的语言,并将来自ES6和更高版本的新特性带到您使用的任何JavaScript运行时。

像往常一样,你可以通过NuGet得到RC,或仅通过运行

npm install -g typescript@rc

然后,您可以轻松地在Visual Studio Code我们的Sublime Text插件中使用RC版本。

在VS2015上安装Update 3后,就可以下载Visual Studio 2015的TypeScript 2.1安装包

虽然TypeScript 2.1有很多很棒的功能,但我们想要强调的是,TypeScript 2.1的类型推断功能将会更强大,以及更容易在所有运行时编写异步代码。

更加智能的类型推断

TypeScript 2.1现在可以更容易地模拟逐渐初始化变量的场景。由于很多代码在JavaScript中是这样写的,这使得将现有的代码库迁移到TypeScript变得更加容易。

为了更好地理解,让我们先谈谈any类型。

大部分的时间,如果TypeScript不能找出一个变量的类型,它会选择尽可能灵活不困扰你的any类型。我们经常称之为隐式any类型(而不是一个明确的你会写出来的类型)。

let x;      // 隐式 'any'
let y = []; // 隐式 'any[]'

let z: any; // 显式 'any'.

从这一点上,你可以使用这些值做任何你想要做的事情。对许多人来说,这种行为太松散了,这就是为什么--noImplicitAny编译参数会在无法推断类型时发出警告。

在TypeScript 2.0中,我们构建了使用控制流分析来跟踪整个程序中类型流的基础。因为该分析检查每个变量的赋值,我们在TypeScript 2.1中利用了同样的基础,更深入地检查了每一个变量的类型,它看起来像是一个更好的类型。而不是只是选择any,TypeScript将根据你最终的赋值来推断类型。

让我们来看下面的例子。

let x;

// 我们仍然可以给'x'赋任何我们需要的值。
x = () => 42;

// 在刚才赋值后,TypeScript 2.1 知道'x'的类型是'() => number',
// 因此它可以被调用。
x();

// 但现在我们会得到一个错误,我们不能添加一个数字到函数!
console.log(x + 42);
//          ~~~~~~
// 错误!运算符 '+' 不能应用于类型`() => number`和'number'。

// TypeScript仍然允许你给'x'赋值你需要的任何值。
x = "Hello world!";

// 但现在它仍然知道'x'是'string'类型的!
x.toLowerCase();

当涉及到赋值,TypeScript仍然会相信你,并允许你给x赋值任何你需要的值。然而,对于任何其他的用途,the type checker will know better by climbing up and looking at whatever you’ve actually done with x.

现在对空数组也进行同样的跟踪。这意味着更好的完成:

let puppies = [];

puppies.push(new Puppy());

for (let pup of puppies) {
    pup.bark();
    //  ^^^^ Get completion on 'bark'
}

这也意味着TypeScript可以捕获更多的明显的错误:

puppies[1] = new Kitty();

for (let pup of puppies) {
    pup.bark();
    //  ~~~~ 错误:'bark'不存在'Kitty'类型中
}

所有这一切的最终结果是,将来你会看到较少隐式any错误,并得到更好的工具支持。

低版本异步函数

TypeScript 2.1将支持低版本异步函数(即async/await),并且现在你就可以在候选版本中使用它!async/await是ECMAScript 2017版本中的新特性,它允许用户在promise中编写代码,而不需要使用回调。async函数写出来的代码风格看起来像同步代码那样,但使用await关键字异步运行。

该特性在TypeScript 2.1之前就已经支持了,但是只能编译为ES6或者ES2015。TypeScript 2.1使其该特性可以在ES3和ES5运行时上使用,这意味着无论您使用什么环境,都可以使用它。

例如,让我们使用下面名为delay的函数,它返回一个promise并在完成之前等待一定的时间:

function delay(milliseconds: number) {
    return new Promise<void>(resolve => {
      setTimeout(resolve, milliseconds);
    });
}

让我们尝试一个简单的探索任务。我们要写一个程序,打印"Hello",三个点,接着是"World!"

function welcome() {
    console.log("Hello");

    for (let i = 0; i < 3; i++) {
        console.log(".");
    }

    console.log("World!");
}

原来是听起来很简单。

现在让我们假设我们要使用我们的delay函数在每个点之前暂停。

没有async / await,我们必须写如下:

function dramaticWelcome() {
    console.log("Hello");

    (function loop(i){
        if (i < 3) {
            delay(500).then(() => {
                console.log(".");
                loop(i + 1);
            });
        }
        else {
            console.log("World!");
        }
    })(0);
}

这看起来不那么简单了!如果我们尝试使用async函数来使这个代码更具可读性呢?

首先,我们需要确保我们的运行时具有全局可用的符合ECMAScript的Promise。这可能需要获取Promise的polyfill,或依赖于你指定版本运行时中其中的一个。我们还需要确保TypeScript知道Promise存在,通过将我们的lib编译参数设置为像“dom”,“es2015”或“dom”,“es2015.promise”,“es5”:

{
    "compilerOptions": {
        "lib": ["dom", "es2015.promise", "es5"]
    }
}

现在我们可以使用asyncawait来重写代码:

async function dramaticWelcome() {
    console.log("Hello");

    for (let i = 0; i < 3; i++) {
        await delay(500);
        console.log(".");
    }

    console.log("World!");
}

请注意,与我们的同步版本的代码相比,这是多么相似!尽管它看起来像同步的,但这个函数实际上是异步的,并且不会阻止其他代码在每次暂停之间运行。事实上,dramaticWelcome的两个版本从基本上归结为相同的代码,但是使用async&await,TypeScript为我们提供了很大的帮助。

下一步

TypeScript 2.1 RC有很多其他特性,我们将有更多的适用于2.1。你可以看看我们的路线图,看看在存储区还有哪些特性。我们希望你尝试一下并且享受它!

[译]ECMAScript 6中的参数使用

ECMAScript 6(又名ECMAScript2015)是ECMAScript标准的最新版本,在JavaScript中明显提升了参数的使用。现在我们可以使用rest参数、默认值和解构等其它特性。

在本教程中,我们将探讨实参和形参的详情,看看ECMAScript 6对其升级了哪些特性。

Arguments与Parameters

Arguments和parameters通常是相互使用。然而在本教程中我们将做一个区分。通常,parameters(形参)是在函数声明时指定的,而arguments(实参)是调用函数时传入的。思考下面这个函数:

function foo(param1, param2){
    // do something
}
foo(10, 20);

在这个函数中,param1param2是函数的形参,传入函数的值(1020)是实参。

扩展运算符(…)

在ECMAScript 5中,apply()方法传递一个数组作为函数的参数是方便的。例如,常用Math.max()方法去查找一个数组中的最大值,思考这段代码:

var myArray = [5, 10, 50];
Math.max(myArray); // Error: NaN
Math.max.apply(Math, myArray); //50

Math.max()方法不支持数组,它仅仅接受数字。当一个数组传入到Math.max()函数中时,它抛出一个错误。但是当使用apply()方法后,数组作为单独的数字传入,所以Math.max()方法可以处理它。

幸运的是,随着在ECMAScript 6扩展操作符的引入,我们不再需要使用apply()方法。使用扩展操作符,我们可以轻易地将一个表达式扩展为多个参数:

var myArray = [5, 10, 50];
Math.max(...myArray);  // 50

在这里,扩展运算符将myArray展开为函数的一个个单独的参数。在ECMAScript 5中使用apply()模拟扩展运算符是可行的,但是语法是混乱的,并且缺乏扩展运算符的灵活性。扩展运算符不但使用简单而且还具有特性。例如,在函数调用时,它可以多次使用并且可以和其它参数混合使用:

function myFunction() {
    for(var i in arguments){
        console.log(arguments[i]);
    }
}
var params = [10, 15];
myFunction(5, ...params, 20, ...[25]); // 5 10 15 20 25

扩展操作符的另一个优点是,它可以很容易地与构造函数一起使用:

new Date(...[2016, 5, 6]); // Mon Jun 06 2016 00:00:00 GMT-0700 (Pacific Daylight Time)

当然,我们可以在ECMAScript 5中重写上面的代码,但我们需要使用复杂的模式来避免类型错误:

new Date.apply(null, [2016, 4, 24]);    // TypeError: Date.apply is not a constructor
new (Function.prototype.bind.apply(Date, [null].concat([2016, 5, 6])));   // Mon Jun 06 2016 00:00:00 GMT-0700 (Pacific Daylight Time)

浏览器对扩展操作符的支持

桌面浏览器

CHROME FIREFOX INTERNET EXPLORER MICROSOFT EDGE OPERA SAFARI
46 27 Supported 7.1

移动浏览器

CHROME FOR ANDROID FIREFOX MOBILE IE MOBILE OPERA MOBILE SAFARI MOBILE
46 27

浏览器支持详情点我查看

Rest参数

rest参数的语法与扩展运算符一样,但是它不是展开一个数组到参数中,相反它是将参数收拢为一个数组:

function myFunction(...options){
    return options;
}
myFunction('a', 'b', 'c'); //["a", "b", "c"]

如果没有实参,rest参数将被设置为一个空数组:

function myFunction(...options){
    return options
}
myFunction(); // []

当创建一个参数可变的函数(即接受的参数数量是有变化的函数)时,rest参数是非常有用的。得益于数组的好处,reset参数可以容易地替换掉arguments对象(我们将在本教程后面解释)。思考这个使用ECMAScript 5写的函数:

function checkSubstring(string) {
    for(var i = 1; i < arguments.length; i++) {
        if(string.indexOf(arguments[i]) === -1) {
            return false;
        }
    }
    return true;
}
checkSubstring('this is a string', 'is', 'this'); // true

这个函数检查一个字符串是否包含一些子字符串。这个函数的第一个问题是,我们必须得看函数的内部才知道它需要多个参数。第二个问题是循环迭代必须从索引1而不是0开始,因为arguments[0]指向第一个参数。如果我们以后决定在string之前或者之后添加另一个参数,我们可能会忘记更新for循环的逻辑处理。使用rest参数,我们就很容易地避免这些问题:

function checkSubstrings(string, ...keys) {
  for (var key of keys) {
    if (string.indexOf(key) === -1) {
      return false;
    }
  }
  return true;
}
checkSubstrings('this is a string', 'is', 'this');   // true

此函数的输出和前一个函数相同。在这里,string参数是传入的第一个参数,其它的参数放进一个数组中,并赋值给了变量keys

使用rest参数代替arguments对象,提高了代码的可读性,避免了JavaScript中的优化问题。然而,rest参数也不是没有它的局限性。例如,它必须是最后一个参数,否则,将出现语法错误。

function logArguments(a, ...params, b) {
        console.log(a, params, b);
}
logArguments(5, 10, 15);    // SyntaxError: parameter after rest parameter

另一个限制是,在函数声明中只允许存在一个rest参数:

function logArguments(...param1, ...param2) {
}
logArguments(5, 10, 15);    // SyntaxError: parameter after rest parameter

浏览器对rest参数的支持

桌面浏览器

CHROME FIREFOX INTERNET EXPLORER MICROSOFT EDGE OPERA SAFARI
47 15 Supported 34

移动浏览器

CHROME FOR ANDROID FIREFOX MOBILE IE MOBILE OPERA MOBILE SAFARI MOBILE
47 15

浏览器支持详情点我查看

默认参数

ECMAScript 5中的默认参数

在ECMAScript 5中JavaScript不支持默认参数,但有一个简单的解决方法。在函数内使用逻辑or操作符(||),我们很容易在ECMAScript 5中模拟默认参数。思考这个函数:

function foo(param1, param2) {
   param1 = param1 || 10;
   param2 = param2 || 10;
   console.log(param1, param2);
}
foo(5, 5);  // 5 5
foo(5);    // 5 10
foo();    // 10 10

这个函数预计需要有两个参数,但是当它被不带参数调用时,它将使用默认值。在函数内部,缺少的参数会自动设置为undefined。因此,我们可以检测到这些参数,并为它们声明为默认值。检测缺失的参数和设置默认值,我们使用逻辑or操作符(||)。这个操作符检查表达式的第一个值:如果它是true,操作符返回它。如果它是假,操作符返回第二个值。

我们经常在函数中使用这种方式来设置参数的默认值,但是它是有缺陷的。向函数传入0null也会触发默认值,因为这些参数的值会被转换为false。因此,如果我们真的需要给函数传入0null,我们需要另一种方法来检查参数是否缺少:

function foo(param1, param2) {
  if(param1 === undefined){
    param1 = 10;
  }
  if(param2 === undefined){
    param2 = 10;
  }
  console.log(param1, param2);
}
foo(0, null);    // 0, null
foo();    // 10, 10

在这个函数中,对传入的参数进行类型检查,以确保在赋默认值之前,它们是undefined。这种方式需要多写一点的代码,但它是一个更安全的选择,并且允许我们向函数传入参数0null

ECMAScript 6中的默认参数

使用ECMAScript 6,我们不再需要检查undefined值来模拟默认参数了,现在我们可以将默认值放在函数声明中:

function foo(a = 10, b = 10) {
  console.log(a, b);
}
foo(5);    // 5 10
foo(0, null);    // 0 null

正如你所看到的,调用函数时,省略参数会触发默认值,但是传入0null并不会触发默认值。
我们甚至可以使用函数返回值作为参数的默认值:

function getParam() {
    alert("getParam was called");
    return 3;
}
function multiply(param1, param2 = getParam()) {
    return param1 * param2;
}
multiply(2, 5);     // 10
multiply(2);     // 6 (also displays an alert dialog)

注意,函数getParam仅仅在第二个参数缺省时才会被调用。因此当我们传入两个参数调用函数multiply()时,alert并不会显示。

默认参数另一个有趣的特性是,在函数声明中我们可以引用其他参数和变量:

function myFunction(a=10, b=a) {
     console.log('a = ' + a + '; b = '  + b);
}
myFunction();     // a=10; b=10
myFunction(22);    // a=22; b=22
myFunction(2, 4);    // a=2; b=4

你甚至可以在函数声明中执行操作:

function myFunction(a, b = ++a, c = a*b) {
     console.log(c);
}
myFunction(5);    // 36

注意,与其它语言不同,JavaScript是在调用函数时计算默认值。

function add(value, array = []) {
  array.push(value);
  return array;
}
add(5);    // [5]
add(6);    // [6], not [5, 6]

浏览器对默认参数的支持

桌面浏览器

特性 CHROME FIREFOX INTERNET EXPLORER MICROSOFT EDGE OPERA SAFARI
基本支持 49 15 14
在默认参数后,无默认值的参数 49 26 14

移动浏览器

特性 CHROME FOR ANDROID FIREFOX MOBILE IE MOBILE OPERA MOBILE SAFARI MOBILE
基本支持 49 15
在默认参数后,无默认值的参数 46 26

浏览器支持详情点我查看

解构

解构是ECMAScript 6中的新特性,解构能够使我们从数组和对象中提取值并且把这些值赋到一些变量上,这些变量的组成语法类似于数组和对象。这种语法是清晰明确和易于理解,并且当传递参数给函数时特别有用。

在ECMAScript 5中,配置对象经常被用来处理大量的可选参数,特别是当属性的顺序并不重要时。思考这个函数:

function initiateTransfer(options) {
    var  protocol = options.protocol,
        port = options.port,
        delay = options.delay,
        retries = options.retries,
        timeout = options.timeout,
        log = options.log;
    // code to initiate transfer
}
options = {
  protocol: 'http',
  port: 800,
  delay: 150,
  retries: 10,
  timeout: 500,
  log: true
};
initiateTransfer(options);

这种模式JavaScript的开发者经常使用,并且效果非常好,但是我们必须得看函数的内部才知道它需要哪些参数。使用解构参数,在函数声明时,我们可以清晰地表明需要哪些参数:

function initiateTransfer({protocol, port, delay, retries, timeout, log}) {
     // code to initiate transfer
};
var options = {
  protocol: 'http',
  port: 800,
  delay: 150,
  retries: 10,
  timeout: 500,
  log: true
}
initiateTransfer(options);

在这个函数中,我们使用了对象解构模式替代了配置对象。这使得我们的函数不仅更简洁,而且易读性更强。

我们可以结合使用解构参数和常规参数:

function initiateTransfer(param1, {protocol, port, delay, retries, timeout, log}) {
     // code to initiate transfer
}
initiateTransfer('some value', options);

注意,在参数调用时省略参数将会抛出类型错误:

function initiateTransfer({protocol, port, delay, retries, timeout, log}) {
     // code to initiate transfer
}
initiateTransfer();  // TypeError: Cannot match against 'undefined' or 'null'

当我们需要参数需要时,这是预期的行为,但如果我们希望他们是可选的呢?为了防止缺省参数时出现这个错误,我们需要给解构参数赋一个默认值:

function initiateTransfer({protocol, port, delay, retries, timeout, log} = {}) {
     // code to initiate transfer
}
initiateTransfer();    // no error

在这个函数中,一个空对象作为默认值赋值给了解构参数。现在,如果这个函数不带任何参数被调用,并不会报错。

我们也可以给每个解构参数赋默认值:

function initiateTransfer({
    protocol = 'http',
    port = 800,
    delay = 150,
    retries = 10,
    timeout = 500,
    log = true
}) {
     // code to initiate transfer
}

在这个示例中,每一个属性都有一个默认值参数,省去了需要我们手动检查未定义的参数和在函数内部赋默认值。

浏览器对默认解构的支持

桌面浏览器

CHROME FIREFOX INTERNET EXPLORER MICROSOFT EDGE OPERA SAFARI
基本支持 49 2.0 14 7.1
解构参数带有默认值 49 26 14

移动浏览器

特性 CHROME FOR ANDROID FIREFOX MOBILE IE MOBILE OPERA MOBILE SAFARI MOBILE
基本支持 49 1 8
解构参数带有默认值 46 47

浏览器支持详情点我查看

参数传递

有两种方法可以将参数传递给函数:通过引用或者值。修改引用参数具有全局的体现,但是修改值参数仅仅在函数内部有所体现。

在某些编程语言中,比如Visual Basic和Powershell,我们有指定通过引用还是值来传递参数的选项,但JavaScript并没有。

值参数

从技术上讲,JavaScript只能通过值传递。当我们传递一个值参数给函数时,该值会在函数的作用域内创建一个副本。因此,对值的任何更改只会在函数内部有所体现。思考这个示例:

var a = 5;
function increment(a) {
    a = ++a;
    console.log(a);
}
increment(a);   // 6
console.log(a);    // 5

在这里,修改函数内部的参数对原始值没有影响。因此,在函数外部变量被记录时,打印的值仍然是5

引用参数

在JavaScript中,一切都是通过值传递的,但是当我们传递一个对象(包含数组)变量时,“值”是对象的引用,更改变量引用对象的属性会更改底层对象。

思考这个函数:

function foo(param){
    param.bar = 'new value';
}
obj = {
    bar : 'value'
}
console.log(obj.bar);   // value
foo(obj);
console.log(obj.bar);   // new value

正如你所看到的,函数内部的对象属性被修改,但修改后的值在函数外部是可见的。

当我们传递一个非基本值时,如数组或对象,在背后就会创建一个变量并指向内存中的原始对象的位置。然后这个变量传递给函数,并且修改它就会影响到原始对象。

类型检查和缺省或额外的参数

在强类型语言中,我们必须在函数声明中指定参数的类型,但是JavaScript缺乏此特性。在JavaScript中,我们传递给函数的参数类型或参数数量都无关紧要。

假设我们有一个只接受一个参数的函数。当我们调用这个函数时,我们无法限制只能传入一个参数,我们可以传递一个,两个或更多参数!我们甚至可以不传入任何参数,并且函数不会报错。

实参和形参的数量有两种不同:

  • 实参的数量少于形参缺少的参数将等于undefined
  • 实参的数量大于形参额外的参数将被忽略,但是可以通过特殊数组比如变量arguments来检索(稍后讨论)。

必要参数

如果函数调用时缺省一个参数,那么这个参数将被设置为undefined。我们可以利用这种行为,如果省略参数抛出一个错误:

function foo(mandatory, optional) {
    if (mandatory === undefined) {
        throw new Error('Missing parameter: mandatory');
    }
}

在ECMAScript 6中,我们可以更进一步,使用默认参数来设置必要参数:

function throwError() {
    throw new Error('Missing parameter');
}
function foo(param1 = throwError(), param2 = throwError()) {
    // do something
}
foo(10, 20);    // ok
foo(10);   // Error: missing parameter

参数对象

rest参数被添加到ECMAScript 4用来替换arguments对象,但是ECMAScript 4没有实现。随着ECMAScript 6的发布,JavaScript现在正式支持rest参数。它还否决了删除arguments对象的计划。

arguments对象是一个数组对象,可以在所有函数中使用。它允许参数的值通过索引而不是名称传递给函数。该对象允许我们将任何数量的参数传递给函数。思考下面的代码:

function checkParams(param1) {
    console.log(param1);    // 2
    console.log(arguments[0], arguments[1]);    // 2 3
    console.log(param1 + arguments[0]);    // 2 + 2
}
checkParams(2, 3);

这个函数预计只接受一个参数。当我们使用两个参数调用它时,第一个参数通过参数名称param1或者参数对象arguments[0]在函数中是可以访问的,但是第二个参数只能通过arguments[1]来访问。此外注意,arguments对象可以和参数名称一起使用。

arguments对象包含传递给函数每个参数的入口,并第一个入口的索引从0开始。如果我们想在上面的示例中访问更多的参数,我们会写arguments[2]arguments[3]等。

我们甚至可以完全跳过设置命名参数,仅仅使用arguments对象:

function checkParams() {
    console.log(arguments[1], arguments[0], arguments[2]);
}
checkParams(2, 4, 6);  // 4 2 6

事实上,命名参数是为了方便,而不是必要的。类似地,rest参数也可以用来体现所传递的参数:

function checkParams(...params) {
    console.log(params[1], params[0], params[2]);    // 4 2 6
    console.log(arguments[1], arguments[0], arguments[2]);    // 4 2 6
}
checkParams(2, 4, 6);

arguments对象是一个数组对象,但是它缺少数组的方法,比如slice()foreach()。为了在arguments对象上使用数组的方法,该对象首先需要转换为一个真正的数组:

function sort() {
    var a = Array.prototype.slice.call(arguments);
    return a.sort();
}
sort(40, 20, 50, 30);    // [20, 30, 40, 50]

在这个函数中,Array.prototype.slice.call()作为一个快速方法将arguments对象转换为一个数组。接下来,sort()方法排序数组中的项并返回了它。

ECMAScript 6有一个更简单的方法。Array.from()是ECMAScript 6中新增的,它可以把一个数组对象创建成一个数组:

function sort() {
    var a = Array.from(arguments);
    return a.sort();
}
sort(40, 20, 50, 30);    // [20, 30, 40, 50]

length属性

虽然参数对象不是数组,但是它拥有length属性,这可以用来检查传入函数中的参数数量:

function countArguments() {
    console.log(arguments.length);
}
countArguments();    // 0
countArguments(10, null, "string");    // 3

通过使用length属性,我们可以更好的控制传递给函数的参数数量。例如,如果函数需要两个参数工作,我们可以使用length属性来检查传入的参数的数量,如果传入的参数数量比预期要少,则抛出错误:

function foo(param1, param2) {
    if (arguments.length < 2) {
        throw new Error("This function expects at least two arguments");
    } else if (arguments.length === 2) {
        // do something
    }
}

rest参数是数组,所以他们有一个length属性。在ECMAScript中6中,前面的代码可以使用rest参数这么来改写:

function foo(...params) {
  if (params.length < 2) {
        throw new Error("This function expects at least two arguments");
    } else if (params.length === 2) {
        // do something
    }
}

callee和caller属性

callee属性是指当前正在运行的函数,caller是指调用了当前函数的执行函数。在ECMAScript 5严格模式下,这些属性已被废弃,如果尝试访问这些属性会导致类型错误。

arguments.callee属性在递归函数(递归函数是指有规律的自己调用自己的函数)中是有用的,尤其是当函数名称不可用(即匿名函数)时。因为匿名函数没有名称,只能通过arguments.callee属性调用自己。

var result = (function(n) {
  if (n <= 1) {
    return 1;
  } else {
    return n * arguments.callee(n - 1);
  }
})(4);   // 24

严格模式和非严格模式中的参数对象

在ECMAScript 5非严格模式下,arguments对象有一个不同寻常特性:它可以与相应的命名参数的值保持同步。

思考下面的代码:

function foo(param) {
   console.log(param === arguments[0]);    // true
   arguments[0] = 500;
   console.log(param === arguments[0]);    // true
   return param
}
foo(200);    // 500

在这个函数中,一个新值赋给了arguments[0]。因为arguments的值始终和相应的命名参数保持同步,改变arguments[0]的值也将改变param的值。事实上,他们像两个不同名称的相同变量。在ECMAScript 5严格模式下,这个令人困惑的arguments对象的行为已被删除:

"use strict";
function foo(param) {
   console.log(param === arguments[0]);    // true
   arguments[0] = 500;
   console.log(param === arguments[0]);    // false
   return param
}
foo(200);   // 200

此时,改变arguments[0]并不会影响param,并且输出结果和预期的一样。该函数的输出结果在ECMAScript 6中和ECMAScript 5严格模式下是相同的,但是记住,当函数声明中使用默认值时,arguments对象并不受影响:

function foo(param1, param2 = 10, param3 = 20) {
   console.log(param1 === arguments[0]);    // true
   console.log(param2 === arguments[1]);    // true
   console.log(param3 === arguments[2]);    // false
   console.log(arguments[2]);    // undefined
   console.log(param3);    // 20
}
foo('string1', 'string2');

在这个函数中,即使param3有默认值,它也和arguments[2]不相等,因为只有两个参数传递给了函数。换句话说,设置默认值并不影响arguments对象。

总结

ECMAScript 6给JavaScript带来了大大小数以百计的的改进。越来越多的开发者使用ECMAScript 6的特性,很快这些特性不可避免会被使用到。在本教程中,我们已经学会了ECMAScript 6是如何升级JavaScript中的参数应用,但我们仅只是触及了ECMAScript 6的皮毛。语言中其它新的和有趣的特性是值得一看。

链接

(rb, ml, al, il)

首页图片版权:JavaScript Planet

TypeScript已经完全支持以上特性,并且你不用担心兼容性问题。详情点击TypeScript中文网查看。