为什么在 JavaScript 中使用 getter 和 setter 是一个坏主意

如你所知,getter和setter已经成为了JavaScript的一部分。它们广泛支持所有的主流浏览器,甚至是IE8。

我不认为这个点子通常是错误的,但我认为它不是非常适合JavaScript。可能看起来getter和setter可以简化代码和节省时间,但其实它们会带来隐藏错误,并且这些错误第一眼看并不明显。

getter和setter如何工作?

首先小小地总结一下这些是什么东西:

有时候,我们希望能允许访问一个会返回动态计算值的属性,或者你可能想要反映内部变量的状态,而不使用显式的方法调用。

为了说明它们是如何工作的,让我们来看一个有着两个属性的person对象,这两个属性为:firstName和lastName,以及一个计算值:fullName。

var obj = {
  firstName: "Maks",
  lastName: "Nemisj"
}

计算值fullName会返回firstName和lastName两者的串联。

Object.defineProperty(person, 'fullName', {
  get: function () {
    return this.firstName + ' ' + this.lastName;
  }
});

为了得到fullName的计算值,不需要像person.fullName()带可怕的括号,只需要使用简单的var fullName = person.fullName。

这同样适用于setter,你可以通过使用函数设置值:

Object.defineProperty(person, 'fullName', {
  set: function (value) {
    var names = value.split(' ');
    this.firstName = names[0];
    this.lastName = names[1];
  }
});

使用就和getter一样简单:person.fullName = ‘Boris Gorbachev’。这将调用上面定义的函数,并分离Boris Gorbachev成firstName和lastName。

问题在哪里?

你也许在想:“嘿,我喜欢getter和setter方法,它们感觉更自然,就像JSON一样。”你说得对,它们的确是这样的,但是我们先退一步来看一看fullName在getter和setter之前是如何工作的。

为得到值,我们将使用类似于getFullName()的一些东西,以及为了设置值,我们将使用person.setFullName(‘Maks Nemisj’)。

如果拼错函数名,person.getFullName()写成person.getFulName()会发生什么呢?

JavaScript会给出一个错误:

person.getFulName();
       ^
TypeError: undefined is not a function

这个错误会在适当的时候适当的地方被触发。访问函数不存在的对象将触发错误——这是好的。

现在,让我们来看看当用错误的名称来使用setter的时候会发生什么?

person.fulName = 'Boris Gorbachev';

什么也没有。对象是可扩展的,可以动态分配键和值,因此不会有错误在运行时被抛出。

这样的行为意味着错误可能显示在用户界面上的某个地方,或者,当某些操作被执行在错误的值上时,而并非是打字错误的时刻。

跟踪应该发生在过去但却显示在将来的代码流上的错误是如此有意思。

seal行不行

这个问题可以通过sealAPI来部分解决。只要对象是密封的,它就不能突变,也就是意味着fulName将试图分配一个新键到person对象,并且它会失败。

出于某种原因,当我在Node.js V4.0测试这个的时候,它没有按照我期待的那样工作。所以,我不能确保这个解决方案。

而更令人沮丧的是,对于setter一点也没有解决方法。正如我前面提到的,对象是可扩展和可故障保护的,这意味着访问一个不存在的键不会导致任何错误。

如果这种情况只适用于对象的文字的话,我不会多此一举地写这篇文章,但在ECMAScript 2015(ES6)和用类定义getter和setter能力的兴起之后,我决定写下关于潜在陷阱的博客。

类的到来

我知道当前类在一些JavaScript社区不是非常受欢迎。人们对在函数式/基于原型的语言,例如JavaScript中是否需要它们,争执不休。然而,事实是,类就在ECMAScript 2015(ES6)规范说明中,并且将存在于此一段时间。

对我来说,类是指定在类的外部世界(消费者)和应用程序的内部世界之间的定义良好的API的一种方式。这就是白纸黑字放入规则的抽象,并且我们假定这些规则不会很快改变。

改进person对象,做一个它的real类。person定义了接口用于获取和设置fullName。

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  getFullName() {
    return this.firstName + ' ' + this.lastName;
  }
  setFullName(value) {
    var names = value.split(' ');
    this.firstName = names[0];
    this.lastName = names[1];
  }
}

类定义了一个严格的接口描述,但getter和setter方法使其变得不太严格。我们已经习惯了臃肿的错误,当工作于对象文字和JSON时的键中出现拼写错误的时候。我希望至少类能够更严格,并且在这个意义上,提供更好的反馈给开发人员。

虽然这种情况在定义getter和setter在一个类上的时候没有任何不同。但它不会阻止任何人拼错。

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  get fullName() {
    return this.firstName + ' ' + this.lastName;
  }
  set fullName(value) {
    var names = value.split(' ');
    this.firstName = names[0];
    this.lastName = names[1];
  }
}

有拼写错误的执行不会给出任何错误:

var person = new Person('Maks', 'Nemisj');
console.log(person.fulName);

同样不严格,不冗长,不可追踪的行为导致可能会出错。

在我发现这一点后,我有一个问题:在使用getter和setter的时候,有没有什么可以做的,以便于使得类更严格?我发现:有是肯定有,但是这值得吗?增加额外层次的复杂性到代码就只是为了使用数量更少的括号?对于API定义,也可以不使用getter和setter,而这样一来就能解决这个问题。除非你是一个铁杆开发人员,并愿意继续进行,不然还有另一种解决方案,如下所述。

proxy来帮助?

除了getter和setter方法,ECMAScript 2015(ES6)还自带proxy对象。proxy可以帮助你确定委托方法,这些委托方法可以在实际访问键执行之前,用来执行各种操作。事实上,它看起来像动态getter / setter方法。

proxy对象可以用来捕捉任何到类的实例的访问,并且如果在类中没有找到预先定义的getter或setter就会抛出错误。

为了做到这一点,必须执行下面两个操作:

  • 创建基于Person原型的getter和setter清单。
  • 创建将测试这些清单的Proxy对象。

让我们来实现它。

首先,为了找出什么样的getter和setter方法可以用在类Person上,可以使用getOwnPropertyNames和getOwnPropertyDescriptor:

var names = Object.getOwnPropertyNames(Person.prototype);
var getters = names.filter((name) => {
  var result =  Object.getOwnPropertyDescriptor(Person.prototype, name);
  return !!result.get;
});
var setters = names.filter((name) => {
  var result =  Object.getOwnPropertyDescriptor(Person.prototype, name);
  return !!result.set;
});

在此之后,创建一个Proxy对象:

var handler = {
  get(target, name) {
    if (getters.indexOf(name) != -1) {
      return target[name];
    }
    throw new Error('Getter "' + name + '" not found in "Person"');
  },
  set(target, name) {
    if (setters.indexOf(name) != -1) {
      return target[name];
    }
    throw new Error('Setter "' + name + '" not found in "Person"');
  }
};
person = new Proxy(person, handler);

现在,只要你尝试访问person.fulName,就会显示Error: Getter “fulName” not found in “Person”的消息。

希望这篇文章可以帮助你全面了解getter和setter方法,以及它们将会带到代码中的危险。 来源:码农网

基于JavaScript的函数式编程概念

Functional Programming Jargon:函数式编程术语解释

本文的主要目的即是希望能够有一种通俗易懂的方式来阐述函数式编程中常见的理论术语概念

Arity:参数数目

Arity代指一个函数的参数数量,该关键字来源于类似于unary、binary、ternary等等,由两个后缀-ary、-ity组成。譬如,如果一个函数允许输入两个参数,那就称为所谓的binary function(二元函数),或者一个有两个参数的函数。有时候这种函数也会被喜欢拉丁语法的人称为”dyadic(二价的)”函数。以此类推,不定参数的方程也就被称为variadic(可变参数函数)。

const sum = (a, b) => a + b;

const arity = sum.length;
console.log(arity);
// => 2
// The arity of sum is 2

Higher-Order Functions (HOF):高等函数

一个接收某个函数作为参数的函数成为高等函数,该函数可以选择返回一个函数也可以返回其他类型

const filter = (pred, xs) => {
  const result = [];
  for (var idx = 0; idx < xs.length; idx += 1) {
    if (pred(xs[idx])) {
      result.push(xs[idx]);
    }
  }
  return result;
};
const is = type => x => Object(x) instanceof type;
filter(is(Number), [0, '1', 2, null]); //=> [0, 2]

Partial Application:局部封装

将原本一个多参数值的函数封装为固定参数数目的函数的过程称为Partial Application

let sum = (a, b) => a + b;

// partially applying `a` to `40`
let partial = sum.bind(null, 40);

// Invoking it with `b`
partial(2); //=> 42

Currying

将一个N参数值的函数转化为N个一元函数的组合,Currying与Partial Application的区别在于Partial Application最终生成的函数允许接收多个值,而Currying生成的函数序列中的每个函数只允许接收一个参数

let sum = (a, b) => a + b;

let curriedSum = (a) => (b) => a + b;

curriedSum(40)(2) // 42.

Composition:组合

感觉有点像设计模式里的Decorator,即能够将两个指定类型组合转化为一个新值的函数

最常见的组合即是常见的函数组合,允许你将不同的函数组合成一个返回单值的函数

const compose = (f, g) => a => f(g(a)) // Definition
const floorAndToString = compose((val)=> val.toString(), Math.floor) //Usage
floorAndToString(121.212121) // "121"

Purity:纯函数

一个没有任何副作用,并且返回值只由输入决定的函数成为纯函数

let greet = "yo";

greet.toUpperCase(); // YO;

greet // yo;

As opposed to:

let numbers = [1, 2, 3];

numbers.splice(0); // [1, 2, 3]

numbers // []

Side effects:副作用

如果一个函数,除了返回值之外,还会修改某些其它状态,或者与外部函数等有可观测的交互

console.log("IO is a side effect!");

Idempotency:幂等性

多次执行下都不会产生副作用的函数被称为具有幂等性的函数

f(f(x)) = f(x)

Math.abs(Math.abs(10))

Point-Free Style

那些并没有线性定义参数的函数风格被称为Point-Free Style,这类型往往需要currying 或者 Higher-Order functions。

// Given
let map = fn => list => list.map(fn);
let add = (a, b) => a + b;

// Then

// Not points-free - `numbers` is an explicit parameter
let incrementAll = (numbers) => map(add(1))(numbers);

// Points-free - The list is an implicit parameter
let incrementAll2 = map(add(1));

incrementAll明确规定了参数numbers, 而incrementAll2是对于参数的封装,并没有显性说明numbers参数,因此它可以被称为Points Free。一般来说,Points-free的函数都不会用常见的function或者=>关键字来定义。


Contracts

暂无

Guarded Functions

暂无

Categories:分类

关联到遵循某些规则的函数的对象,譬如monoid


Value:值

计算中常用到的一些复合值(complex)或者简单值(primitive),包括函数。一般来说,函数式编程中的值都被认为是不可变值。

5
Object.freeze({name: 'John', age: 30}) // The `freeze` function enforces immutability.
(a) => a

注意,譬如Functor, Monad这样包含其他值的结构体本身也是值,这就是说,这些复合值也可以相互包含。

Constant:常量

对于一个值的不可变引用,不能跟变量相混淆。Variable即指那些可能在任意点呗更改的引用。

const five = 5
const john = {name: 'John', age: 30}

常量一般认为是透明的,也就是说,它们可以被值本身代替而不影响最终的计算结果,上面的两个常量也可以用下述方式表述:

john.age + five === ({name: 'John', age: 30}).age + (5)

上述表达式会一直返回真。


Functor

Functor即指那些可以引用map函数的对象,JavaScript中最简单的函数就是Array。

[2,3,4].map( n => n * 2 ); // [4,6,8]

假设func构造为一个实现了map函数的对象,f、g则是任意的函数,只要func遵循以下规则就可以将func称为一个functor:
Let func be an object implementing a map function, and f, g be arbitrary functions, then func is said to be a functor if the map function adheres to the following rules:

func.map(x => x) == func

以及

func.map(x => f(g(x))) == func.map(g).map(f)

我们将Array称为Functor,也是因为它遵循了以下规则:

[1, 2, 3].map(x => x); // = [1, 2, 3]

以及

let f = x => x + 1;
let g = x => x * 2;

[1, 2, 3].map(x => f(g(x))); // = [3, 5, 7]
[1, 2, 3].map(g).map(f);     // = [3, 5, 7]

Pointed Functor

实现了of方法的Functor,Of会将任何单值转化为一个Functor

Pointed Functor在Array中的实现为:

  Array.prototype.of = (v) => [v];
  
  [].of(1) // [1]

Lift

Lift很类似于map,不过它可以用于多个Functors:

在单值函数下,Map与Lift的作用是一致的:

lift(n => n * 2)([2,3,4]); // [4,6,8]

而Lift可以允许输入多个值:

lift((a, b) => a * b)([1, 2], [3]); // [3, 6]

Referential Transparency:透明引用

一个可以直接用其值来替换而不会影响到程序表现的表达式称为透明引用

譬如我们有一个叫greet的引用

let greet = () => "Hello World!";

任何对于greet()的调用都可以被Hello World!直接替换,因此可以将greet称为透明引用。

Equational Reasoning

当一个应用由表达式组合而成并且没有任何副作用的时候,该系统可以由部分推导而来


Lazy evaluation:懒计算

Lazy evaluation 即是所谓的只有在需要某个值的时候才进行计算的机制。在函数式语言中,这个机制就允许对于那些近乎无限的列表进行操作。

let rand = function*() {
    while(1<2) {
        yield Math.random();
    }
}
let randIter = rand();
randIter.next(); // Each exectuion gives a random value, expression is evaluated on need.

Monoid:独异点

一个monoid就是与某个恒等值进行组合之后不会影响现有结果的数据类型

一个最简单的Monoid就是如下所示:

1 + 1; // 2

数据类型是number,函数是+:

1 + 0; // 1

恒等式的值是0,将0与任何数相加并不会改变值。有时候,monoid类型进行不同的交换操作也不会影响结果:

1 + (2 + 3) == (1 + 2) + 3; // true

数组连接也可以认为是一个monoid:

[1, 2].concat([3, 4]); // [1, 2, 3, 4]

恒等值即是空数组: []

[1, 2].concat([]); // [1, 2]

Monad

一个Monad就是拥有of以及chain函数的对象。 Chain 类似于 map只不过它会扁平化最终求得的嵌套式结果。

['cat,dog','fish,bird'].chain(a => a.split(',')) // ['cat','dog','fish','bird']

//Contrast to map
['cat,dog','fish,bird'].map(a => a.split(',')) // [['cat','dog'], ['fish','bird']]

You may also see of and chain referred to as return and bind (not be confused with the JS keyword/function…) in languages which provide Monad-like constructs as part of their standard library (e.g. Haskell, F#), on Wikipedia and in other literature. It’s also important to note that return and bind are not part of the Fantasy Land spec and are mentioned here only for the sake of people interested in learning more about Monads.

Comonad:余单子

实现了extract与extend函数的对象

let CoIdentity = v => ({
    val: v,
    extract: this.v,
    extend: f => CoIdentity(f(this))
})

Extract 可以将值从Functor中吐出来:

CoIdentity(1).extract() // 1

Extend则是会返回一个跟Commonad相同值的函数:

CoIdentity(1).extend(co => co.extract() + 1) // CoIdentity(2)

Applicative(可适用的) Functor

一个Applicative Functor就是一个实现了ap函数的对象,Ap可以将某个对象中的某个值转化为另一个对象中的相同类型的值

[(a)=> a + 1].ap([1]) // [2]

Morphism:态射

一个转化函数

Isomorphism:同态转换

用不同方式存储的能够表明相同数据的转换

譬如,一个二维的数组可以存储为数组:[2,3]或者对象:{x: 2, y: 3}。

// Providing functions to convert in both directions makes them isomorphic.
const pairToCoords = (pair) => ({x: pair[0], y: pair[1]})

const coordsToPair = (coords) => [coords.x, coords.y]

coordsToPair(pairToCoords([1, 2])) // [1, 2]

pairToCoords(coordsToPair({x: 1, y: 2})) // {x: 1, y: 2}

Setoid

实现了equals函数的对象,即可以与其他对象进行对比判断是否属于同一类型,被称为Setoid。

下面对于原型的扩充可以将Array变成Setoid。

Array.prototype.equals = arr => {
    var len = this.length
    if (len != arr.length) {
        return false
    }
    for (var i = 0; i < len; i++) {
        if (this[i] !== arr[i]) {
            return false
        }
    }
    return true
}

[1, 2].equals([1, 2]) // true
[1, 2].equals([0]) // false

Semigroup:半群

一个拥有concat,即将另一个对象转化为相同类型的函数,函数的对象称为Semigroup。

[1].concat([2]) // [1, 2]

Foldable:可折叠

实现了reduce函数,即可以将一个对象转化为其他类型的函数,的对象称为Foldable对象。

let sum = list => list.reduce((acc, val) => acc + val, 0);
sum([1, 2, 3]) // 6

Traversable

暂无

Type Signatures:类型签名

一般来说,函数都会注释表明它们的参数类型和返回值类型

// functionName :: firstArgType -> secondArgType -> returnType

// add :: Number -> Number -> Number
let add = x => y => x + y

// increment :: Number -> Number
let increment = x => x + 1

如果一个函数接收其他函数作为参数,譬如这样:

// call :: (a -> b) -> a -> b
let call = f => x => f(x)

这里的a, b, c, d表明参数可以是任意类型,不过它会将类型a转化为另一个类型b,而对于下面这个map,它的注释表明了它会输入一个a类型的列表,然后转化为另一个包含了b类型的列表。

// map :: (a -> b) -> [a] -> [b]
let map = f => list =>  list.map(f)

原文地址