边城

边城 查看完整档案

绵阳编辑西南科技大学  |  (计算机科学与技术) (MBA) 编辑四川凯路威科技有限公司  |  软件总工程师 编辑 ke.sifou.com/course/1650000023477528 编辑
编辑

从事软件开发 20 年,在软件分析、设计、架构、开发及软件开发技术研究和培训等方面有着非常丰富的经验,近年主要在研究 Web 前端技术、基于 .NET 的后端开发技术和相关软件架构。

个人动态

边城 发布了文章 · 4月15日

安全地在前后端之间传输数据 - 「1」技术预研

已经不是第一次写这个主题了,最近有朋友拿 5 年前的《Web 应用中保证密码传输安全》来问我:“为什么按你说的一步步做下来,后端解不出来呢?”加解密这种事情,差之毫厘谬以千里,我认为多半就是什么参数没整对,仔细查查改对了就行。代码拿来一看,傻眼了……没毛病啊,为啥解不出来呢?

时间久远,原文附带的源代码已经下不下来了。翻阅各种参考链接的时候从 CodeProject 上找了个代码,把各参数换过去一试,没毛病呀!这可奇了怪了,于是去 RSA.js 的文档(没有专门的文档,就是文档注释)中查,发现 RSA.js 在 2014 年 1 月加入了 Padding 参数,《Web 应用中保证密码传输安全》虽然是 2014 年 2 月写的,但可能阴差阳错用到了老版本。

不就是 Padding 吗,文档也懒得看了,前后端都指定 PKCS1Padding 试试。失败!

那暴力一点,所有 Padding 都试试!

前端使用 RSA.js 在 RSAAPP 中定义的 4 种 Padding,后端 C# 使用 RSAEncryptionPadding 中定义的 5 种 Padding,组合了 20 种情况,逐一试验……好吧,没一个对的!

世界上这么多树,何必非要在这一棵上吊死,何况它还没有发布到 npm …… 理由找够了,咱就换!

网上搜了一圈之后,选择了 JSEncrypt 这个库。

核心知识

在讲 JSEncrypt 之前,咱们回到“安全传输”这一主题。这一主题的关键技术在于加解密,说起加解密,那就是三大类算法:HASH(摘要)算法、对称加密算法和非对称加密算法。基本的安全传输过程可以用一张图来 展示:

image.png

不过这只是最基本的安全传输理论,实际上,证书(公钥)分发等方面仍然存在安全隐患,所以才会有CA、才会有受信根证书……不过这里不作延展,只给个结论:在 Web 前后端传输这个问题上,HTTPS 就是最佳实践,是首先 Web 传输解决方案,只有在不能使用 HTTPS 的情况,才退而求其次,用自己的实现来提高一点安全门槛。

JSEncrypt

JSEncrypt 一个月前刚有新版本,还算活跃。不过在使用方式上跟 RSA.js 不同,它不需要指定 RSA 的参数,而是直接导入一个 PEM 格式的密钥(证书)。关于证书格式呢,就不在这里科普了,总之 PEM 是一种文本格式,Base64 编码。

既然 JSEnrypt 需要导入密钥,这里主要是需要导入公钥。我们来看看 C# 里 RSACryptoServiceProvider 能导出些什么,搜了一下 Export... 方法,导出公约相关的主要就这两个:

因为原始需求是用 .NET,所以先研究 .NET 跟 JSEncrypt 的配合,后面再补充 NodeJS 和 Java 的。
  • ExportRSAPublicKey(),以 PKCS#1 RSAPublicKey 格式导出当前密钥的公钥部分。
  • ExportSubjectPublicKeyInfo(),以 X.509 SubjectPublicKeyInfo 格式导出当前密钥的公钥部分。

还有两个 Try... 前缀的方法作用相似,可以忽略。这两个方法的区别就在于导出的格式不同,一个是 PKCS#1 (Public-Key Cryptography Standards),一个是 SPKI (Subject Public Key Info)。

JSEncrypt 能导入哪种格式呢?文档里没明确说明,不妨试试。

C# 产生密钥并导出

C# 中产生 RSA 密钥对比较简单,使用 RSACryptoServiceProvider 就行,比如产生一对 1024 位的 RSA 密钥,并以 XML 格式导出:

// C# Code

private RSACryptoServiceProvider GenerateRsaKeys(int keySize = 1024)
{
    var rsa = new RSACryptoServiceProvider(keySize);
    var xmlPrivateKey = rsa.ToXmlString(true);
    // 如果需要单独的公钥部分,将传入 `ToXmlString()` 改为 false 就好
    // var xmlPublicKey = rsa.ToXmlString(false);

    File.WriteAllText("RSA_KEY", xmlPrivateKey);
    return rsa;
}

为了能在进程每次重启都使用相同的密钥,上面的示例将产生的 xmlPrivateKey 保存到文件中,重启进程时可以尝试从文件加载导入。注意,由于私钥包含公钥,所以只需要保存 xmlPrivateKey 就够了。那么加载的过程:

// C# Code

private RSACryptoServiceProvider LoadRsaKeys()
{
    if (!File.Exists("RSA_KEY")) { return null; }
    var xmlPrivateKey = File.ReadAllText("RSA_KEY");

    var rsa = new RSACryptoServiceProvider();
    rsa.FromXmlString(xmlPrivateKey);
    return rsa;
}

先尝试导入,不成再新生成的过程就一句话:

// C# Code

var rsa = LoadRsaKeys() ?? GenerateRsaKeys();

导出 XML Key 是为了持久化。JSEncrypt 需要的是 PEM 格式的证书,也就是 Base64 编码的证书。ExportRSAPublicKeyExportSubjectPublicKeyInfo 这两个方法的返回类型都是 byte[],所以需要对它们进行 Base64 编码。这里使用 Viyi.Util 提供的 Base64Encode() 扩展方法来实现:

// C# Code

var pkcs1 = rsa.ExportRSAPublicKey().Base64Encode();
var spki = rsa.ExportSubjectPublicKeyInfo().Base64Encode();

严格的说,PEM 格式还应该加上 -----BEGIN PUBLIC KEY----------END PUBLIC KEY----- 这样的标头标尾,Base64 编码也应该按每行 64 个字符进行折行处理。不过实测 JSEncrypt 导入时不会要求这么严格,省了不少事。

剩下的就是将 pkcs1spki 传递给前端了。Web 应用直接通过 API 返回一个 JSON,或者 TEXT 都行,根据接口规范来决定。当然也可以通过拷贝/粘贴的方式来传递。这里既然是在做实验,那就用 Console.WriteLine 输出到控制台,通过剪贴板来传递好了。

我这里 PKCS#1 导出的是长度为 188 个字符的 Base64:

MIGJAoGB...tAgMBAAE=

SPKI 导出的是长度为 216 个字符的 Base64:

MIGfMA0GC...QIDAQAB

JSEncrypt 导入公钥并加密

JSEncrypt 提供了 setPublicKey()setPrivateKey() 来导入密钥。不过文档中提到它们其实都是 setKey() 的别名,这点需要注意一下。为了避免语义不清,我建议直接使用 setKey()

You can use also setPrivateKey and setPublicKey, they are both alias to setKey

from: http://travistidwell.com/jsen...

那么导入公钥并试验加密的过程大概会是这样:

// JavaScript Code

const pkcs1 = "MIGJAoGB...tAgMBAAE=";   // 注意,这里的 KEY 值仅作示意,并不完整
const spki = "MIGfMA0GC...QIDAQAB";     // 注意,这里的 KEY 值仅作示意,并不完整

[pkcs1, spki].forEach((pKey, i) => {
    const jse = new JSEncrypt();
    jse.setKey(pKey);
    const eCodes = jse.encrypt("Hello World");
    console.log(`[${i} Result]: ${eCodes}`);
});

运行后得到输出(密文也是省略了中间很长一串的 ):

[0 Result]: false
[1 Result]: ZkhFRnigoHt...wXQX4=

看这结果,没啥悬念了,JSEncrypt 只认 SPKI 格式

不过还得去 C# 中验证这个密文是可以解出来的。

C# 验证可以解密 JSEncrypt 生成的密文

上面生成的那一段 ZkhFRnigoHt...wXQX4= 拷贝到 C# 代码中,用来验证解密。C# 使用 RSACryptoServiceProvider.Decrypt() 实例方法来解密,这个方法的第 1 个参数是密文,类型 byte[],是以二进制数据的形式提供的。

第二个参数可以是 boolean 类型,true 表示使用 OAEP 填充方式,false 表示使用 PKCS#1 v1.5;这个参数也可以是 RSAEncryptionPadding 对象,直接从预定义的几个静态对象中选择一个就好。这些在文档中都说得很清楚。因为一般都是使用的 PKCS 填充方式,所以这次赌一把,直接上:

// C# Code

var eCodes = "ZkhFRnigoHt...wXQX4=";    // 示例代码这里省略了中间大部分内容
var rsa = LoadRsaKeys();   // rsa 肯定是使用之前生成的密钥对,要不然没法解密
byte[] data = rsa.Decrypt(eCodes.Base64Decode(), false);
Console.WriteLine(data.GetString());    // GetString 也是 Viyi.Util 中定义的扩展方法,默认用 UTF8 编码

结果正如预期:

Hello World

技术总结

现在,通过实验,Web 前端使用 JSEncrypt 和 .NET 后端之间已经实现了 RSA 加/解密来完成安全的数据传输。其作法总结如下:

  1. 后端产生 RSA 密钥对,保存备用。保存方式可根据实际情况选择:内存、文件、数据库、缓存服务等
  2. 后端以 SPKI 格式导出公钥(别忘了 Base64 编码),通过某种业务接口形式传递给前端,或由前端主动请求获得(比如调用特定 API)
  3. 前端使用 JSEncrypt,通过 setKey() 导入公钥,使用 encrypt() 加密字符串。加密前字符串会按 UTF8 编码成二进制数据。
  4. 后端获得前端加密后的数据(Base64 编码)后,解密成二进制数据,并使用 UTF8 解码成文本。

特别需要注意的一点是:不管以何种方式(XML、PEM 等)将公钥传送给前端的时候,都切记不要把私钥给出去了。这尤其容易发生在使用 .ToXmlString(true) 之后再直接把结果送给前端。不要问我为什么会有这么个提醒,要问就是因为……我见过!

关门放 Node

还没完呢,前面说过要补充 NodeJS 后端的情况。NodeJS 关于加/解密的 SDK 都在 crypto 模块中,

  • 使用 generateKeyPair()generateKeyPairSync() 来产生密钥对
  • 使用 privateDecrypt() 来解密数据
generateKeyPair() 是异步操作。现在 Node 中异步函数很常见,尤其是写 Web 服务端的时候,到处都是异步。不喜欢回调方式的话,可以使用 util 模块中的 promisify() 把它转换一下。
// JavaScript Code, in Node environtment

import { promisify } from "util";
import crypto from "crypto";

const asyncGenerateKeyPair = promisify(crypto.generateKeyPair);

(async () => {
    const { publicKey, privateKey } = await asyncGenerateKeyPair(
        "rsa",
        {
            modulusLength: 1024,
            publicKeyEncoding: {
                type: "spki",
                format: "pem",
            },
            privateKeyEncoding: {
                type: "pkcs1",
                format: "pem"
            }
        }
    );

    console.log(publicKey)
    console.log(privateKey);
})();

generateKeyPair 第 1 个参数是算法,很明显。第 2 个参数是选项,强度 1024 也很明显。只有 publicKeyEncodingprivateKeyEncoding 需要稍微解释一下 —— 其实文档也说得很明白:参考 keyObject.export()

对于公钥,type 可选 "pkcs1" 或者 "spki",之前已经试过,JSEncrypt 只认 "spki",所以没得选。

对于私钥,RSA 只能选 "pkcs1",所以还是没得选。

不过 NodeJS 的 PEM 输出要规范得多,看(同样省略了中间部分):

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYur0zYBtqOqs98l4rh1J2olBb
... ... ...
8I8y4j9dZw05HD3u7QIDAQAB
-----END PUBLIC KEY-----
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCYur0zYBtqOqs98l4rh1J2olBbYpm5n6aNonWJ6y59smqipfj5
... ... ...
UJKGwVN8328z40R5w0iXqtYNvEhRtYGl0pTBP1FjJKg=
-----END RSA PRIVATE KEY-----

不管是否含标头/标尾,也不管是不是有折行,JSEncrypt 都认,所以倒不用太在意这些细节。总之 JSEncrypt 拿到公钥之后还是跟之前一样,做同样的事情,逻辑代码一个字都不用改。

然后回到 NodeJS 解密:

// JavaScript Code, in Node environtment

import crypto from "crypto";

const eCodes = "ZkhFRnigoHt...wXQX4=";    // 作为示例,偷个懒就用之前的那一段了
const buffer = crypto.privateDecrypt(
    {
        key: privateKey,
        padding: crypto.constants.RSA_PKCS1_PADDING
    },
    Buffer.from(eCodes, "base64")
);

console.log(buffer.toString());

privateDecrypt() 第 1 个参数给私钥,可以是之前导出的私钥 PEM,也可以是没导出的 KeyObject 对象。需要注意的是必须要指定填充方式是 RSA_PKCS1_PADDING,因为文档说默认使用 RSA_PKCS1_OAEP_PADDING

还有一点需要注意的是别忘了 Buffer.from(..., "base64")

解密的结果是保存在 Buffer 中的,直接 toString() 转成字符串就好,显示指定 UTF-8,用 toString("utf-8") 当然也是可以的。

等等,还有 Java 呢

Java 也大同小异,不过说实在,代码量要大不少。为了干这些事情,大概需要导入这么些类:

// Java Code

import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.Base64.Encoder;
import javax.crypto.Cipher;

然后是产生密钥对

// Java Code

KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
gen.initialize(1024);
KeyPair pair = gen.generateKeyPair();

Encoder base64Encoder = Base64.getEncoder();
String publicKey = base64Encoder.encodeToString(pair.getPublic().getEncoded());
String privateKey = base64Encoder.encodeToString(pair.getPrivate().getEncoded());

// 这里输出 PKCS#8,所以解密时需要用 PKCS8EncodedKeySpec
System.out.println(pair.getPrivate().getFormat());

产生的 publicKeyprivateKey 都是纯纯的 Base64,没有其他内容(没有标头/标尾等)。

然后是解密过程……

// Java Code

String eCode = "k7M0hD....qvdk=";  // 再次声明,这是仅为演示写的阉割版数据

Decoder base64Decoder = Base64.getDecoder();
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(base64Decoder.decode(privateKey));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");

Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, keyFactory.generatePrivate(keySpec));
byte[] data = cipher.doFinal(base64Decoder.decode(eCode));

System.out.println(new String(data, StandardCharsets.UTF_8));

尾声

写完 Java 是真累,所以,以后的后端示例就用 NodeJS 了 —— 不是 Java 的锅,主要是不想切环境。

下节看点:「注册」的 DEMO,安全传输和保存用户密码。

查看原文

赞 13 收藏 4 评论 1

边城 回答了问题 · 4月13日

js变量定义的函数不会“函数提升"吗?

这个问题有意思。

首先,这里报错确实是因为在声明 b 之前使用了 b,用 const 或者 let 都会这样,不用纠结原因,记住不能在声明前使用就对了。

问题是,就算改成 var,仍然会报错,只不过报的是:b 不是函数。

var 可以让变量声明提升,但声明(更准确地说是 Initialization,初始化)只会给它默认的 undefined,赋值为函数,是在使用(调用 b())之后的事情,所以会报这个错。

关注 4 回答 4

边城 回答了问题 · 4月6日

解决请问这个是vscode什么主题什么字体

字体应该是 @唯一丶 说的那个,Fira Code。

推荐几款我用过的字体

  • JetBrains Mono
  • Cascadia Code
  • Source Code Pro
  • Hasklig
  • Fira Code
  • Consolas

主题,感觉用默认主题「深色+ (默认深色)」挺好的哒

关注 3 回答 2

边城 赞了回答 · 4月4日

闭包 JS基础 编程题

(function() {

    let foo = function(...args) {
        let argsArr = args

        function _add() {
            argsArr.push(...arguments)
            return _add;

        }
        _add.getValue = function() {
            let sum = argsArr.reduce(function(a, b) {
                return a + b
            })
            console.log(sum)

            return sum
        }
        return _add;
    }
    let f1 = foo(1, 2, 3)
    f1.getValue()
    let f2 = foo(1)(2, 3)
    f2.getValue()

    let f3 = foo(1)(2)(3)(4)
    f3.getValue()
}
)()

关注 3 回答 2

边城 回答了问题 · 4月4日

闭包 JS基础 编程题

  • 根据结果是通过 f1.getValue() 来取值,说明 foo() 返回一个对象
  • 根据结果 foo(1)(...) 说明 foo() 返回一个函数,可以理解为参数只有 1 个时返回函数
  • 根据结果 foo(...)(4).getValue() 说明 foo() 在参数只有1 个的时候也可能返回对象

综上,foo() 返回一个带额外属性(.getValue)的函数。

  • 当它作为一个函数使用的时候,其行为和 foo() 是一致的,可以考虑递归调用 foo()
  • 当它作为对象的时候,需要通过 getValue() 返回计算结果,所以这个结果可以在 getValue() 中计算出来,也可以作为一个对象状态(属性)保存起来
  • 由于存在函数连调 foo()(),而且后面的调用会累加前面的结果,所以上面一条中提及的计算结果用状态保存比较好,即可以用于后面的计算,也可以通过 getValue() 返回出去

综上分析:

var foo = function (...args) {
    // 函数,是将当前状态值和参数累加
    const add = (...moreArgs) => foo(add.sum, ...moreArgs);
    
    // sum 状态是当前参数的和
    add.sum = args.reduce((s, n) => s + n, 0);
    
    add.getValue = () => add.sum;
    return add;
}

这个问题的关键点不在于闭包(当然会用到闭包),而是柯里化的过程分析。

不过如果你想深入了解闭包可以看看这篇:还搞不懂闭包算我输(JS 示例)

@circle 的答案比我的简单,更容易理解。他那个是闭包实现的答案,更符合题主要求!把那个答案改写一下,不保存参数列表,直接保存中间结果是这样:

var foo = function (...args) {
    let sum = 0;

    const add = (...more) => {
        sum = more.reduce((s, n) => s + n, sum);
        return add;
    };
    add.getValue = () => sum;

    add(...args);
    return add;
}

关注 3 回答 2

边城 回答了问题 · 4月4日

解决关于 typescript 中的 replace

String.prototype.replace 定义了几个重载,这里涉及到的是

  • replace(s: string | RegExp, r: string)
  • replace(s: string | RegExp, replacer: (substring: string, ...args: any[]) => string

很遗憾它并没有定义这样一个重载:

replace(
    s: string | RegExp,
    r: string | (substring: string, ...args: any[]) => string
)

所以直接给一个 string | (substring: string, ...args: any[]) => string 类型的值作为第二个参数是不行的,应该按情况区分开来

    return syntax.reduce((t, p) => {
        return typeof p[1] === "string"
            ? t.replace(p[0], p[1])  // 这里 p[1] 明确是 string
            : t.replace(p[0], p[1])  // 这里 p[1] 明确是 (...) => string
    }, this.text)

如果把参数从数组里拆出来,VSCode 里可以看到明确的类型提示:

image.png

实际上在写代码的时候,为了运行时少做一次判断,可以这样写

强制使用 any 来绕过类型检查,但是注释里要说清楚这样的原因
    // NOTE 明确 p[1] 是 String.prototype.replace 第 2 个参数可接受的类型,不存在例外
    return syntax.reduce((t, p) =>t.replace(p[0], p[1] as any), this.text)

关注 2 回答 1

边城 回答了问题 · 4月4日

解决不懂就问,用 setTimeout 简单实现 setInterval 的问题

如果把输出写得详细一点,加上时间信息

    let timer;
    const _setInterval = (cb, time) => {
        timer = setTimeout(() => {
            cb()
            _setInterval(cb, time)
        }, time);
    }

    _setInterval(() => {
        const now = Date.now();
        console.log('count', now, now - start);
    }, 1000)

    setTimeout(() => {
        clearTimeout(timer)
    }, 3001);

    const start = Date.now();
    console.log("start: ", start);

会发现输出并不是精确的 1000 毫秒延时

start:  1617522317036
count 1617522318045 1009
count 1617522319051 2015

用原生的 setInterval() 会更准确一些(因为模拟的 cb() 处理业务会花一些时间),但仍然可能会有非常小的误差的。

关注 3 回答 2

边城 赞了回答 · 4月4日

解决Chrome怎么阻止JS删除DOM内容?

实现不了。那么考虑考虑其他的手段,比如说clone一份。然后去重之类的操作。

MutationObserver 能监听改变

image.png

关注 5 回答 4

边城 回答了问题 · 4月3日

合并数组,取相应的key并传参,详细看正文,求解

方法一

@林枫 的方法,只不过里面有一点需要改进:不要在 b.map 循环里每次去取 Object.keys(a[0]),提前一次取好重复用就行。

另外,reduce 很好,效率也不错,不过从语法上来说,可以用 map + Object.fromEntries() 来写逻辑会更清楚

const res = (() => {
    const keys = Object.keys(a[0]);
    return b.map(it => Object.fromEntries(keys.map(key => [key, it[key]])));
})();
console.log(res);

方法二「推荐」

说实在的,处理数据我还是比较喜欢用 Lodash

import _ from "lodash";
const result = (() => {
    const keys = Object.keys(a[0]);
    return b.map(it => _.pick(it, keys));
})();

console.log(result);

方法三

这种方法直接修改原数据,将多余的属性删除掉

const result2 = (() => {
    const keys = new Set(Object.keys(a[0]));
    b.forEach(it =>
        Object.keys(it)
            .filter(key => !keys.has(key))
            .forEach(key => delete it[key])
    );
    return b;
})();

console.log(result2);

对于当前这个问题,由于 b 中的属性固定,可以提前把需要删除的 keys 算好,

const result3 = (() => {
    const removeKeys = (keys => {
        Object.keys(a[0]).forEach(key => keys.delete(key));
        return keys;
    })(new Set(Object.keys(b[0])));

    b.forEach(it => removeKeys.forEach(key => delete it[key]));
    return b;
})();

console.log(result3);

关注 4 回答 2

边城 回答了问题 · 4月3日

解决Chrome怎么阻止JS删除DOM内容?

删除行为是在代码里吧,找到相关的代码,去掉就好了

关注 5 回答 4

边城 回答了问题 · 4月3日

小白求助,express的get请求发送失败什么原因?

app.get('/updateuser/:userid/:userphone/:username:/:userpassword/:useremail'
//                                               ^ 这里多了一个冒号

看看是不是这个冒号的原因

关注 2 回答 1

边城 回答了问题 · 4月2日

做的H5页面,app嵌入页面,H5发布上线后

一个思路:App 的 WebView 是可以通过 JS 跟页面进行交互的,所以页面打开之后等 App 的交互(或主动去请求),得到一个预定的标识,如果是,就继续,否则就跳转到其他页面(比如 App 下载页)。

如果要防止模拟标识,可以采用加密认证的方式。简单的可以使用 HMAC 算法或对称(如 AES 算法)来加密,不过密钥在前端拿得到,并不是很安全。要安全一点可以用非对称加密,就是算起来比较耗资源。

关注 6 回答 6

边城 回答了问题 · 3月30日

vue项目 同一个页面逻辑,在development环境执行成功,production环境执行失败

上面是 min-width: 200px,下面是 min-width: 44px。感觉应该是哪个逻辑没整对,要不就是跟页面宽度有关系。具体是什么原因,猜不出来。

关注 2 回答 1

边城 回答了问题 · 3月28日

解决js怎样通过递归的方式改变树状数据的key值?

关注 5 回答 4

边城 回答了问题 · 3月28日

vue3 setup 怎么获取子组件的ref

感觉跟 defineAsyncComponent 有关系,组件还没加载出来,onMounted 已经执行完了。

关注 4 回答 3

边城 回答了问题 · 3月23日

async 修饰符对微观队列的插入顺序存在特殊影响吗?

关键不在于 async,而在于 await

来看看「理解 JavaScript 的 async/await」

关注 2 回答 1

边城 回答了问题 · 3月21日

解决求一个js的二进制问题

// 如果要用位操作,先抹去第 2 位(就是把 2 变成 0)再加 1 
function func1(n) {
    return (n & 0x01) + 1;
}

// 其实看规律跟奇偶有关,所以
function func2(n) {
    return n % 2 + 1;
}

[0, 1, 2].forEach(n => console.log(n, "->", func1(n), func2(n)));

// 0 -> 1 1
// 1 -> 2 2
// 2 -> 1 1

关注 3 回答 2

边城 赞了回答 · 3月21日

解决请教一道算法题

关注 3 回答 1

边城 回答了问题 · 3月20日

js执行时关于作用域链和let的一些疑惑

简单的说,在调用 show() 的时候才会真正执行 fn,这时候 y 已经声明并赋值了。
如果把 let y = 60 放在 show(fn) 的后面,就会出错,因为执行的时候 y 还未声明。
这跟变量提升没啥关系。

关注 2 回答 2

边城 回答了问题 · 3月17日

解决C# -CefSharp:白色情人节之走丢的Keys Enum? 按键触发事件机制

老弟,搜一下 C# Keys Enum,答案就来了

顺手再给你个网址

关注 2 回答 1