JavaScript 修饰符是什么及何时使用它们

栏目: JavaScript · 发布时间: 8年前

内容简介:JavaScript 修饰符是什么及何时使用它们

JavaScript 修饰符是什么及何时使用它们

随着 ES2015+ 的到来,转译已经司空见惯, 人们会在代码或者教程中看到各种新的语言特性。这些特性中有一个经常让人家挠头的特性,就是 JavaScript 的修饰符。

修饰符随 Angular 2+ 变得流行起来。在 Angular 中,是 TypeScript 带来了修饰符,不过修饰符会在今年晚些时间更新为 ES2017 的一部分。我们来看看修饰符是什么,以及如何使用它们可以让代码变得更容易理解。

注意:如果你比较喜欢视频,为什么不加入 SitePoint Premium 查看一些我们 流行的 JavaScript 课程 呢?

什么是修饰符?

用最简单的形式来说,修饰符是用一段代码包装另一段代码的方式 —— 字面上的“修饰”。

这个概念你以前可能听说过,就是“功能组合”,或者“高阶函数”。

对于许多用例来说,这种情况已经在标准 JavaScript 中实现了,只需要调用一个包装着另一个函数的函数即可:

function doSomething(name) {
  console.log('Hello, ' + name);
}

function loggingDecorator(wrapped) {
  return function() {
    console.log('Starting');
    const result = wrapped.apply(this, arguments);
    console.log('Finished');
    return result;
  }
}

const wrapped = loggingDecorator(doSomething);

这个示例产生了一些新的函数 —— 由变量 wrapped 引用 —— 可以像调用  doSomething 一样来调用  wrapped,而且它也会做相同的事情。不同之处在于 wrapped 会在被包装的函数调用之前和之后记录一些日志。

doSomething('Graham');
// Hello, Graham
wrapped('Graham');
// Starting
// Hello, Graham
// Finished

怎么使用 JavaScript 修饰符?

修饰符使用一个在 ES2017 中定义的特殊语法,在被修饰的代码前放置一个 开头的符号。

注意:在写这篇文章的时候,修饰符已经处于“ 第 2 阶段草案 ”,这表示它们肯定会被最终完成,但目前仍然有可能发生变化。

你可以根据需要对同一段代码使用多个修饰符,它们会按你声明的顺序来应用。

比如:

@log()
@immutable()
class Example {
  @time('demo')
  doSomething() {

  }
}

这段代码定义了一个类并使用了三个修饰符:两个用在类上,还有一个用在类的属性上:

  • @log 会记录对类进行的所有访问。
  • @immutable 让类成为不可变类 —— 它可能是在新实例上调用了  Object.freeze
  • @time 记录一个方法要执行多少时间,将将之用一个唯一的标记记录下来。

目前,使用修饰符需要转译器的支持,因为浏览器和 Node 都还不支持这一特性。如果你使用 Babel, transform-decorators-legacy 会让这一切变得容易。

注意:插件中使用“legacy(遗产)” 这个词是因为它以 Babel5 的方式处理修饰符,这一行为跟最终形成的标准有可能不同。

为什么使用修饰符?

JavaScript 中已经可以实现功能组合,但明显比较困难 —— 甚至不可能 —— 把相同的技术应用到其它代码上(比如类和类属性)。

ES2017 草案添加了支持类和属性的修饰符,它可以用来解决这些问题,将来的 JavaScript 版本可能会允许在其它棘手的代码区域添加修饰符。

Decorators also allow for a cleaner syntax for applying these wrappers around your code, resulting in something that detracts less from the actual intention of what you are writing.

修饰符的不同类型

目前,唯一支持的修饰类型是用在类和类成员上的,包括属性、方法、getters 和 setters。

修饰符只不过是返回另一个函数的函数,这被称为被修饰项适当的细节。这些修饰符函数会在程序首次运行时被执行一次,而其返回值会替代被修饰的代码。

类成员修饰符

属性修饰符用于类的某个成员 —— 不管它们是属性、方法、getter 还是 setter。

这类修改符函数调用时会传入三个参数:

  • target(目标) – 成员所在的类。
  • name(名称)  – 类成员的名称。
  • descriptor(描述符) – 成员描述符。本质上是传入 Object.defineProperty 的对象。

典型的应用示例是 @readonly。现在起来很简单:

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

只需要通过设置属性描述符的 “writable” 标记为 false。

然后在类属性上像这样使用:

class Example {
  a() {}
  @readonly
  b() {}
}

const e = new Example();
e.a = 1;
e.b = 2;
// TypeError: Cannot assign to read only property 'b' of object '#<Example>'

然而我们再做得好一些。我们可以用不同的行为来改变修饰功能。比如,记录所有输入和输出:

function log(target, name, descriptor) {
  const original = descriptor.value;
  if (typeof original === 'function') {
    descriptor.value = function(...args) {
      console.log(`Arguments: ${args}`);
      try {
        const result = original.apply(this, args);
        console.log(`Result: ${result}`);
        return result;
      } catch (e) {
        console.log(`Error: ${e}`);
        throw e;
      }
    }
  }
  return descriptor;
}

这段代码使用新的方法取代了原来的方法,新方法会记录参数,调用原来的方法,然后记录原有方法的输出。

注意我们使用了 展开运算符 来自动从传入的所有参数构建数组,这种新语法用来代替以前的  arguments 值。

用法如下:

class Example {
    @log
    sum(a, b) {
        return a + b;
    }
}

const e = new Example();
e.sum(1, 2);
// Arguments: 1,2
// Result: 3

你会发现我们不得不使用一个有点好笑的语法执行被修饰的方法。这种方法在整篇文章都有使用。不过总的来说, apply 函数能让你调用函数,指定  this 以及调用函数时传入的参数。

我们来进行一点改进,让修饰符处理一些参数。比如你下面这样重写 log 修饰符:

function log(name) {
  return function decorator(t, n, descriptor) {
    const original = descriptor.value;
    if (typeof original === 'function') {
      descriptor.value = function(...args) {
        console.log(`Arguments for ${name}: ${args}`);
        try {
          const result = original.apply(this, args);
          console.log(`Result from ${name}: ${result}`);
          return result;
        } catch (e) {
          console.log(`Error from ${name}: ${e}`);
          throw e;
        }
      }
    }
    return descriptor;
  };
}

现在它变得更复杂了,但是拆解开来就容易解理了:

  • 一个函数, log,需要一个参数name
  • 这个函数返回另一个 本身是修饰符 的函数。

这与之前的 log 修饰符相同,除了它要使用来自外部函数的  name 参数。

像下面这样使用:

class Example {
  @log('some tag')
  sum(a, b) {
    return a + b;
  }
}

const e = new Example();
e.sum(1, 2);
// Arguments for some tag: 1,2
// Result from some tag: 3

马上我们可以看到,我们提供的标签让我们可以在日志中很容易识别需要的内容。

这是因为 log('some tag') 调用时,JavaScript 运行时立即对其进行运算,然后从 sum 方法的修饰符得到响应。

类修饰符

类修饰符应用于整个类类型。这类修饰符调用时会传入一个参数,即被修饰的构造函数。

注意,它只应用于构造函数,而不会应用于创建出来的每个实例。也就是说,如果你想操作实例,就必须返回构造函数的包装版本。

一般来说,它不如类成员的修饰符有用,因为你在这里做的所有事情都要吧通过一个简单的方法调用来实现。你用它们做的任何事情,最终都需要返回一个新的构造函数来代替原来的构造函数。

回到我们记录日志的示例,我们来写一个修饰符记录构造函数的参数:

function log(Class) {
  return (...args) => {
    console.log(args);
    return new Class(...args);
  };
}

这里我们接受一个类作为参数,然后返回一个新的函数来充当构造函数。它只是单纯地记录参数和构造函数通过这些参数构造出来的新实例。

比如:

@log
class Example {
  constructor(name, age) {
  }
}

const e = new Example('Graham', 34);
// [ 'Graham', 34 ]
console.log(e);
// Example {}

我们可以看到构造 Example 类实例时会记录提供给它的参数,而且构造出来的值确实是 Example 的实例。正是我们想要的。

向类修饰符传递参数与向类成员修饰符传递参数一样:

function log(name) {
  return function decorator(Class) {
    return (...args) => {
      console.log(`Arguments for ${name}: args`);
      return new Class(...args);
    };
  }
}

@log('Demo')
class Example {
  constructor(name, age) {}
}

const e = new Example('Graham', 34);
// Arguments for Demo: args
console.log(e);
// Example {}

实际案例

Core 修饰符

有一个神奇的库,称为 Core Decorators ,它提供一些平常有用的修饰符。

这些修饰符使用简洁的语法,提供了非常有用的通用功能(比如,调用方法的时候,否决警告,允许某个值只读等)。

React

React 库很好的利用了高阶组件(Higher-Order Components)的概念。React 组件可以简单的写成函数,而它可以包含另一个组件。

欢迎购买我们的高级教程: 以 ES6 的方式使用 React

使用修饰符是理想的选择,因为改变成这样不需要大动作。比如,Redux 库有一个函数,connect,用于连接 React 组件和 Redux 存储。

一般会这样使用:

class MyReactComponent extends React.Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

然而,有了修饰符语法之后,就可以这样用了:

@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}

这与之前的功能完全相同。

MobX

MobX 库广泛使用了修饰符,让你很容易把字段标记为 Observable(可观察对象) 或 Computed(计算属性),以及把类变成 Observers(观察者)。

总结

类成员修饰符提供了一种极好的方式来包装类中的代码,这种方式与平时写独立函数的方式类似。它是一种编写简单辅助代码方式,可以简单地应用于各个地方,清晰,而且易于理解。

这种功能应用广泛,其用途仅限于你的想象!


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Impractical Python Projects

Impractical Python Projects

Lee Vaughan / No Starch Press / 2018-11 / USD 29.95

Impractical Python Projects picks up where the complete beginner books leave off, expanding on existing concepts and introducing new tools that you’ll use every day. And to keep things interesting, ea......一起来看看 《Impractical Python Projects》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具