最近学习 SwiftUI,看到有如下写法:

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("Trutle Rock")
                .font(.title)
                .foregroundColor(.black)
            HStack {
                Text("Joshua Tree National Park")
                    .font(.subheadline)
                Spacer()
                Text("California")
                    .font(.subheadline)
            }
        }
    }
}

其中

VStack(alignment: .leading) {
    //...
}

这个闭包语句,一开始没有很好理解。它的语法看起来像函数声明语句,另外它和计算属性有什么区别?为什么可以写到实例化语句的后边?

闭包

定义上,闭包是实现词法作用域的一种手段。在 Python、JavaScript 中都有闭包。简单来说,闭包就是一个保留了其定义时环境变量的函数,即使该环境已经不复存在。变量作为自由变量存在于函数内,就像被函数捕获了一样(参见Python 中的闭包与局部作用域)。

通常会把一个在局部定义的函数成为闭包。

|—————————————————|
|   scope A       |
|   x             |
|                 |
|     |————————|  |        |————————|
|     | scopeB |  |        | scopeB |
|     | x      |--|------> |        |
|     |        |  |        | |      |
|     |————————|  |        ||——————|
|                 |          |
|—————————————————|          x (as free variable)

Swift 中的闭包

Swift 中的闭包也不例外,它是自包含的函数代码块,可以在代码中被传递和使用,闭包可以捕获和存储其所在上下文中任意常量和变量的引用

Swift 中有三种形式的闭包:

  1. 全局函数:一个有名字但不捕获任何值的闭包。
  2. 嵌套函数:一个有名字且可以捕获其封闭函数域内值的闭包。
  3. 闭包表达式:一个轻量级的可以捕获其上下文变量及常量的匿名闭包。

全局函数和嵌套函数闭包在 JavaScript 等语言内也是很常见的,在此我们就不再做进一步的研究,这里我们主要研究的是第三中形式:闭包表达式。

闭包表达式

闭包表达式的语法结构如下:

{ (parameters) -> (return type) in
    statements
}

一个例子:

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

我们定义了一个闭包作为 by 参数的值。

闭包表达式可以有以下几个更简洁的形式:

闭包表达式可以省略其参数或者返回值的类型

names.sorted(by: { s1, s2 in return s1 > s2 })

闭包表达式可以省略其参数名字,然后用$0、$1依次来引用参数

如果参数名字和类型都省略了,in 关键字也可以省略。

names.sorted(by: {return $0 > $1})

如果返回值只是一个简单的语句,那么 return 也可以省略

names.sorted(by: {$0 > $1})

尾随闭包

如果需要将一个闭包表达式作为一个函数的最后一个参数,而这个闭包表达式又很长,可以使用尾随闭包来增加可读性。尾随闭包写在函数括号后,即使它是函数的一个参数。

下边我们看 SwiftUI 的 VStack 定义:

public struct VStack<Content> : View where Content : View {
    @inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)

    public typealias Body = Never
}

可以看到其构造器最后一个参数接收一个返回 Content 类型的闭包,所以文章开头提到的代码其实就是尾随闭包。

VStack(alignment: .leading) {
    Text("Trutle Rock")
        .font(.title)
        .foregroundColor(.black)
    HStack {
        Text("Joshua Tree National Park")
            .font(.subheadline)
        Spacer()
        Text("California")
            .font(.subheadline)
    }
}

如果尾随闭包是函数的唯一参数,则函数括号也可以省略:

names.sorted() {$0 > $1}

// equals

names.sorted {$0 > $1}

捕获值

捕获值是闭包最基础的性质。利用这一性质,我们可以方便的写出一些高阶函数。

func makeIncrementer(_ amount: Int) -> () -> Int {
    var total = 0
    func incrementer() -> Int {
        total += amount
        return total
    }
    return incrementer
} 

let incrementOne = makeIncrementer(1)
let incrementTwo = makeIncrementer(2)

print(incrementOne()) // 1
print(incrementOne()) // 2
print(incrementOne()) // 3

print(incrementTwo()) // 2
print(incrementTwo()) // 4
print(incrementTwo()) // 6

闭包是引用类型

注意上述 increOneincreTwo 都是常量,但是闭包仍然可以修改其捕获的 total 变量。这是因为函数和闭包是引用类型。当我们把闭包赋值给一个常量或者变量时,指向闭包的 increOne 是常量或者变量,而非闭包的内容。

let anotherIncrement = incrementOne
print(anotherIncrement()) // 4

逃逸闭包

如果一个闭包作为参数传递给函数,但是在函数返回之后才执行,就称这个闭包从函数中逃逸。我们可以在参数名前添加 @escaping 来标注允许这个闭包逃逸。

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

逃逸闭包必须明确引用 self:

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

class SomeClass {
    var x = 10
    func doSomething() {
        // 必须明确引用 self
        someFunctionWithEscapingClosure { self.x = 100 }
        // 不必明确引用 self
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"

completionHandlers.first?()
print(instance.x)
// Prints "100"

自动闭包

如果一个闭包不接收任何参数,调用时返回闭包内的表达式,我们可以添加 @autoclosure 来将表达式自身作为闭包。

func autoClosure(closure: @autoclosure () -> Int ) -> Int {
    reeturn closure()
}

print(autoClosure(closure: 9)) // 9

// 对比非自动闭包
func nonAutoClosure(closure: () -> Int ) -> Int {
    reeturn closure()
}

print(nonAutoClosure(closure: { 9 })) // 9

自动闭包可以省略花括号,但是过度使用会降低代码的可读性。

参考:

在这篇文章中,我们会尝试使 JavaScript 的类具有 Python 的风格:在方法内明确地传递和使用 self 参数,但是调用时自动传入实例参数。我们将使用描述符和 Proxy 来进行实现,在此过程中来学习属性描述符、Proxy、Reflect 的概念。

this 问题

this 是 JavaScript 中一个很重要有很迷惑人的东西,我们总是要时刻思考函数内的 this 到底是谁。随便翻一翻一些开发论坛,像 JavaScript 中的 this 指的是什么 是很高频的文章(实际上,我自己也写过)。那么为什么像 Python 就没有这个问题呢。

对于下边这个 Python 类:

class Dog:
    def __init__(self, name):
        self.name = name
    def bark(self):
        print(f'Wang! {self.name}')
tom = Dog('Tom')
tom.bark()
# Wang! Tom

bark = tom.bark
bark()
# Wang! Tom

snoopy = Dog('Snoopy')
snoopy.bark = bark
snoopy.bark()
# Wang! Tom

无论怎么调用 dog.bark, 函数内的 self 永远都是 tom

Explicit is better than implicit.

———— The Zen of Python

Python 中的 self 是明确传参,明确引用,所以我们在任何时候调用,self 都指向实例对象 tom。而 JavaScript 中 this 并没有明确传入而是依赖于调用时的对象,但是我们可以明确地引用,这就造成了 this 的混乱。

你可能会说:我调用 tom.bark() / bark() 的时候并没有把实例对象(tom)作为参数传递进去呀?原因就在于实例的方法是一个属性描述符而非定义时的那个函数对象。方法本身已经绑定了该实例,我们只用传递额外的参数,而方法会自动将实例对象传递给函数。

在 JavaScript 中,我们可以使用 class field 与箭头函数 => 来实现实现 this 的自动绑定,但本文的目的不是为了实现 this 绑定,而是在此过程中来学习了解 Proxy、Reflect 等概念。

什么是描述符

Python 中的描述符就是一个实现了__get__/__set__/__delete__方法的对象。JavaScript 中的描述符则可以具有 configurable enumerable value writable get set 等属性,分别构造出数据描述符/存取描述符

我们来使用描述符来使 bark 方法成为一个绑定了实例参数的方法。

class Dog {
    constructor(name) {
        this.name = name
    }
    bark(self) {
        console.log(`Wang! ${self.name}`)
    }
}

let tom = new Dog('Tom')

Object.defineProperty(tom, 'bark', {
    get() {
        return () => {
            return Dog.prototype.bark(tom)
        }
    }
})

tom.bark()
// Wang! Tom

let bark = tom.bark
bark()
// Wang! Tom

在我们读取 tom.bark 的时候,属性描述符的 getter 被执行,返回了一个新的函数,该函数不是我们在 Class 内定义的函数,而是一个包含了实例自由变量的闭包函数。当我们调用它的时候,它会自动把实例对象传递给 Dog.prototype.bark 方法。

用描述符(简单)实现了我们的目标,但是还有问题:

  • 如果有多个方法,需要对每个方法定义一次描述符,很麻烦。
  • 如果用户用 class field + 箭头函数的方法定义方法,name 方法将作为实例的属性,而非其原型的属性。Dog.prototype.bark 调用将会出错。如果我们在描述符内直接用 tom.bark(tom) 会导致重复无限递归查找:
    • tom.bark --getter--> tom.bark --getter--> tom.bark...

Proxy

现在我们来研究如何通过 Proxy 来实现。Proxy 就如其名字一样,可以代理(Proxy)对象的一些底层操作。

var p = new Proxy(target, handler);

Proxy 的关键在 handler,handler 对象有多个陷阱(trap)用来处理代理对象的各种操作,包括属性读取、被删除、对象被调用、被 new、被判断属性的存在等等。如果没有对相应的行为定义 trap,那么对代理对象的操作将被转发到目标对象身上。

现在我们来拦截所有对方法的访问:如果对象属性是一个函数,我们就返回一个绑定了实例对象的高阶函数。

class Dog {
    constructor(name) {
        this.name = name
    }
    bark(self) {
        console.log(`Wang! ${self.name}`)
    }
}

let methodHandler = {
    // target: 目标对象
    // property: 属性名
    // receiver: 代理对象
    get(target, property, receiver) {
        let value = Reflect.get(target, property)
        if (typeof value === 'function') {
            return (...args) => value(target, ...args)
        }
        return value
    }
}

let tom = new Dog('Tom')
let proxiedTom = new Proxy(tom, methodHandler)

proxiedTom.bark()
// Wang! Tom

let bark = proxiedTom.bark
bark()
// Wang! Tom

let snoopy = new Dog('Snoopy')
let proxiedSnoopy = new Proxy(snoopy, methodHandler)

proxiedSnoopy.bark()
// Wang! Snoopy

看起来我们不必为每个属性定义操作符了,而且我们调用 proxiedTom.bark 时不管 bark 方法在 tom 自己身上还是在其原型身上,都不会触发无限递归查找了:proxiedTom.bark --proxy--> Reflect.get(tom, 'bark') -> bark method

但是我们需要为每一个实例生成它的代理对象。可不可以在 new Dog 的时候就返回生成的代理对象呢?只需要代理 Dog 的 construct 行为即可。

class Dog {
    constructor(name) {
        this.name = name
    }
    bark(self) {
        console.log('Wang~', self.name)
    }
    eat(self, food) {
        console.log(self.name, 'eat', food)
    }
}

let constructHandler = {
    // target: 目标对象
    // argumentsList: 参数列表
    // newTarget: 最初被调用来构造的函数,如代理对象
    construct(target, argumentsList, newTarget) {
        // 构造实例对象
        let instance = Reflect.construct(target, argumentsList)
        // 代理并返回代理对象
        return new Proxy(instance, methodHandler)
    }
}

let ProxiedDog = new Proxy(Dog, constructHandler)

let tom = new ProxiedDog('Tom')

tom.bark()
// Wang~ Tom

let snoopy = new ProxiedDog('Snoopy')

snoopy.bark()
// Wang~ Snoopy

snoopy.eat('milk')
// Snoopy eat milk

snoopy.name
// 'Snoopy'

snoopy.tomsbark = tom.bark
snoopy.tomsbark()
// Wang~ Tom

那么 Reflect 又是什么?

Reflect 有一系列操作对象的静态方法。 每一个 handler trap 都对应一个 Reflect 操作,如 get trap 对应于 Reflect.get。defineProperty trap 对应于 Reflect.defineProperty

有一些 Reflect 方法实现的效果与 Object 对象上的方法一样,但是有一些不同。

Reflect.defineProperty 为例,它的效果与 Object.defineProperty 一样,但是 Reflect.defineProperty 会在操作成功是返回布尔值 trueObject.defineProperty 则返回传递给它的对象。

let tom = {
    name: 'tom'
}
let tommy = Object.defineProperty(tom, 'age', {
    get() {
        return 3
    }
})

tom === tommy
// true

let snoopy = {
    name: 'snoopy'
}
let result = Reflect.defineProperty(snoopy, 'age', {
    get() { 
        return 3
    }
})

result === true
// true

关于 Reflect 提供的方法与 Object 的方法的具体不同,可以参考这个表格

Reflect 与 Object 相比的另一个有用的地方是,在对象设置了 getter 的时候,Reflect 可以很好地处理这种情况。

// 比如我们有一个会员对象
let member = {
    _name: 'Han Solo',
    get name() {
        return this._name
    }
}

// 然后我们想用一个代理来实现会员匿名。

let anonymousMember = new Proxy(member, {
    get(target, property, receiver) {
        if (property === '_name') {
            return 'anonymous'
        }
        return target[property]
    }
})

当我们调用 anonymousMember.name,会输出什么?

anonymousMember.name --proxy--> member['name'] ---getter--> this._name -> member._name -> 'Han Solo'

所以我们没有拦截到对 member_name 属性的访问。

使用 Reflect 可以让我们指定 getter 内的 this 来实现对 _name 属性的真正拦截。

let anonymousMember = new Proxy(member, {
    get(target, property, receiver) {
        if (property === '_name') {
            return 'anonymous'
        }
        // 第三个参数 receiver 指定了 getter 调用时的 this,在此为我们的代理对象 anonymousMember
        return Reflect.get(target, property, receiver)
    }
})

anonymousMember.name
// 'anonymous'

// anonymousMember.name --proxy--> Reflect.get(member, 'name', anonymousMember) ---getter---> anonymousMember._name -> 'anonymous'

参考

前一段时间项目上有过一个需求,可以考虑通过自动给 React 组件添加属性来实现。虽然这是一个很反模式的方式并且最终否决了这个方案,但是还是尝试研究了一下如何实现 自动给 React 组件添加属性。更确切地说,是在 webpack 打包出来的最终文件内给 React 组件添加上自定义的 props

webpack 处理 React 文件(js/jsx)使用 babel-loader,babel 就是我们的 JavaScript 编译器,它接收我们的源代码作为输入,产出编译后的可运行于浏览器的目标代码作为输出。babel 支持插件(plugin),可以视作编译器前端与后端之间的中间件:前端根据源代码生成抽象语法树(AST)等,后端根据抽象语法树生成目标代码,而插件作为中间件则是在生成目标代码之前对抽象语法树做相应的修改。

基础概念

抽象语法树

@babel/parser 模块用来接收我们的源代码来生成抽象语法树,

import * as parser from '@babel/parser'

let code = 'let result = x + y'

let ast = parser.parse(code)

console.log(ast)

将会输出

Node {
  type: 'File',
  start: 0,
  end: 13,
  loc:
   SourceLocation {
     start: Position { line: 1, column: 0 },
     end: Position { line: 1, column: 13 } },
  program:
   Node {
     type: 'Program',
     start: 0,
     end: 13,
     loc: SourceLocation { start: [Position], end: [Position] },
     sourceType: 'script',
     interpreter: null,
     body: [ [Node] ],
     directives: [] },
  comments: [] }

如果我们打印出 ast.program.body,我们会得到:

[ Node {
    type: 'VariableDeclaration',
    start: 0,
    end: 18,
    loc: SourceLocation { start: [Position], end: [Position] },
    declarations: [ [Node] ],
    kind: 'let' } ]

可以看到抽象语法树是由一个个 Node 组成的,每个 Node 有很多属性,而 Node 还会有一些 body、left 之类的属性,这些属性的值的类型也可能是 Node。对抽象语法树的修改就是修改这些 Node 的值或者属性。

访问者模式

访问者模式是一种将算法与对象结构分离的软件设计模式。

这个模式的基本想法如下:首先我们拥有一个由许多对象构成的对象结构,这些对象的类都拥有一个accept方法用来接受访问者对象;访问者是一个接口,它拥有一个visit方法,这个方法对访问到的对象结构中不同类型的元素作出不同的反应;在对象结构的一次访问过程中,我们遍历整个对象结构,对每一个元素都实施accept方法,在每一个元素的accept方法中回调访问者的visit方法,从而使访问者得以处理对象结构的每一个元素。我们可以针对对象结构设计不同的实在的访问者类来完成不同的操作。

———— 维基百科

具体来说,我们的 AST 的每一个 Node 有一个 accept 方法,当我们用一个 visitor 来遍历我们的 AST 时,每遍历到一个 Node 就会调用这个 Node 的 accept 方法来 接待 这个 visitor,而在 accept 方法内,我们会回调 visitor 的 visit 方法。我们来用访问者模式来实现一个 旅行者访问城市景点 的逻辑。

*实际上 Node 是有两个方法,enter 和 exit,指遍历进入和离开 Node 的时候。通常访问者的 visit 方法会在 enter 内被调用。

// 旅游景点
class ScenicPoint {
    constructor(name) {
        this.name = name
    }
    // 景点的 accept 方法接收 visitor,函数内调用 visitor 的 visit 方法来 visit 景点的实例
    accept(visitor) {
        visitor.visit(this)
    }
}

class Park extends ScenicPoint { }

class Museum extends ScenicPoint { }

// 我们的城市
class City {
    constructor(name, scenicPointList) {
        this.name = name
        this.scenicPointList = scenicPointList
    }
    accept(visitor) {
        for (let scenicPoint of this.scenicPointList) {
            scenicPoint.accept(visitor)
        }
    }
}

// visitors: Alice 与 Bob
let Alice = {
    name: 'Alice',
    visit(scenicPoint) {
        if (scenicPoint instanceof Park) {
            console.log(`${scenicPoint.name} is a wonderful park~`)
        } else {
            console.log(`${this.name} visiting ${scenicPoint.name}`)
        }
    }
}
let Bob = {
    name: 'Bob',
    visit(scenicPoint) {
        if (!(scenicPoint instanceof Museum)) {
            console.log(`I want to go to some Museum.`)
        } else {
            console.log(`${scenicPoint.name} is a wonderful Museum~`)
        }
    }
}

let BeiJing = new City('BeiJing', [
    new ScenicPoint('八达岭长城'),
    new Park('玉渊潭公园'),
    new Museum('国家博物馆'),
])

我们用一个访问者来访问北京:

BeiJing.accept(Alice)
// Alice visiting 八达岭长城
// 玉渊潭公园 is a wonderful park~
// Alice visiting 国家博物馆

BeiJing.accept(Bob)
// I want to go to some Museum.
// I want to go to some Museum.
// 国家博物馆 is a wonderful Museum~

可以发现,我们可以自定义 visitor 的 visit 方法,针对不同的景点实现不同的逻辑。

返回到我们的 babel 插件,每一个 Node 都可以视为一个景点,而我们的插件则是一个 visitor,我们在插件内定义了针对不同 Node 类型的处理方式,当遍历到对应的节点时调用对应的方法进行处理。

插件编写

首先我们应该看我们的 React 元素是怎么创建出来的:

let element = <Button color="red" />

会被编译为:

var element = _react.default.createElement(Button, {
  color: "red"
})

可以看到组件属性作为 _react.default.createElement 的第二个参数,我们只需要修改这个参数值就可以了。

我们可以先看上述代码生成的 AST:

{ type: 'CallExpression',
  callee:
   { type: 'MemberExpression',
     object: { type: 'Identifier', name: 'React' },
     property: { type: 'Identifier', name: 'createElement' },
     computed: false,
     optional: null },
  arguments:
   [ Node {
       type: 'Identifier',
       start: 42,
       end: 48,
       loc: [SourceLocation],
       name: 'Button' },
     { type: 'ObjectExpression', properties: [Array] } ],
  typeAnnotation: undefined,
  typeParameters: undefined,
  returnType: undefined,
  start: 41,
  loc:
   SourceLocation {
     start: Position { line: 3, column: 14 },
     end: Position { line: 3, column: 36 } },
  end: 63,
  trailingComments: [],
  leadingComments: [],
  innerComments: [] }

可以发现 props 是这个 CallExpression 的 arguments 属性数组的第二个元素,而 props 自身是一个 ObjectExpression,我们只需要用我们的 props 对象来替换/合并原来的 ObjectExpression 即可。

思路已有,直接上最终代码:

// parser
import * as parser from '@babel/parser'
// 各种 Node 类型定义
import * as types from '@babel/types'
import traverse, { NodePath } from '@babel/traverse'

export default function() {
    return {
        // 我们的 visitor
        visitor: {
            // 针对函数调用的单独逻辑处理
            CallExpression(path: NodePath<types.CallExpression>, state) {
                // 我们只处理 React.createElement 函数调用
                let { callee } = path.node
                if (
                    !(
                        types.isMemberExpression(callee) &&
                        types.isIdentifier(callee.object) &&
                        callee.object.name === 'React' &&
                        types.isIdentifier(callee.property) &&
                        callee.property.name === 'createElement'
                    )
                ) {
                    return
                }

                // 从第一个参数获取组件名称(Button)
                // 从第二个参数获取组件属性
                let [element, propsExpression] = path.node.arguments
                let elementType: string
                if (types.isStringLiteral(element)) {
                    elementType = element.value
                } else if (types.isIdentifier(element)) {
                    elementType = element.name
                }

                // 我们的插件支持自定义选项,针对不同的组件类型传入不同的额外自定义属性
                const options: Object = state.opts
                let extraProps: Object | undefined = options[elementType]

                // 如果没有针对次组件类型的额外参数,我们的插件什么都不做
                if (!extraProps) {
                    return
                }

                // 否则,我们利用 parser.parseExpression 方法以及我们的自定义属性生成一个 ObjectExpression
                let stringLiteral = JSON.stringify(extraProps)
                let extraPropsExpression = parser.parseExpression(stringLiteral)

                // 如果组件原本 props 为空,我们直接将我们的自定义属性作为属性参数
                if (types.isNullLiteral(propsExpression)) {
                    path.node.arguments[1] = extraPropsExpression
                } else if (types.isObjectExpression(propsExpression)) {
                // 否则,我们将我们的自定义属性与原属性进行合并(只处理对象类型的 props)
                    path.node.arguments[1] = types.objectExpression(
                        propsExpression.properties.concat(
                            (<types.ObjectExpression>extraPropsExpression).properties,
                        ),
                    )
                }
            },
        },
    }
}

然后我们修改 babel 的配置文件启用我们的插件:

// babel.config.js
const plugins = [
    [
        'babel-plugin-react-auto-props',
        {
            'Button': {
                size: 'small',
            },
        },
    ],
]

module.exports = { presets, plugins }

然后我们的源代码为:

let button = <Button type="primary" />

使用 babel 编译后的结果为:

var button = _react.default.createElement(Button, {
  type: "primary",
  "size": "small"
});

可以看到属性已经自动添加成功。

我应该使用这个插件吗?

这个插件已经发布到 npm,不过请注意,该插件只是一个实验性的插件,而且它的结果很反模式。如果在项目中确实需要为组件自动添加属性,请使用特例关系组件

class SmallButton extends Component {
    render() {
        return <Button {...this.props} size="small" />
    }
}

如果只是想尝试,可以通过 npm 安装:

npm install -D babel-plugin-react-auto-props

RSA 加密算法是目前广泛使用的一种非对称加密算法。RSA 加密算法的可靠性依赖于极大整数因数分解的困难度。本文主要根据 RSA 加密算法实现一个简单版本的 Python 代码实现,包括私钥、公钥生成与加密、解密方法。

RSA 加密算法概述

RSA 加密算法为非对称加密算法,意味着加密和解密使用不同的秘钥,称为 公钥私钥 ,公钥可以任意分发并用于加密,私钥需妥善保存用于解密。

公钥

公钥由两个数 E、N 组成,使用公钥加密时运算

密文 = (明文 ** E) % N

私钥

私钥由两个数 D、N 组成,使用私钥解密时运算

明文 = (密文 ** D) % N

所以生成 RSA 加密算法的秘钥就是生成 E、D、N 这三个数。生成过程大致如下:

  1. 生成两个大质数 p 和 q,计算 N = p*q
  2. 求得 L = (p-1) * (q-1)
  3. 选择一个大于 1 小于 L 的正整数 E,使 E 与 L 互质
  4. 求得 E 关于 L 的模逆元,即为 D,D 小于 L

实现

质数生成函数

为了生成质数,我们需要判断一个数是否是质数,我们可以使用数学上的费马小定理

如果 a 是一个整数,n 是一个质数,那么 a ** n ≡ a (mod n)

———— 费马小定理

符合费马小定理是一个数是质数的必要而非充分条件,所以为了判断一个数 p 是否是质数,通常需要使用 k 个不同的 a 值来进行验证,称为费马素性检验,如果随机出来的 a 值都符合费马小定理,我们认为该数是一个质数。

质数判断函数:

import random

# 使用费马小定理检查一个数 n 是不是质数
def is_prime(n):
    if n < 2:
        return False

    # 100次 费马素性检测
    for _ in range(100):
        a = random.randint(2, n-1)
        if pow(a, n, n) != a:
            return False
    return True

质数生成函数:

# 生成一个随机的大质数(介于 10^100 与 10^110 之间)
def generate_prime():
    while True:
        expect_number = random.randint(1E100, 1E110)
        if is_prime(expect_number):
            break
    return expect_number

生成 p、q、N、L:

p = generate_prime()
q = generate_prime()

N = p*q

L = (p-1) * (q-1)

生成 E:

根据算法,E 是介于 1 与 L 之间与 L 互质的数(最大公约数为 1):

import math

# E 的随机生成函数
def generate_e():
    while True:
        expect_e = random.randint(2, L)
        if math.gcd(expect_e, L) == 1:
            break
    return expect_e

E = generate_e()

生成 D:

根据算法,D 是 E 关于 L 的模逆元,也就是:

E * D ≡ 1 (mod L)

我们当然可以从 1 开始一直试到 L 来寻找 D,但是在 D 很大大的情况下计算量是非常大的。这里我们使用扩展欧几里得算法(以下代码出自该 wiki 条目):

# 扩展欧几里得算法求模逆元
def ext_euclid(E, L):
     if L == 0:
         return 1, 0, E
     else:
         x, y, q = ext_euclid(L, E % L) # q = gcd(E, L) = gcd(L, E%L)
         x, y = y, (x - (E // L) * y)
         return x, y, q

D = ext_euclid(E, L)[0]

加密与解密算法

至此,我们的秘钥已经生成完成:公钥 (N, E)、私钥:(N, D)。

# 加密算法:
# 密文 = 明文 ** E % N
def encrypt(message):
    return pow(message, E, N)

# 解密算法
# 明文 = 密文 ** D % N
def decrypt(secret):
    return pow(secret, D, N)

下边为我们根据上述算法生成的一对秘钥:

公钥 (N, E) = (
    4035201027052526716476770815301885547930386611424746846836729207720402684586903067390911032635700387807809913761690633108036479608875715316392163632899103649241282491070030290136720168432012867861620750453121897352765277, 
    3008241542036098467083169586257729095679728537524058797442421603938590337844782185494379049491888737248445705876804690371511766177598271941925223485143156067146179224661664800047568667114630699761924649833705169821421939
)

私钥:(N, D) = (
    4035201027052526716476770815301885547930386611424746846836729207720402684586903067390911032635700387807809913761690633108036479608875715316392163632899103649241282491070030290136720168432012867861620750453121897352765277, 
    263104268796040141692115220957372648791994375662679669674392390239402717514924248311933394103742807630673302384124516817979074152829180217297193870626824603082561775343291294186200630148873101176593862045968382744834439
)

我们尝试一次加密与解密:

message = 3141592653

secret = encrypt(message)
print(secret)
# 1926350294561951470341018025045828648112140614621362576015267907023473556254498224885881403355559114529772490367097950170850223416554454126315058562501008274952527345854593658423828584546446402279730866190911413291563786

decrypted_message = decrypt(secret)
print(decrypted_message)
# 3141592653

message == decrypted_message
# True

解密后的明文和开始的明文相同,加密解密成功。

软件开发离不开开源项目,自己单干无异闭门造车,所以我们需要别人的库/工具(package)帮助我们完成工作。我们需要的,是尽可能轻松地集成到我们的项目内,并且能很好地解决我们的需求而不引起其他问题。我们以以下三个方面来考虑是否选择一个库:

  • 功能性:可以解决我们的问题
  • 可靠性:不会因为引入它而引起其他问题
  • 复杂性:集成尽可能简单

功能性是我们的根本目的,但是不应该仅仅止于此。不能简单地只是因为一个库能够解决我们的问题我们就选它

1. 我们可能不需要一个 package

module_count

这张图反映的是 npm 包含的库的数量对比 Java、Python 库的数量,可以看到 npm 库的数量非常之多。但是有多少是真是有用的呢。

这个库:is-odd,在目前每周有 849,241 次下载,在 GitHub 上有 622,528 个其他仓库依赖于它。但是它的作用,就是和它的名字一样,用来检查一个数字是不是奇数,它的源代码不到 20 行。

这是一个例子,告诉我们在实现很简单的情况下,我们完全可以自己实现,而不必去借助外部的库来帮我们实现。

GitHub 上有一些项目,专门用来总结“你可能不需要 XXX”,比如:

我们再以 moment 为例,moment 是一个非常强大,同时也非常 API 友好的项目,它能够方便地解决我们的问题。但是如果我们统计一下,就会发现,我们最常用到的 moment 功能,可能不超过 5 个。但是 moment 打包体积有 329KB,即使 GZip 压缩之后,仍然有 69.6KB。而那几个最常用的功能如果我们自己来实现的话,可能体积会缩小几十到上百倍,这时候更好的方案可能就是不引入 moment(当然这只是理想情况,比如我们用到了 antd,antd 的一些组件会依赖于 moment,那我们还是需要引入 moment)。

2. 当我们确实需要一个 package 时,我们应该考虑的因素

module_react

可靠性

一个常见且简单的方法是看它的 GitHub Star 数,但是我们会发现这个评价标准是非常片面的。我们应该从以下这些方面来评估其可靠性:

更新日期(越近越好)

上图中,react 的 上次提交时间是 Latest commit 45acbdc 7 hours ago,说明它的维护很及时。而如果一个项目的最近更新日期是 3 年前,那选择它之前就要好好考虑一下了。

Issues / Pull Requests(越少,Open/Close 比越小越好)

Issue 数值越低,说明存在问题的可能越小。另外由于不同项目流行度的差异,看 Issues 的 Open/Close 比值更能说明问题。比值越小说明作者维护项目的意愿越强,出了问题更可能被解决。

Pull Requests 同理。

Contributors 贡献者数量(越多越好)

Contributors 越多,说明社区力量越强大,项目可维护性更好。

作者(优先选择知名作者)

相比于 someone/project,我可能更倾向于选择 Microsoft/project。当然这个标准也不是绝对的。

Star / Watch / npm downloads(越多越好)

没错,这些指标还是有一定参考意义的。但是我们应该知道,一个项目比另一个更流行,可能仅仅是因为它是一个知名作者的项目,或者被媒体/知名作者推荐过。所以我们不能说一个 500 Star 的比 5000 Star 的差。

但是 5000 Star 的项目通常来说会比 50 Star 的项目要好。

体积(越小越好)

对于 Web 应用来说,代码体积至关重要,它影响页面渲染时间,进而影响用户体验。所以要尽量选择较小的库。

开源许可协议

如果软件涉及商用的话,那就需要了解 MIT、BSD、Apache、GPL 等开源协议的区别。

Choose an open source license

复杂性

复杂性低的表现:

  • 有全面的文档
  • Get Started 指引,帮助我们快速上手
  • 即插即用,无需额外操作

复杂性高的表现:

  • 没有文档或文档混乱
  • 不知道怎样快速上手
  • 接入十分复杂,需要改我们的很多代码,甚至其他第三方代码

一个例子是关于我们如何舍弃 react-hot-loader 的。该项目是著名的 Dan Abramov 开发的,现在由社区维护版本升级。直到 React 16.8 发布之后,它与 React Hooks 不兼容,以至于要想使其正常工作需要在开发时舍弃 React 官方的 React-Dom 而采用他们维护的 React-Dom 版本。即使如此,经过复杂的配置之后,我还是没能让它顺利正确地工作起来,所以最后只能舍弃了它。