现代JavaScript正在快速发展,以应对新框架和环境不断的变化。了解如何利用这些更改可以节省您的时间,提高您的技能,写出优秀的代码。

学习现代JavaScript可以帮助您什么时候使用新技术,什么时候使用旧技术。

必知必会

不管你是JavaScript新手还是老手,太多的人对JavaScript的发展现状都会感到困惑。太多的新框架,太多的语言变化,太多的上下文需要考虑。每个人都能完成工作,每个月都能学到新东西,这真是个奇迹。

我相信任何编程语言的成功秘诀,不管应用程序有多复杂,都是回归基础。如果您想了解Rails,可以从您的Ruby技能开始,如果您想在React同构应用中使用immutables单向数据流,首先从JavaScript核心知识开始。

理解语言的工作原理要比熟悉最新的框架和工具实际得多。这些变化比天气变化还快。问题是,JavaScript最新版本中出现的一些新语法导致部分旧语法过时了。但不是全是这样!有时,为了实现相同的功能,新语法可能会取代更笨拙的语法。其他时候,这种新方法只是一种更简单的替代方法,但是有一些细微的差别,了解这些差别是很重要的。

语法糖

近年来JavaScript中的许多变化都被描述为语法糖。在许多情况下,语法糖可以帮助学习如何使用JavaScript的开发人员,提供一个更干净、更简单的语法来完成我们想做的事。

如果想使用新语法来代替旧语法,在对新语法不是特别熟悉的情况下,会面临以下风险:

  • 必须调试以前的代码
  • 在运行时捕获您的错误
  • 创建在您最不期望的时候悄无声息地失败的代码。


事实上,一些看起来是现有技术的临时替代的更改实际上与它们应该替换的代码有不同的行为。在许多情况下,使用原始的、旧的风格来完成您想要做的事情会更有意义。识别何时发生这种情况,并知道如何做出选择,对于编写正确的现代JavaScript非常重要。

const 常量

ES6引入了两个新的关键字: let和const,用于代替使用var声明变量,在大多数情况下是没问题的,但如果我们深入研究后会发现与var还是有所区别。

在ES5中,在使用变量之前使用var关键字声明变量一直业界推荐的做法。但是下面的写法也是没问题的:

console.log(usingVar); // undefined
var usingVar = "defined";
console.log(usingVar); // "defined"

在默认情况下,带有var的变量声明总是被提升到其包含范围的顶部,而不管它们位于该范围的什么位置。这意味着即使是深嵌套的变量也可以被认为是声明的,并且可以从其包含范围的开始就可用。

由于多个脚本可能同时被一个web页面加载,这意味着在一个脚本中声明的变量有可能泄漏到另一个脚本中。

let和const与var不同:

console.log(usingLet); // error
let usingLet = "defined"; // never gets executed
console.log(usingLet); // never gets executed

当您使用let或const声明一个变量时,该变量的作用域仅限于声明它的代码块。

JavaScript中的代码块由一组花括号{}来区分,例如函数体或循环。

前面说到使用var声明的变量是存在于当前上下文的,当有多个循环体时,必须必须确保两个变量名不能相同,而let没有这个限制:

for (var count = 0; count < 5; count++) {
  console.log(count);
}
console.log(count); // 5


for (let otherCount = 0; otherCount < 5; otherCount++) {
  console.log(otherCount);
}
console.log(otherCount); // error, otherCount is undefined

我们来看另一个关键字:const 代表是一个常量; 但有时却与我们想的不一样。

与var或let变量不同,const声明时必须赋值:

var x; // 合法
let y; //合法
const z; // 非法

如果您在声明const之后将其设置为一个新值,那么会抛出一个错误:

const z = 3; // 合法
z = 4; // 非法

如果你认为const常量在任何情况下都是不变的,你可能会失望:

const z = []; // 合法
z.push(1); // 合法, z = [1]
z = [2] // 非法

当const变量是数组或对象类型时,它是允许改变的。

我倾向于使用const来初始化简单的数字或字符串变量,或者用于命名函数和类,我希望定义一次,然后关闭它们进行修改。其它情况,我将使用let声明变量—特别是那些我希望限制变量的作用域。如果我想要一个变量来打破作用域并提升到脚本的顶部,我就会使用var。

函数作用域

使用function定义函数:

function doSomething(param) {
  return(`Did it: ${param}`);
}
console.log(doSomething("Hello")); // "Did it: Hello"

它们还可以与new关键字一起使用来构造具有原型继承的对象,对象定义可以放置在作用域中的任何位置:

function Animal(name) {
  this.name = name;
}
let cat = new Animal("Fluffy");
console.log(`My cat's name is ${cat.name}.`); // "My cat's name is Fluffy."

两种形式的函数可以定义在调用之前或之后:

console.log(doSomething("Hello")); // "Did it: Hello"

let cat = new Animal("Fluffy");
console.log(`My cat's name is ${cat.name}.`); // "My cat's name is Fluffy."

function doSomething(param) {
  return(`Did it: ${param}`);
}
function Animal(name) {
  this.name = name;
}

传统函数还创建自己的上下文 this。它所定义的任何语句或子函数都在执行,并且允许我们在调用函数时自动传入this。

实际编程中这个this对程序员非常有用。因此,现代JavaScript将传统的函数分为箭头函数和类。

类函数

在JavaScript中进行面向对象编程时,函数和类之间的一个区别是JavaScript中的类需要前置声明。

也就是说,在使用new关键字实例化类之前,需要在提前声明该类。使用function关键字的原型继承在JavaScript中工作,即使它是在脚本稍后定义的。

与类不同,函数声明是自动提升到顶部的
// Using a function to declare and instantiate an object (hoisted)
let aProto = new Proto("Myra");
aProto.greet(); // "Hi Myra"

function Proto(name) {
  this.name = name;
  this.greet = function() {
    console.log(`Hi ${this.name}`);
  };
};

// Using a class to declare and instantiate an object (not hoisted)
class Classy {
  constructor(name) {
    this.name = name;
  }
  greet() {
    console.log(`Hi ${this.name}`);
  }
};

let aClassy = new Classy("Sonja");
aClassy.greet(); // "Hi Sonja"

箭头函数的特性

箭头函数,这是一种ES6新语法,允许您更简洁地编写函数:

const traditional = function(data) {
  return (`${data} from a traditional function`);
}
const arrow = data => `${data} from an arrow function`;

console.log(traditional("Hi")); // "Hi from a traditional function"
console.log(arrow("Hi"));  // "Hi from an arrow function"

箭头函数封装了一些特性,这些特性可以使调用它们更加方便,并且省略了调用函数时无关的细节。

例如,箭头函数从调用它的上下文中继承了这个参数和参数。这对于事件处理或setTimeout这样的情况非常有用,因为程序员经常希望调用的行为应用于请求它的上下文。传统函数迫使程序员编写复杂的代码,通过使用.bind(this)将函数绑定到现有的函数。

class GreeterTraditional {
  constructor() {
    this.name = "Joe";
  }
  greet() {
    setTimeout(function () {
      console.log(`Hello ${this.name}`);
    }, 1000); // inner function has its own this with no name
  }
}
let greeterTraditional = new GreeterTraditional();
greeterTraditional.greet(); // "Hello "

class GreeterBound {
  constructor() {
    this.name = "Steven";
  }
  greet() {
    setTimeout(function () {
      console.log(`Hello ${this.name}`);
    }.bind(this), 1000); // passing this from the outside context
  }
}
let greeterBound = new GreeterBound(); // "Hello Steven"
greeterBound.greet();

class GreeterArrow {
  constructor() {
    this.name = "Ravi";
  }
  greet() {
    setTimeout(() => {
      console.log(`Hello ${this.name}`);
    }, 1000); // arrow function inherits this by default
  }
}
let greeterArrow = new GreeterArrow();
greeterArrow.greet(); // "Hello Ravi"