疯狂的技术宅

疯狂的技术宅 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织 blog.yidengxuetang.com/ 编辑
编辑

资深技术宅,爱好广泛,兴趣多变。博览群书,喜欢扯淡。十八种语言样样稀松。想要了解更多,请关注微信公众号:充实的脑洞

个人动态

疯狂的技术宅 收藏了文章 · 今天 10:12

一些小众却有用的 Node.js 包

作者:Scott Robinson

翻译:疯狂的技术宅

原文:https://stackabuse.com/useful...

yargs

yargs 是一个用来处理命令行参数的包,可以帮你处理自行设置的命令行标志和输入的任何类型的数据,其中包括布尔值、浮点数和字符串等。这个包非常简单明了,不需要在项目中编写大量的样板代码。

yargs 能够帮你处理 “用法帮助” 输出,可以轻松地告诉用户在使用你程序时需要输入哪些选项,包括哪些是必选的。

var argv = require('yargs')
    .usage('Usage: $0 -x [num] -y [num]')
    .demand(['x','y'])
    .argv;
 
console.log('Pow(x, y):', Math.pow(argv.x, argv.y));

把上面的代码保存为 index.js,然后在命令行中执行 node index.js -x 3,会看到如下消息:

Usage: index.js -x [num] -y [num]

Options:
  -x                                                                  [required]
  -y                                                                  [required]

Missing required argument: y

yargs 能够提示我们命令行中到底缺少什么参数,而我们只需要简单的调用 .usage().demand() 方法就行了。

toobusy

这是一个非常实用的包。

它轮询 Node 事件循环并跟踪完成请求所需的时间,如果发现延迟时间太长,则 toobusy 会通知你,然后你就可以将 HTTP 503 "Service Unavailable" 状态码返回给客户端。

这种处理是很重要的,因为服务器越忙,请求所等待的时间也就越长。这很快就成为一个很复杂的问题,随着时间的流逝会越来越严重。如果你听之任之的话,那么服务将会崩溃。如果我们能及时的停止一些请求的处理,并返回 HTTP 503,这样的话至少还能处理一些请求。

可以轻松的用 npm 命令安装 toobusy

npm install toobusy

然后把它和类似 Express 的东西集成在一起:

var toobusy = require('toobusy'),
    express = require('express');
    
var app = express();
    
// 如果服务器压力过大将会阻止请求
app.use(function(req, res, next) {
    if (toobusy()) {
        res.send(503, "Too many users!");
    } else {
        next();
    } 
});
  
var server = app.listen(3000);
  
process.on('SIGINT', function() {
    server.close();
    toobusy.shutdown();    // 正常退出
    process.exit();
});

不需要写多少代码,也不用太多的配置就能继承到我们自己的项目中。

chalk

在命令行上很难开发出一个好用的用户界面,因为用于和用户交互的只是命令行窗口。那么你该如何去提示一些重要的信息呢?在输出的文本中添加格式不失为一种好方法。Express 就是一个很典型的例子,从它的输出中,你可以很轻松地快读找到重要的信息。

以下是 chalk 支持的样式列表:

修饰符

  • bold
  • underline
  • dim
  • reset
  • hidden
  • inverse
  • italic(并非所有环境都支持)
  • strikethrough(任何环境下都不支持)

颜色

  • red
  • black
  • green
  • white
  • yellow
  • blue(在 Windows 上会使用较亮的版本,因为普通的蓝色很难辨认)
  • cyan
  • gray
  • magenta

背景颜色

  • bgBlue
  • bgBlack
  • bgRed
  • bgGreen
  • bgCyan
  • bgYellow
  • bgWhite
  • bgMagenta

虽然官方只支持这些颜色,但是任何符合 xterm 标准的终端都可以使用完整的 8 位色代码。

只需要将字符串传给用于着色或格式化的函数就能轻松的格式化这些文本。如果你需要让用户注意到严重错误提示,可以用下面的格式:

var chalk = require('chalk');

var str = chalk.red.bold('ERROR: ') + chalk.bold('Everything just blew up...');
console.log(str);

node-inspector

好用的调试器很难找,尤其是那些带有好用的 GUI 的调试器,node-inspector 为你提供了一个网页 GUI 来帮助调试代码。它有标准调试器的所有功能,例如断点、单步执行、退出代码以及变量检查等,另外还有一些不太常用的功能,但是这些功能非常有用,例如 CPU 和堆分析、网络客户端请求检查以及实时编辑运行代码的功能。

node-inspector

不过 Node Inspector 只与 Chrome 和 Opera 兼容,因为它使用了Blink Developer Tools,并与Node兼容。

一直以来我非常依赖控制台输出进行调试,这会花费了大量的时间。使用 GUI 能够大大的节省调试时间。

terminal-kit

如果你的 Node 程序需要在命令行下支持除简单的文本输入输出之外的其他操作,那么你应该需要 terminal-kit。 terminal-kit 简化了与用户交互的许多东西,使你可以专注于在程序中开发重要的内容。terminal-kit 的主要功能是:

  • 文字样式(很像 chalk
  • 编辑屏幕
  • 进度条
  • 用户输入

有很多适合终端工具包的例子。例如,如果你需要从网上下载一些内容,那么就需要向用户显示进度条。下面的代码用来显示虚拟进度条:

var terminal = require( 'terminal-kit' ).terminal;

var progressBar;
var progress = 0;

function updateProgress() {
    // 产生一个随机的进度值
    progress += Math.random() / 10;
    progressBar.update(progress);
    
    // 检查是否完成
    if (progress >= 1) {
        setTimeout(function() {
            terminal('\n');
            process.exit();
        }, 250);
    }
    else {
        setTimeout(updateProgress, 100 + Math.random() * 500);
    }
}

progressBar = terminal.progressBar({
    width: 80,
    title: 'Downloading file:',
    eta: true,
    percent: true
});

updateProgress();

上面的代码会产生下面这种效果:

terminal-kit进度栏

validator

validator 可以帮你进行一系列常见的字符串验证(例如:电子邮件地址、电话号码、IP地址等)。每当你从用户那里获得输入时,这样的软件包都是必不可少的。用户会犯错误,并会在文本框中输入一些非常奇怪的东西,所以需要一个验证输入的包,避免数据损坏或服务器崩溃。

以下是一些常用的验证器:

  • isEmail(str [, options])
  • isIP(str [, version])
  • isMobilePhone(str, locale)
  • isURL(str [, options])

validator 也提供检测器,可以对输入字符串进行规范化、删除或转义。例如对用户提交的内容进行清理,避免他们输入恶意的 HTML 或 JavaScript 代码。

下面是常用的检测器:

  • blacklist(input, chars)
  • escape(input)
  • normalizeEmail(email [, options])
  • whitelist(input, chars)

    normalizeEmail() 方法它能够确保电子邮件地址都是小写字母,甚至可以删除需要忽略的字符。假设你有电子邮件 abc.def+ghi@163.comnormalizeEmail() 会将其标准化为 abcdefghi@163.com

formidable

formidable 可以帮你处理文件上传的每个步骤,包括 multi-part 解析器、把文件写入磁盘以及错误处理等。这是我最喜欢的一个包,如果你不想重新发明轮子可以试一试。

下面是一个在普通 HTTP 服务器上使用 formidable 的例子,代码是从包本身中给出的示例修改而来的:

var http = require('http');
var util = require('util');
var formidable = require('formidable');
var path = require('path');

var PORT = 8080;

var root = path.join(__dirname, '../');
exports.dir = {
    root    : root,
    lib     : root + '/lib',
    fixture : root + '/test/fixture',
    tmp     : root + '/test/tmp',
};

var server = http.createServer(function(req, res) {
  if (req.url == '/') {
    res.writeHead(200, {'content-type': 'text/html'});
    res.end(
      '<form action="/post" method="post">' +
      '<input type="text" name="title"><br>' +
      '<input type="text" name="data[foo][]"><br>' +
      '<input type="submit" value="Submit">' +
      '</form>'
    );
  } else if (req.url == '/post') {
    var form = new formidable.IncomingForm(),
        fields = [];

    form
      .on('error', function(err) {
        res.writeHead(200, {'content-type': 'text/plain'});
        res.end('error:\n\n' + util.inspect(err));
      })
      .on('field', function(field, value) {
        console.log(field, value);
        fields.push([field, value]);
      })
      .on('end', function() {
        console.log('-> post done');
        res.writeHead(200, {'content-type': 'text/plain'});
        res.end('received fields:\n\n ' + util.inspect(fields));
      });
    form.parse(req);
  } else {
    res.writeHead(404, {'content-type': 'text/plain'});
    res.end('404');
  }
});

server.listen(PORT);

console.log('listening on http://localhost:' + PORT + '/');

shelljs

shelljs是一个能够让你在任何系统上使用通用的Unix命令的包,不管是 Windows、Linux 还是 Mac。这样你就不用再为项目分别编写 bash 和批处理脚本。 shelljs 提供了类似 Unix 的环境,如果你需要编写脚本来运行测试、提交代码或在服务器上启动,则只需编写一次即可。

可以用命令执行类似操作:

require('shelljs/global');

ls('*.js').forEach(function(file) {
    sed('-i', 'BUILD_VERSION', 'v2.0.3', file);
    sed('-i', /.*REMOVE_THIS_LINE.*\n/, '', file);
    sed('-i', /.*REPLACE_THIS_LINE.*\n/, cat('macro.js'), file);
});

执行常见命令:

require('shelljs/global');

mkdir('-p', 'release/data');
cp('-R', 'data/*', 'release/data');

检查可用的二进制文件:

require('shelljs/global');

if (!which('git')) {
    echo('This script requires git!');
    exit(1);
}

甚至可以像在 bash 脚本中一样运行命令:

if (exec('git commit -am "Release commit"').code !== 0) {
  echo('Error: Git commit failed!');
  exit(1);
}

你还知道有哪些好用的包?请在评论留言。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 收藏了文章 · 10月20日

初探 TensorFlow.js

作者:Aral Roca

翻译:疯狂的技术宅

原文:https://aralroca.com/blog/fir...

未经允许严禁转载

在本文中我们来研究怎样用 TensorFlow.js 创建基本的 AI 模型,并使用更复杂的模型实现一些有趣的功能。我只是刚刚开始接触人工智能,尽管不需要深入的人工智能知识,但还是需要搞清楚一些概念才行。

什么是模型?

真实世界是很复杂的,我们需要对其进行简化才能理解,可以用通过模型来进行简化,这种模型有很多种:比如世界地图,或者图表等。

模型

比如要建立一个用来表示房子出租价格与房屋面积关系的模型:首先要收集一些数据:

房间数量价格
3131000
3125000
4235000
4265000
5535000

然后,把这些数据显示在二维图形上,把每个参数(价格,房间数量)都做为 1 个维度:

线性回归

然后我们可以画一条线,并预测 更多房间的房屋出租价格。这种模型被称为线性回归,它是机器学习中最简单的模型之一。不过这个模型还不够好:

  1. 只有 5 个数据,所以不够可靠。
  2. 只有 2 个参数(价格,房间),但是还有更多可能会影响价格的因素:比如地区、装修情况等。

可以通过添加更多的数据来解决第一个问题,比如一百万个。对于第二个问题,可以添加更多维度。在二维图表中可以很容易理解数据并画一条线,在三维图中可以使用平面:

平面

但是当数据的维度是三维呢四维甚至是 1000000 维的时候,大脑就没有办法在图表上对其进行可视化了,但是可以在维度超过三维时通过数学来计算超平面,而神经网络就是为了解决这个问题而生的。

什么是神经网络?

要解什么是神经网络,需要知道什么是神经元。真正的神经元看上去是这样的:

神经元

神经元由以下几部分组成:

  • 树突:这是数据的输入端。
  • 轴突:这是输出端。
  • 突触(未在图中表示):该结构允许一个神经元与另一个神经元之间进行通信。它负责在轴突的神经末梢和附近神经元的树突之间传递电信号。这些突触是学习的关键,因为它们会根据用途增减电活动。

机器学习中的神经元(简化):

机器学习中的神经元

  • Inputs(输入) :输入的参数。
  • Weights(权重) :像突触一样,用来通过调节神经元更好的建立线性回归。
  • Linear function(线性函数) :每个神经元就像一个线性回归函数,对于线性回归模型,只需要一个神经元够了。
  • Activation function(激活函数) :可以用一些激活函数来将输出从标量改为另一个非线性函数。常见的有 sigmoid、RELU 和 tanh。
  • Output(输出) :应用激活函数后的计算输出。

激活函数是非常有用的,神经网络的强大主要归功于它。假如没有任何激活功能,就不可能得到智能的神经元网络。因为尽管你的神经网络中有多个神经元,但神经网络的输出始终将是线性回归。所以需要一些机制来将各个线性回归变形为非线性的来解决非线性问题。通过激活函数可以将这些线性函数转换为非线性函数:

神经网络

训练模型

正如 2D 线性回归的例子所描述的,只需要在图中画一条线就可以预测新数据了。尽管如此,“深度学习”的思想是让我们的神经网络学会画这条线。对于一条简单的线,可以用只有一个神经元的非常简单的神经网络即可,但是对于想要做更复杂事情的模型,例如对两组数据进行分类这种操作,需要通过“训练”使网络学习怎样得到下面的内容:

分类问题

这个过程并不复杂,因为它是二维的。每个模型都用来描述一个世界,但是“训练”的概念在所有模型中都非常相似。第一步是绘制一条随机线,并在算法中通过迭代对其进行改进,每次迭代中过程中修正错误。这种优化算法名为 Gradient Descent(梯度下降)(有着相同概念的算法还有更复杂的 SGD 或 ADAM 等)。每种算法(线性回归,对数回归等)都有不同的成本函数来度量误差,成本函数会始终收敛于某个点。它可以是凸函数或凹函数,但是最终要收敛在 0% 误差的点上。我们的目标就是实现这一点。

凸函数和凹函数

当使用梯度下降算法时,先从其成本函数的某个随机点开始,但是我们不知道它究竟在什么地方!这就像你被蒙着眼睛丢在一座山上,想要下山的话必须一步一步地走到最低点。如果地形是不规则的(例如凹函数),则下降会更加复杂。

在这里不会深入解释“梯度下降”算法,只需要记住这是训练 AI 模型过程中最小化预测误差的优化算法就足够了。这种算法需要大量的时间和 GPU 进行矩阵乘法。通常在第一次执行时很难达到这个收敛点,因此需要修正一些超参数,例如学习率(learning rate)或添加正则化(regularization)。在梯度下降迭代之后,当误差接近 0% 时,会接近收敛点。这样就创建了模型,接下来就能够进行预测了。

进行预测

用 TensorFlow.js 训练模型

TensorFlow.js 提供了一种创建神经网络的简便方法。首先用 trainModel 方法创建一个 LinearModel 类。我们将使用顺序模型。顺序模型是其中一层的输出是下一层的输入的模型,即当模型拓扑是简单的层级结构,没有分支或跳过。在 trainModel 方法内部定义层(我们仅使用一层,因为它足以解决线性回归问题):

import * as tf from '@tensorflow/tfjs';

/**
* 线性模型类
*/
export default class LinearModel {
  /**
 * 训练模型
 */
  async trainModel(xs, ys){
    const layers = tf.layers.dense({
      units: 1, // 输出空间的纬度
      inputShape: [1], // 只有一个参数
    });
    const lossAndOptimizer = {
      loss: 'meanSquaredError',
      optimizer: 'sgd', // 随机梯度下降
    };

    this.linearModel = tf.sequential();
    this.linearModel.add(layers); // 添加一层
    this.linearModel.compile(lossAndOptimizer);

    // 开始模型训练
    await this.linearModel.fit(
      tf.tensor1d(xs),
      tf.tensor1d(ys),
    );
  }

  //...
}

使用这个类进行训练:

const model = new LinearModel()

// xs 与 ys 是 数组成员(x-axis 与 y-axis)
await model.trainModel(xs, ys)

训练结束后就可以开始预测了。

用 TensorFlow.js 进行预测

尽管在训练模型时需要事先定义一些超参数,但是进行一般的预测还是很容易的。通过下面的代码就够了:

import * as tf from '@tensorflow/tfjs';

export default class LinearModel {
  ... //前面训练模型的代码

  predict(value){
    return Array.from(
      this.linearModel
      .predict(tf.tensor2d([value], [1, 1]))
      .dataSync()
    )
  }
}

现在就可以预测了:

const prediction = model.predict(500) // 预测数字 500
console.log(prediction) // => 420.423

用线性模型进行

在 TensorFlow.js 中使用预训练的模型

训练模型是最难的部分。首先对数据进行标准化来进行训练,还需要正确的设定所有超参数等等。对于咱们初学者,可以直接用那些预先训练好的模型。 TensorFlow.js 可以使用很多预训练的模型,还可以导入使用 TensorFlow 或 Keras 创建的外部模型。例如可以直接用 posenet 模型(实时人体姿态评估)做一些有意思的项目:

posenet

📕 这个 Demo 的代码:https://github.com/aralroca/p...

它用起来很容易:

import * as posenet from '@tensorflow-models/posenet'

// 设置一些常数
const imageScaleFactor = 0.5
const outputStride = 16
const flipHorizontal = true
const weight = 0.5

// 加载模型
const net = await posenet.load(weight)

// 进行预测
const poses = await net.estimateSinglePose(
  imageElement,
  imageScaleFactor,
  flipHorizontal,
  outputStride
)

这个 JSON 是 pose 变量:

{
  "score": 0.32371445304906,
  "keypoints": [
    {
      "position": {
        "y": 76.291801452637,
        "x": 253.36747741699
      },
      "part": "nose",
      "score": 0.99539834260941
    },
    {
      "position": {
        "y": 71.10383605957,
        "x": 253.54365539551
      },
      "part": "leftEye",
      "score": 0.98781454563141
    }
    // 后面还有: rightEye, leftEar, rightEar, leftShoulder, rightShoulder
    // leftElbow, rightElbow, leftWrist, rightWrist, leftHip, rightHip,
    // leftKnee, rightKnee, leftAnkle, rightAnkle...
  ]
}

从官方的 demo 可以看得到,用这个模型可以开发出很多有趣的项目。

体感控制🐟的游动

📕 这个项目的源代码: https://github.com/aralroca/f...

导入 Keras 模型

可以把外部模型导入 TensorFlow.js。下面是一个用 Keras 模型(h5格式)进行数字识别的程序。首先要用 tfjs_converter 对模型的格式进行转换。

pip install tensorflowjs

使用转换器:

tensorflowjs_converter --input_format keras keras/cnn.h5 src/assets

最后,把模型导入到 JS 代码中:

// 载入模型
const model = await tf.loadModel('./assets/model.json')

// 准备图片
let img = tf.fromPixels(imageData, 1)
img = img.reshape([1, 28, 28, 1])
img = tf.cast(img, 'float32')

// 进行预测
const output = model.predict(img)

只需要几行代码行就完成了。当然还可以在代码中添加更多的逻辑来实现更多功能,例如可以把数字写在 canvas 上,然后得到其图像来进行预测。

例子

📕 这个项目的源代码: https://github.com/aralroca/M...

为什么要用在浏览器中?

由于设备的不同,在浏览器中训练模型时,效率可能很低。用 TensorFlow.js 利用 WebGL 在后台训练模型,比用 Python 版的 TensorFlow 慢 1.5 ~ 2倍。

但是在 TensorFlow.js 出现之前,没有能直接在浏览器中使用机器学习模型的 API,现在则可以在浏览器应用中离线训练和使用模型。而且预测速度更快,因为不需要向服务器发送请求。另一个好处是成本低,因为所有这些计算都是在客户端完成的。

总结

  • 模型是表示现实世界的一种简化方式,可以使用它来进行预测。
  • 可以用神经网络创建模型。
  • TensorFlow.js 是创建神经网络的简便工具。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 发布了文章 · 10月20日

初探 TensorFlow.js

作者:Aral Roca

翻译:疯狂的技术宅

原文:https://aralroca.com/blog/fir...

未经允许严禁转载

在本文中我们来研究怎样用 TensorFlow.js 创建基本的 AI 模型,并使用更复杂的模型实现一些有趣的功能。我只是刚刚开始接触人工智能,尽管不需要深入的人工智能知识,但还是需要搞清楚一些概念才行。

什么是模型?

真实世界是很复杂的,我们需要对其进行简化才能理解,可以用通过模型来进行简化,这种模型有很多种:比如世界地图,或者图表等。

模型

比如要建立一个用来表示房子出租价格与房屋面积关系的模型:首先要收集一些数据:

房间数量价格
3131000
3125000
4235000
4265000
5535000

然后,把这些数据显示在二维图形上,把每个参数(价格,房间数量)都做为 1 个维度:

线性回归

然后我们可以画一条线,并预测 更多房间的房屋出租价格。这种模型被称为线性回归,它是机器学习中最简单的模型之一。不过这个模型还不够好:

  1. 只有 5 个数据,所以不够可靠。
  2. 只有 2 个参数(价格,房间),但是还有更多可能会影响价格的因素:比如地区、装修情况等。

可以通过添加更多的数据来解决第一个问题,比如一百万个。对于第二个问题,可以添加更多维度。在二维图表中可以很容易理解数据并画一条线,在三维图中可以使用平面:

平面

但是当数据的维度是三维呢四维甚至是 1000000 维的时候,大脑就没有办法在图表上对其进行可视化了,但是可以在维度超过三维时通过数学来计算超平面,而神经网络就是为了解决这个问题而生的。

什么是神经网络?

要解什么是神经网络,需要知道什么是神经元。真正的神经元看上去是这样的:

神经元

神经元由以下几部分组成:

  • 树突:这是数据的输入端。
  • 轴突:这是输出端。
  • 突触(未在图中表示):该结构允许一个神经元与另一个神经元之间进行通信。它负责在轴突的神经末梢和附近神经元的树突之间传递电信号。这些突触是学习的关键,因为它们会根据用途增减电活动。

机器学习中的神经元(简化):

机器学习中的神经元

  • Inputs(输入) :输入的参数。
  • Weights(权重) :像突触一样,用来通过调节神经元更好的建立线性回归。
  • Linear function(线性函数) :每个神经元就像一个线性回归函数,对于线性回归模型,只需要一个神经元够了。
  • Activation function(激活函数) :可以用一些激活函数来将输出从标量改为另一个非线性函数。常见的有 sigmoid、RELU 和 tanh。
  • Output(输出) :应用激活函数后的计算输出。

激活函数是非常有用的,神经网络的强大主要归功于它。假如没有任何激活功能,就不可能得到智能的神经元网络。因为尽管你的神经网络中有多个神经元,但神经网络的输出始终将是线性回归。所以需要一些机制来将各个线性回归变形为非线性的来解决非线性问题。通过激活函数可以将这些线性函数转换为非线性函数:

神经网络

训练模型

正如 2D 线性回归的例子所描述的,只需要在图中画一条线就可以预测新数据了。尽管如此,“深度学习”的思想是让我们的神经网络学会画这条线。对于一条简单的线,可以用只有一个神经元的非常简单的神经网络即可,但是对于想要做更复杂事情的模型,例如对两组数据进行分类这种操作,需要通过“训练”使网络学习怎样得到下面的内容:

分类问题

这个过程并不复杂,因为它是二维的。每个模型都用来描述一个世界,但是“训练”的概念在所有模型中都非常相似。第一步是绘制一条随机线,并在算法中通过迭代对其进行改进,每次迭代中过程中修正错误。这种优化算法名为 Gradient Descent(梯度下降)(有着相同概念的算法还有更复杂的 SGD 或 ADAM 等)。每种算法(线性回归,对数回归等)都有不同的成本函数来度量误差,成本函数会始终收敛于某个点。它可以是凸函数或凹函数,但是最终要收敛在 0% 误差的点上。我们的目标就是实现这一点。

凸函数和凹函数

当使用梯度下降算法时,先从其成本函数的某个随机点开始,但是我们不知道它究竟在什么地方!这就像你被蒙着眼睛丢在一座山上,想要下山的话必须一步一步地走到最低点。如果地形是不规则的(例如凹函数),则下降会更加复杂。

在这里不会深入解释“梯度下降”算法,只需要记住这是训练 AI 模型过程中最小化预测误差的优化算法就足够了。这种算法需要大量的时间和 GPU 进行矩阵乘法。通常在第一次执行时很难达到这个收敛点,因此需要修正一些超参数,例如学习率(learning rate)或添加正则化(regularization)。在梯度下降迭代之后,当误差接近 0% 时,会接近收敛点。这样就创建了模型,接下来就能够进行预测了。

进行预测

用 TensorFlow.js 训练模型

TensorFlow.js 提供了一种创建神经网络的简便方法。首先用 trainModel 方法创建一个 LinearModel 类。我们将使用顺序模型。顺序模型是其中一层的输出是下一层的输入的模型,即当模型拓扑是简单的层级结构,没有分支或跳过。在 trainModel 方法内部定义层(我们仅使用一层,因为它足以解决线性回归问题):

import * as tf from '@tensorflow/tfjs';

/**
* 线性模型类
*/
export default class LinearModel {
  /**
 * 训练模型
 */
  async trainModel(xs, ys){
    const layers = tf.layers.dense({
      units: 1, // 输出空间的纬度
      inputShape: [1], // 只有一个参数
    });
    const lossAndOptimizer = {
      loss: 'meanSquaredError',
      optimizer: 'sgd', // 随机梯度下降
    };

    this.linearModel = tf.sequential();
    this.linearModel.add(layers); // 添加一层
    this.linearModel.compile(lossAndOptimizer);

    // 开始模型训练
    await this.linearModel.fit(
      tf.tensor1d(xs),
      tf.tensor1d(ys),
    );
  }

  //...
}

使用这个类进行训练:

const model = new LinearModel()

// xs 与 ys 是 数组成员(x-axis 与 y-axis)
await model.trainModel(xs, ys)

训练结束后就可以开始预测了。

用 TensorFlow.js 进行预测

尽管在训练模型时需要事先定义一些超参数,但是进行一般的预测还是很容易的。通过下面的代码就够了:

import * as tf from '@tensorflow/tfjs';

export default class LinearModel {
  ... //前面训练模型的代码

  predict(value){
    return Array.from(
      this.linearModel
      .predict(tf.tensor2d([value], [1, 1]))
      .dataSync()
    )
  }
}

现在就可以预测了:

const prediction = model.predict(500) // 预测数字 500
console.log(prediction) // => 420.423

用线性模型进行

在 TensorFlow.js 中使用预训练的模型

训练模型是最难的部分。首先对数据进行标准化来进行训练,还需要正确的设定所有超参数等等。对于咱们初学者,可以直接用那些预先训练好的模型。 TensorFlow.js 可以使用很多预训练的模型,还可以导入使用 TensorFlow 或 Keras 创建的外部模型。例如可以直接用 posenet 模型(实时人体姿态评估)做一些有意思的项目:

posenet

📕 这个 Demo 的代码:https://github.com/aralroca/p...

它用起来很容易:

import * as posenet from '@tensorflow-models/posenet'

// 设置一些常数
const imageScaleFactor = 0.5
const outputStride = 16
const flipHorizontal = true
const weight = 0.5

// 加载模型
const net = await posenet.load(weight)

// 进行预测
const poses = await net.estimateSinglePose(
  imageElement,
  imageScaleFactor,
  flipHorizontal,
  outputStride
)

这个 JSON 是 pose 变量:

{
  "score": 0.32371445304906,
  "keypoints": [
    {
      "position": {
        "y": 76.291801452637,
        "x": 253.36747741699
      },
      "part": "nose",
      "score": 0.99539834260941
    },
    {
      "position": {
        "y": 71.10383605957,
        "x": 253.54365539551
      },
      "part": "leftEye",
      "score": 0.98781454563141
    }
    // 后面还有: rightEye, leftEar, rightEar, leftShoulder, rightShoulder
    // leftElbow, rightElbow, leftWrist, rightWrist, leftHip, rightHip,
    // leftKnee, rightKnee, leftAnkle, rightAnkle...
  ]
}

从官方的 demo 可以看得到,用这个模型可以开发出很多有趣的项目。

体感控制🐟的游动

📕 这个项目的源代码: https://github.com/aralroca/f...

导入 Keras 模型

可以把外部模型导入 TensorFlow.js。下面是一个用 Keras 模型(h5格式)进行数字识别的程序。首先要用 tfjs_converter 对模型的格式进行转换。

pip install tensorflowjs

使用转换器:

tensorflowjs_converter --input_format keras keras/cnn.h5 src/assets

最后,把模型导入到 JS 代码中:

// 载入模型
const model = await tf.loadModel('./assets/model.json')

// 准备图片
let img = tf.fromPixels(imageData, 1)
img = img.reshape([1, 28, 28, 1])
img = tf.cast(img, 'float32')

// 进行预测
const output = model.predict(img)

只需要几行代码行就完成了。当然还可以在代码中添加更多的逻辑来实现更多功能,例如可以把数字写在 canvas 上,然后得到其图像来进行预测。

例子

📕 这个项目的源代码: https://github.com/aralroca/M...

为什么要用在浏览器中?

由于设备的不同,在浏览器中训练模型时,效率可能很低。用 TensorFlow.js 利用 WebGL 在后台训练模型,比用 Python 版的 TensorFlow 慢 1.5 ~ 2倍。

但是在 TensorFlow.js 出现之前,没有能直接在浏览器中使用机器学习模型的 API,现在则可以在浏览器应用中离线训练和使用模型。而且预测速度更快,因为不需要向服务器发送请求。另一个好处是成本低,因为所有这些计算都是在客户端完成的。

总结

  • 模型是表示现实世界的一种简化方式,可以使用它来进行预测。
  • 可以用神经网络创建模型。
  • TensorFlow.js 是创建神经网络的简便工具。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 10 收藏 7 评论 0

疯狂的技术宅 收藏了文章 · 10月19日

用回溯算法求解数独问题

作者:Christina

翻译:疯狂的技术宅

原文:https://dev.to/christinamcmah...

未经允许严禁转载

前几天我们在《浅析常见的算法范式》中讨论了一些常见的算法范式,但是还留下了回溯算法没有解决。本文来研究回溯算法。

回溯是通过逐步构建解决方案来解决递归问题的算法。通常回溯从可能的解决方案开始,如果它不起作用,则需要回溯并尝试另一种解决方案,直到找到可行的解决方案为止。回溯在解决 CSP(约束满足问题)时特别有用,例如填字游戏、迷宫和数独等。

通常回溯算法可用于以下三种类型的问题:

  1. 需要找到可行解决方案的决策问题
  2. 需要找到最佳解决方案的优化问题
  3. 需要找到一组可行解决方案的列举问题

在本文中,我将通过解决数独问题来演示回溯策略。

解决数独问题

针对此类问题的回溯算法会尝试在每个空格中列举所有的数字,直到问题被解决为止。先从 main 方法开始:

function sudokuSolver(matrix) {
    if (solveSudoku(matrix) === true) {
        return matrix;
    }
    return '无解';
}

接下来看一看算法的主要逻辑:

const UNASSIGNED = 0;

function solveSudoku(matrix) {
    let row = 0;
    let col = 0;
    let checkBlankSpaces = false;

    // 验证数独是否已解决,如果尚未解决,则获取下一个空格的位置
    for (row = 0; row < matrix.length; row++) {
        for (col = 0; col < matrix[row].length; col++) {
            if (matrix[row][col] === UNASSIGNED) {
                checkBlankSpaces = true;
                break;
            }
        }
        if (checkBlankSpaces === true) {
            break;
        }
    }
    //当没有空格时则意味着已经解决
    if (checkBlankSpaces === false) {
        return true;
    }

    // 尝试用正确的数字填充空格
    for (let num = 1; num <= 9; num++) {
        // isSafe 用于检查在行、列或 3x3 的格子中是否已经存在了数字 num(代码实现在后面)
        if (isSafe(matrix, row, col, num)) {
            matrix[row][col] = num;

            if (solveSudoku(matrix)) {
                return true;
            }
            // 如果 num 所在的位置不合适,需要再次标记为“空格”,然后用不同的 num 回溯
            matrix[row][col] = UNASSIGNED;
        }
    }
    return false;
}

接下来看辅助函数的实现:

function isSafe(matrix, row, col, num) {
    return (
        !usedInRow(matrix, row, num) && 
        !usedInCol(matrix, col, num) && 
        !usedInBox(matrix, row - (row % 3), col - (col % 3), num)
    );
}

function usedInRow(matrix, row, num) {
    for (let col = 0; col < matrix.length; col++) {
        if (matrix[row][col] === num) {
            return true;
        }
    }
    return false;
}

function usedInCol(matrix, col, num) {
    for (let row = 0; row < matrix.length; row++) {
        if (matrix[row][col] === num) {
            return true;
        }
    }
    return false;
}

function usedInBox(matrix, boxStartRow, boxStartCol, num) {
    for (let row = 0; row < 3; row++) {
        for (let col = 0; col < 3; col++) {
            if (matrix[row + boxStartRow][col + boxStartCol] === num) {
                return true;
            }
        }
    }
    return false;
}

最后对算法进行测试:

const sudokuGrid = [
    [5, 3, 0, 0, 7, 0, 0, 0, 0], 
    [6, 0, 0, 1, 9, 5, 0, 0, 0],
    [0, 9, 8, 0, 0, 0, 0, 6, 0],
    [8, 0, 0, 0, 6, 0, 0, 0, 3],
    [4, 0, 0, 8, 0, 3, 0, 0, 1],
    [7, 0, 0, 0, 2, 0, 0, 0, 6],
    [0, 6, 0, 0, 0, 0, 2, 8, 0],
    [0, 0, 0, 4, 1, 9, 0, 0, 5],
    [0, 0, 0, 0, 8, 0, 0, 7, 9]
];

console.log(sudokuSolver(sudokuGrid));

以下是通过回溯法求解数独问题的模拟动画:

sudoku being solved by backtracking

希望本文能帮你理解回溯算法,以后我门还会再讨论更多有趣的算法。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 发布了文章 · 10月19日

用回溯算法求解数独问题

作者:Christina

翻译:疯狂的技术宅

原文:https://dev.to/christinamcmah...

未经允许严禁转载

前几天我们在《浅析常见的算法范式》中讨论了一些常见的算法范式,但是还留下了回溯算法没有解决。本文来研究回溯算法。

回溯是通过逐步构建解决方案来解决递归问题的算法。通常回溯从可能的解决方案开始,如果它不起作用,则需要回溯并尝试另一种解决方案,直到找到可行的解决方案为止。回溯在解决 CSP(约束满足问题)时特别有用,例如填字游戏、迷宫和数独等。

通常回溯算法可用于以下三种类型的问题:

  1. 需要找到可行解决方案的决策问题
  2. 需要找到最佳解决方案的优化问题
  3. 需要找到一组可行解决方案的列举问题

在本文中,我将通过解决数独问题来演示回溯策略。

解决数独问题

针对此类问题的回溯算法会尝试在每个空格中列举所有的数字,直到问题被解决为止。先从 main 方法开始:

function sudokuSolver(matrix) {
    if (solveSudoku(matrix) === true) {
        return matrix;
    }
    return '无解';
}

接下来看一看算法的主要逻辑:

const UNASSIGNED = 0;

function solveSudoku(matrix) {
    let row = 0;
    let col = 0;
    let checkBlankSpaces = false;

    // 验证数独是否已解决,如果尚未解决,则获取下一个空格的位置
    for (row = 0; row < matrix.length; row++) {
        for (col = 0; col < matrix[row].length; col++) {
            if (matrix[row][col] === UNASSIGNED) {
                checkBlankSpaces = true;
                break;
            }
        }
        if (checkBlankSpaces === true) {
            break;
        }
    }
    //当没有空格时则意味着已经解决
    if (checkBlankSpaces === false) {
        return true;
    }

    // 尝试用正确的数字填充空格
    for (let num = 1; num <= 9; num++) {
        // isSafe 用于检查在行、列或 3x3 的格子中是否已经存在了数字 num(代码实现在后面)
        if (isSafe(matrix, row, col, num)) {
            matrix[row][col] = num;

            if (solveSudoku(matrix)) {
                return true;
            }
            // 如果 num 所在的位置不合适,需要再次标记为“空格”,然后用不同的 num 回溯
            matrix[row][col] = UNASSIGNED;
        }
    }
    return false;
}

接下来看辅助函数的实现:

function isSafe(matrix, row, col, num) {
    return (
        !usedInRow(matrix, row, num) && 
        !usedInCol(matrix, col, num) && 
        !usedInBox(matrix, row - (row % 3), col - (col % 3), num)
    );
}

function usedInRow(matrix, row, num) {
    for (let col = 0; col < matrix.length; col++) {
        if (matrix[row][col] === num) {
            return true;
        }
    }
    return false;
}

function usedInCol(matrix, col, num) {
    for (let row = 0; row < matrix.length; row++) {
        if (matrix[row][col] === num) {
            return true;
        }
    }
    return false;
}

function usedInBox(matrix, boxStartRow, boxStartCol, num) {
    for (let row = 0; row < 3; row++) {
        for (let col = 0; col < 3; col++) {
            if (matrix[row + boxStartRow][col + boxStartCol] === num) {
                return true;
            }
        }
    }
    return false;
}

最后对算法进行测试:

const sudokuGrid = [
    [5, 3, 0, 0, 7, 0, 0, 0, 0], 
    [6, 0, 0, 1, 9, 5, 0, 0, 0],
    [0, 9, 8, 0, 0, 0, 0, 6, 0],
    [8, 0, 0, 0, 6, 0, 0, 0, 3],
    [4, 0, 0, 8, 0, 3, 0, 0, 1],
    [7, 0, 0, 0, 2, 0, 0, 0, 6],
    [0, 6, 0, 0, 0, 0, 2, 8, 0],
    [0, 0, 0, 4, 1, 9, 0, 0, 5],
    [0, 0, 0, 0, 8, 0, 0, 7, 9]
];

console.log(sudokuSolver(sudokuGrid));

以下是通过回溯法求解数独问题的模拟动画:

sudoku being solved by backtracking

希望本文能帮你理解回溯算法,以后我门还会再讨论更多有趣的算法。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 8 收藏 7 评论 0

疯狂的技术宅 发布了文章 · 10月17日

8个写JavaScript代码的小技巧

作者:Orkhan Jafarov

翻译:疯狂的技术宅

原文:https://dev.to/gigantz/9-java...

未经允许严禁转载

1. 生成指定区间内的数字

有时候需要创建在某个数字范围内的数组。比如在选择生日时。以下是最简单的实现方法。

let start = 1900,
    end = 2000;
[...new Array(end + 1).keys()].slice(start);
// [ 1900, 1901, ..., 2000]

// 也可以这样,但是大范围结果不稳定
Array.from({ length: end - start + 1 }, (_, i) => start + i);

2. 把值数组中的值作为函数的参数

有时候我们需要先把值放到数组中,然后再作为函数的参数进行传递。使用 ES6 语法可以只凭借扩展运算符(...)就可以把值从数组中提取出来: [arg1,arg2] => (arg1,arg2)

const parts = {
  first: [0, 2],
  second: [1, 3],
};

["Hello", "World", "JS", "Tricks"].slice(...parts.second);
// ["World", "JS", "Tricks"]

这个技巧在任何函数中都适用,请继续看第 3 条。

3. 把值数组中的值作为 Math 方法的参数

当需要在数组中找到数字的最大或最小值时,可以像下面这样做:

// 查到元素中的 y 位置最大的那一个值
const elementsHeight =  [...document.body.children].map(
  el => el.getBoundingClientRect().y
);
Math.max(...elementsHeight);
// 输出最大的那个值

const numbers = [100, 100, -1000, 2000, -3000, 40000];
Math.min(...numbers);
// -3000

4. 展平嵌套数组

Array 有一个名为 Array.flat 的方法,它需要一个表示深度的参数来展平嵌套数组(默认值为 1)。但是如果你不知道深度怎么办,这时候只需要将 Infinity 作为参数即可。另外还有一个很好用的 flatMap 方法。

const arrays = [[10], 50, [100, [2000, 3000, [40000]]]];
arrays.flat(Infinity);
// [ 10, 50, 100, 2000, 3000, 40000 ]

5. 防止代码崩溃

如果在代码中存在不可预测的行为,后果是难以预料的,所以需要对其进行处理。

例如当你想要获取的属性为 undefinednull 时,会得到 TypeError 错误。

如果你的项目代码不支持可选链( optional chaining)的话,可以这样做:

const found = [{ name: "Alex" }].find(i => i.name === 'Jim');
console.log(found.name);
// TypeError: Cannot read property 'name' of undefined

可以这样避免

const found = [{ name: "Alex" }].find(i => i.name === 'Jim') || {};
console.log(found.name);
// undefined

不过这要视情况而定,对于小规模的代码进行处理完全没什么问题。不需要太多代码就可以处理它。

6. 传参的好方法

在 ES6 中可以把 模板字面量(Template literal) 当作是不带括号的函数的参数。这在进行格式化或转换文本的时非常好用。

const makeList = (raw) =>
  raw
    .join()
    .trim()
    .split("\n")
    .map((s, i) => `${i + 1}. ${s}`)
    .join("\n");

makeList`
Hello, World
Hello, World
`;

// 1. Hello
// 2. World

7. 像变戏法一样交换变量的值

通过解构赋值语法,可以轻松地交换变量。

let a = "hello";
let b = "world";

// 错误 ❌
a = b
b = a
// { a: 'world', b: 'world' }

// 正确 ✅
[a, b] = [b, a];
// { a: 'world', b: 'hello' }

8. 遮蔽字符串

某些时候我们需要遮蔽字符串的一部分,当然不只是对密码做这种操作。下面代码中通过 substr(-3) 得到字符串的一部分,即从字符串末尾开始往前 3 个字符,然后再用你喜欢的字符填充剩余的位置(比如说用 *

const password = "hackme";
password.substr(-3).padStart(password.length, "*");
// ***kme

结语

在编码时还需要保持代码整洁,平时注意积累在编码时所使到的技巧,并关注 JavaScript 的新增特性。


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


173382ede7319973.gif

查看原文

赞 13 收藏 11 评论 0

疯狂的技术宅 收藏了文章 · 10月16日

8个写JavaScript代码的小技巧

作者:Orkhan Jafarov

翻译:疯狂的技术宅

原文:https://dev.to/gigantz/9-java...

未经允许严禁转载

1. 生成指定区间内的数字

有时候需要创建在某个数字范围内的数组。比如在选择生日时。以下是最简单的实现方法。

let start = 1900,
    end = 2000;
[...new Array(end + 1).keys()].slice(start);
// [ 1900, 1901, ..., 2000]

// 也可以这样,但是大范围结果不稳定
Array.from({ length: end - start + 1 }, (_, i) => start + i);

2. 把值数组中的值作为函数的参数

有时候我们需要先把值放到数组中,然后再作为函数的参数进行传递。使用 ES6 语法可以只凭借扩展运算符(...)就可以把值从数组中提取出来: [arg1,arg2] => (arg1,arg2)

const parts = {
  first: [0, 2],
  second: [1, 3],
};

["Hello", "World", "JS", "Tricks"].slice(...parts.second);
// ["World", "JS", "Tricks"]

这个技巧在任何函数中都适用,请继续看第 3 条。

3. 把值数组中的值作为 Math 方法的参数

当需要在数组中找到数字的最大或最小值时,可以像下面这样做:

// 查到元素中的 y 位置最大的那一个值
const elementsHeight =  [...document.body.children].map(
  el => el.getBoundingClientRect().y
);
Math.max(...elementsHeight);
// 输出最大的那个值

const numbers = [100, 100, -1000, 2000, -3000, 40000];
Math.min(...numbers);
// -3000

4. 展平嵌套数组

Array 有一个名为 Array.flat 的方法,它需要一个表示深度的参数来展平嵌套数组(默认值为 1)。但是如果你不知道深度怎么办,这时候只需要将 Infinity 作为参数即可。另外还有一个很好用的 flatMap 方法。

const arrays = [[10], 50, [100, [2000, 3000, [40000]]]];
arrays.flat(Infinity);
// [ 10, 50, 100, 2000, 3000, 40000 ]

5. 防止代码崩溃

如果在代码中存在不可预测的行为,后果是难以预料的,所以需要对其进行处理。

例如当你想要获取的属性为 undefinednull 时,会得到 TypeError 错误。

如果你的项目代码不支持可选链( optional chaining)的话,可以这样做:

const found = [{ name: "Alex" }].find(i => i.name === 'Jim');
console.log(found.name);
// TypeError: Cannot read property 'name' of undefined

可以这样避免

const found = [{ name: "Alex" }].find(i => i.name === 'Jim') || {};
console.log(found.name);
// undefined

不过这要视情况而定,对于小规模的代码进行处理完全没什么问题。不需要太多代码就可以处理它。

6. 传参的好方法

在 ES6 中可以把 模板字面量(Template literal) 当作是不带括号的函数的参数。这在进行格式化或转换文本的时非常好用。

const makeList = (raw) =>
  raw
    .join()
    .trim()
    .split("\n")
    .map((s, i) => `${i + 1}. ${s}`)
    .join("\n");

makeList`
Hello, World
Hello, World
`;

// 1. Hello
// 2. World

7. 像变戏法一样交换变量的值

通过解构赋值语法,可以轻松地交换变量。

let a = "hello";
let b = "world";

// 错误 ❌
a = b
b = a
// { a: 'world', b: 'world' }

// 正确 ✅
[a, b] = [b, a];
// { a: 'world', b: 'hello' }

8. 遮蔽字符串

某些时候我们需要遮蔽字符串的一部分,当然不只是对密码做这种操作。下面代码中通过 substr(-3) 得到字符串的一部分,即从字符串末尾开始往前 3 个字符,然后再用你喜欢的字符填充剩余的位置(比如说用 *

const password = "hackme";
password.substr(-3).padStart(password.length, "*");
// ***kme

结语

在编码时还需要保持代码整洁,平时注意积累在编码时所使到的技巧,并关注 JavaScript 的新增特性。


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


173382ede7319973.gif

查看原文

疯狂的技术宅 收藏了文章 · 10月15日

检查 JavaScript 变量是否为数字的几种方式

作者:Marcus Sanatan

翻译:疯狂的技术宅

原文:https://stackabuse.com/javasc...

未经允许严禁转载

介绍

JavaScript 是一种动态类型的语言,这意味着解释器是在运行时确定变量类型的。这允许我们可以用同一变量中存储不同类型的数据。但是如果没有文档和保持一致性,在使用代码时,我们很有可能并不知道变量究竟是哪种类型。

当我们打算对数字进行操作时,如果对字符串或数组进行操作会带来奇怪的结果。在本文中,我们将研究可以帮我们确定所用的变量是否为数字的各种函数。

字符串形式的数字例如 "100" 不应该被处理,同时在 JavaScript中 NaNInfinity-Infinity 之类的特殊值也都是数字,不过我们将忽略这些值。

根据这些要求,最好使用 Number 对象内置 isFinite() 函数。但是有时候我们也会使用其他函数,例如 Number.isNaN()typeof() 等。

首先创建一些测试变量:

let intVar = 2;
let floatVar = 10.5;
let stringVar = '4';
let nanVar = NaN;
let infinityVar = Infinity;
let nullVar = null;
let undefinedVar = undefined;

使用 Number.isFinite() 函数

Number.isFinite() 用来函数检查变量是否为数字,但也用来检查其是否为某些特殊值。它在遇到 NaN, Infinity 或者 -Infinity 时会返回 false

接下来在上面定义的变量上进行测试:

> Number.isFinite(intVar);
true
> Number.isFinite(floatVar);
true
> Number.isFinite(stringVar);
false
> Number.isFinite(nanVar);
false
> Number.isFinite(infinityVar);
false
> Number.isFinite(nullVar);
false
> Number.isFinite(undefined);
false

结果令人满意。特殊的数字值以及所有非数字类型的变量都将会被忽略。如果想要检查某个变量是否为数字, Number.isFinite() 函数是最好的选择。

使用 Number.isNaN() 函数

标准的 Number 对象具有 isNaN() 方法。用来判断传入的参数值是否为 NaN。由于我们要检查变量是否为数字,所以需要在检查中要使用非运算符 !

现在看看通过非运算符加 Number.isNaN() 函数能否只过滤数字:

> !Number.isNaN(intVar);
true
> !Number.isNaN(floatVar);
true
> !Number.isNaN(stringVar);
true # 判断错误
> !Number.isNaN(nanVar);
false
> !Number.isNaN(infinityVar);
true # 判断错误
> !Number.isNaN(nullVar);
true # 判断错误
> !Number.isNaN(undefinedVar);
true # 判断错误

这种方法相当宽松,因为它接受的值根本不是数字。这种方法最适合在你知道自己的值是数字并且要检查它是否为 NaN 值的情况下,并不适合常规数字的。

使用 typeof() 函数

typeof() 函数是一个全局函数,它的参数可以接受变量或值,并返回其类型的字符串表示形式。 JavaScript 共有 9 种类型:

  • undefined
  • boolean
  • number
  • string
  • bigint
  • symbol
  • object
  • null (typeof() 显示为对象)
  • function (对象的一种特殊类型)

为了验证变量是否为数字,我们只需要检查 typeof() 返回的值是否为 "number"。让我们尝试一下测试变量:

> typeof(intVar) == 'number';
true
> typeof(floatVar) == 'number';
true
> typeof(stringVar) == 'number';
false
> typeof(nanVar) == 'number';
true # 判断错误
> typeof(infinityVar) == 'number';
true # 判断错误
> typeof(nullVar) == 'number';
false
> typeof(undefined) == 'number';
false

typeof() 函数比 Number.isNaN() 好得多。它可以正确的判断 nullundefined 不是数字。但如果是 NaNInfinity,它会返回 true。

尽管从技术角度上来说这是正确的,但 NaNInfinity 是特殊的数字值,我们在大多数情况下都会忽略它们。

总结

本文研究了如何检查 JavaScript 中的变量是否为数字。

  • 只有在我们知道自己的变量是一个数字,并且需要验证它是否为 NaN 时,Number.isNaN() 函数才适用。
  • 如果你的代码需要处理 NaNInfinity-Infinity 及其他数字时,则 typeof() 函数是适用的。
  • Number.isFinite() 方法能够处理特殊数字,并且最适合我们的要求。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 发布了文章 · 10月15日

检查 JavaScript 变量是否为数字的几种方式

作者:Marcus Sanatan

翻译:疯狂的技术宅

原文:https://stackabuse.com/javasc...

未经允许严禁转载

介绍

JavaScript 是一种动态类型的语言,这意味着解释器是在运行时确定变量类型的。这允许我们可以用同一变量中存储不同类型的数据。但是如果没有文档和保持一致性,在使用代码时,我们很有可能并不知道变量究竟是哪种类型。

当我们打算对数字进行操作时,如果对字符串或数组进行操作会带来奇怪的结果。在本文中,我们将研究可以帮我们确定所用的变量是否为数字的各种函数。

字符串形式的数字例如 "100" 不应该被处理,同时在 JavaScript中 NaNInfinity-Infinity 之类的特殊值也都是数字,不过我们将忽略这些值。

根据这些要求,最好使用 Number 对象内置 isFinite() 函数。但是有时候我们也会使用其他函数,例如 Number.isNaN()typeof() 等。

首先创建一些测试变量:

let intVar = 2;
let floatVar = 10.5;
let stringVar = '4';
let nanVar = NaN;
let infinityVar = Infinity;
let nullVar = null;
let undefinedVar = undefined;

使用 Number.isFinite() 函数

Number.isFinite() 用来函数检查变量是否为数字,但也用来检查其是否为某些特殊值。它在遇到 NaN, Infinity 或者 -Infinity 时会返回 false

接下来在上面定义的变量上进行测试:

> Number.isFinite(intVar);
true
> Number.isFinite(floatVar);
true
> Number.isFinite(stringVar);
false
> Number.isFinite(nanVar);
false
> Number.isFinite(infinityVar);
false
> Number.isFinite(nullVar);
false
> Number.isFinite(undefined);
false

结果令人满意。特殊的数字值以及所有非数字类型的变量都将会被忽略。如果想要检查某个变量是否为数字, Number.isFinite() 函数是最好的选择。

使用 Number.isNaN() 函数

标准的 Number 对象具有 isNaN() 方法。用来判断传入的参数值是否为 NaN。由于我们要检查变量是否为数字,所以需要在检查中要使用非运算符 !

现在看看通过非运算符加 Number.isNaN() 函数能否只过滤数字:

> !Number.isNaN(intVar);
true
> !Number.isNaN(floatVar);
true
> !Number.isNaN(stringVar);
true # 判断错误
> !Number.isNaN(nanVar);
false
> !Number.isNaN(infinityVar);
true # 判断错误
> !Number.isNaN(nullVar);
true # 判断错误
> !Number.isNaN(undefinedVar);
true # 判断错误

这种方法相当宽松,因为它接受的值根本不是数字。这种方法最适合在你知道自己的值是数字并且要检查它是否为 NaN 值的情况下,并不适合常规数字的。

使用 typeof() 函数

typeof() 函数是一个全局函数,它的参数可以接受变量或值,并返回其类型的字符串表示形式。 JavaScript 共有 9 种类型:

  • undefined
  • boolean
  • number
  • string
  • bigint
  • symbol
  • object
  • null (typeof() 显示为对象)
  • function (对象的一种特殊类型)

为了验证变量是否为数字,我们只需要检查 typeof() 返回的值是否为 "number"。让我们尝试一下测试变量:

> typeof(intVar) == 'number';
true
> typeof(floatVar) == 'number';
true
> typeof(stringVar) == 'number';
false
> typeof(nanVar) == 'number';
true # 判断错误
> typeof(infinityVar) == 'number';
true # 判断错误
> typeof(nullVar) == 'number';
false
> typeof(undefined) == 'number';
false

typeof() 函数比 Number.isNaN() 好得多。它可以正确的判断 nullundefined 不是数字。但如果是 NaNInfinity,它会返回 true。

尽管从技术角度上来说这是正确的,但 NaNInfinity 是特殊的数字值,我们在大多数情况下都会忽略它们。

总结

本文研究了如何检查 JavaScript 中的变量是否为数字。

  • 只有在我们知道自己的变量是一个数字,并且需要验证它是否为 NaN 时,Number.isNaN() 函数才适用。
  • 如果你的代码需要处理 NaNInfinity-Infinity 及其他数字时,则 typeof() 函数是适用的。
  • Number.isFinite() 方法能够处理特殊数字,并且最适合我们的要求。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 9 收藏 8 评论 0

疯狂的技术宅 收藏了文章 · 10月14日

浅析常见的算法范式

作者:Aral Roca

翻译:疯狂的技术宅

原文:https://dev.to/christinamcmah...

未经允许严禁转载

首先明确三个概念:

算法: 按步骤解决问题的过程。

范式: 思考问题的模式。

算法范式: 为问题构建高效解决方案的常规方法。

本文讨论一些常用的算法范式,例如

  • 分治算法
  • 动态规划
  • 贪婪算法
  • 回溯算法

分治法

在排序算法中,合并和快速排序这两种算法的共同点就是分而治之的算法。

分而治之是一种常见的算法设计,它的思路是把问题分解为与原始问题相似的较小子问题。通常以递归方式解决子问题,并结合子问题的解决方案来解决原始问题。

分治法的逻辑可以分为三个步骤:

  1. 将原始问题划分为较小的子问题。
  2. 通过递归解决子问题,解决完毕之后返回子问题的解决方案。
  3. 将子问题的解决方案合并为原始问题的解决方案。

分治法的例子:二叉搜索

下面是用分治实现的二叉搜索。

function binarySearchRecursive(array, value, low, high) {
    if (low <= high) {
        const mid = Math.floor((low + high) / 2);
        const element = array[mid];

        if (element < value) {
            return binarySearchRecursive(array, value, mid + 1, high);
        } else if (element > value) {
            return binarySearchRecursive(array, value, low, mid - 1);
        } else {
            return mid;
        }
    }
    return null;
}

export function binarySearch(array, value) {
    const sortedArray = quickSort(array);
    const low = 0;
    const high = sortedArray.length - 1;

    return binarySearchRecursive(array, value, low, high);
}

请注意,上面的 binarySearch 函数是供他人调用的,而 binarySearchRecursive 是实现分治法的地方。

动态规划法

动态规划是一种优化技术,用于通过把复杂问题分解为较小的子问题来解决。看上去很像是分治法,但动态规划不是把问题分解为独立的子问题然后再组合在一起,而是只把问题分解为独立的子问题。

算法逻辑分为三个步骤:

  1. 定义子问题。
  2. 重复解决子问题。
  3. 识别并解决基本问题。

动态规划案例:最小硬币找零问题

这是一个名为为硬币找零问题的常见面试题。硬币找零问题是给定找零的金额,找出可以用多少特定数量的硬币来找零的方式。最小硬币找零问题只是找到使用给定面额的钱所需的最少硬币数量。例如,如果需要找零 3 毛 7 分,则可以使用 1 个 2 分,1个 5 分,1 个 1 毛钱和1个 2 毛钱。

function minCoinChange(coins, amount) {
    const cache = [];
    const makeChange = (value) => {
        if (!value) {
            return [];
        }
        if (cache[value]) {
            return cache[value];
        }
        let min = [];
        let newMin;
        let newAmount;
        for (let i = 0; i < coins.length; i++) {
            const coin = coins[i];
            newAmount = value - coin;
            if (newAmount >= 0) {
                newMin = makeChange(newAmount);
            }
            if (newAmount >= 0 && 
            (newMin.length < min.length - 1 || !min.length) && (newMin.length || !newAmount)) {
                min = [coin].concat(newMin);
            }
        }
        return (cache[value] = min);
    }
    return makeChange(amount);
}

在上面的代码中,参数 coins 表示面额(在人民币中为 [1, 2, 5, 10, 20, 50])。为了防止重复计算,用到了一个 cachemakeChange 函数是递归实现的,它是一个内部函数,可以访问 cache

console.log(minCoinChange([1, 2, 5 10, 20], 37)); // => [2, 5, 10, 20]
console.log(minCoinChange([1, 3, 4], 6)) // => [3, 3]

贪心算法

贪心算法与当前的最优解决方案相关,并试图找到一个全局的最佳方案。与动态规划不同,它不考虑全局。贪心算法倾向于简单直观,但可能不是整体最优的解决方案。

贪心算法案例:最小硬币找零问题

上面用动态规划解决的硬币问题也可以用贪心算法解决。这个解决方案的是否能得到最优解取决于所采用的面额。

function minCoinChange(coins, amount) {
    const change = [];
    let total = 0;
    for (let i = coins.length; i>= 0; i--) {
        const coin = coins[i];
        while (total + coin <= amount) {
            change.push(coin);
            total += coin;
        }
    }
    return change;
}

可以看到,贪心算法比动态规划的方案要简单得多。下面看一下同样的求解案例,来了解两者之间的区别:

console.log(minCoinChange([1, 2, 5 10, 20], 37)); // => [2, 5, 10, 20]
console.log(minCoinChange([1, 3, 4], 6)) // => [4, 1, 1] 

贪心算法给出了第一个问题的最优解,但第二个并不是最优解(应该是 [3,3])。

贪心算法比动态规划算法要简单而且更快,但是得到的有可能不是最优解。

回溯算法

回溯算法非常适合逐步查找和构建解决方案。

  1. 尝试以一种方式解决问题。
  2. 如果它不起作用,就回溯并重复步骤 1,直到找到合适的解决方案为止。

对于回溯算法,我会另写一篇文章来介绍更复杂的算法。究竟写什么我还没想好,也许是写一个对数独求解的程序,如果你对这个感兴趣,请关注我的公众号!

算法是永无止境的,希望本文能帮你了解一些常见的算法范式。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 发布了文章 · 10月14日

浅析常见的算法范式

作者:Aral Roca

翻译:疯狂的技术宅

原文:https://dev.to/christinamcmah...

未经允许严禁转载

首先明确三个概念:

算法: 按步骤解决问题的过程。

范式: 思考问题的模式。

算法范式: 为问题构建高效解决方案的常规方法。

本文讨论一些常用的算法范式,例如

  • 分治算法
  • 动态规划
  • 贪婪算法
  • 回溯算法

分治法

在排序算法中,合并和快速排序这两种算法的共同点就是分而治之的算法。

分而治之是一种常见的算法设计,它的思路是把问题分解为与原始问题相似的较小子问题。通常以递归方式解决子问题,并结合子问题的解决方案来解决原始问题。

分治法的逻辑可以分为三个步骤:

  1. 将原始问题划分为较小的子问题。
  2. 通过递归解决子问题,解决完毕之后返回子问题的解决方案。
  3. 将子问题的解决方案合并为原始问题的解决方案。

分治法的例子:二叉搜索

下面是用分治实现的二叉搜索。

function binarySearchRecursive(array, value, low, high) {
    if (low <= high) {
        const mid = Math.floor((low + high) / 2);
        const element = array[mid];

        if (element < value) {
            return binarySearchRecursive(array, value, mid + 1, high);
        } else if (element > value) {
            return binarySearchRecursive(array, value, low, mid - 1);
        } else {
            return mid;
        }
    }
    return null;
}

export function binarySearch(array, value) {
    const sortedArray = quickSort(array);
    const low = 0;
    const high = sortedArray.length - 1;

    return binarySearchRecursive(array, value, low, high);
}

请注意,上面的 binarySearch 函数是供他人调用的,而 binarySearchRecursive 是实现分治法的地方。

动态规划法

动态规划是一种优化技术,用于通过把复杂问题分解为较小的子问题来解决。看上去很像是分治法,但动态规划不是把问题分解为独立的子问题然后再组合在一起,而是只把问题分解为独立的子问题。

算法逻辑分为三个步骤:

  1. 定义子问题。
  2. 重复解决子问题。
  3. 识别并解决基本问题。

动态规划案例:最小硬币找零问题

这是一个名为为硬币找零问题的常见面试题。硬币找零问题是给定找零的金额,找出可以用多少特定数量的硬币来找零的方式。最小硬币找零问题只是找到使用给定面额的钱所需的最少硬币数量。例如,如果需要找零 3 毛 7 分,则可以使用 1 个 2 分,1个 5 分,1 个 1 毛钱和1个 2 毛钱。

function minCoinChange(coins, amount) {
    const cache = [];
    const makeChange = (value) => {
        if (!value) {
            return [];
        }
        if (cache[value]) {
            return cache[value];
        }
        let min = [];
        let newMin;
        let newAmount;
        for (let i = 0; i < coins.length; i++) {
            const coin = coins[i];
            newAmount = value - coin;
            if (newAmount >= 0) {
                newMin = makeChange(newAmount);
            }
            if (newAmount >= 0 && 
            (newMin.length < min.length - 1 || !min.length) && (newMin.length || !newAmount)) {
                min = [coin].concat(newMin);
            }
        }
        return (cache[value] = min);
    }
    return makeChange(amount);
}

在上面的代码中,参数 coins 表示面额(在人民币中为 [1, 2, 5, 10, 20, 50])。为了防止重复计算,用到了一个 cachemakeChange 函数是递归实现的,它是一个内部函数,可以访问 cache

console.log(minCoinChange([1, 2, 5 10, 20], 37)); // => [2, 5, 10, 20]
console.log(minCoinChange([1, 3, 4], 6)) // => [3, 3]

贪心算法

贪心算法与当前的最优解决方案相关,并试图找到一个全局的最佳方案。与动态规划不同,它不考虑全局。贪心算法倾向于简单直观,但可能不是整体最优的解决方案。

贪心算法案例:最小硬币找零问题

上面用动态规划解决的硬币问题也可以用贪心算法解决。这个解决方案的是否能得到最优解取决于所采用的面额。

function minCoinChange(coins, amount) {
    const change = [];
    let total = 0;
    for (let i = coins.length; i>= 0; i--) {
        const coin = coins[i];
        while (total + coin <= amount) {
            change.push(coin);
            total += coin;
        }
    }
    return change;
}

可以看到,贪心算法比动态规划的方案要简单得多。下面看一下同样的求解案例,来了解两者之间的区别:

console.log(minCoinChange([1, 2, 5 10, 20], 37)); // => [2, 5, 10, 20]
console.log(minCoinChange([1, 3, 4], 6)) // => [4, 1, 1] 

贪心算法给出了第一个问题的最优解,但第二个并不是最优解(应该是 [3,3])。

贪心算法比动态规划算法要简单而且更快,但是得到的有可能不是最优解。

回溯算法

回溯算法非常适合逐步查找和构建解决方案。

  1. 尝试以一种方式解决问题。
  2. 如果它不起作用,就回溯并重复步骤 1,直到找到合适的解决方案为止。

对于回溯算法,我会另写一篇文章来介绍更复杂的算法。究竟写什么我还没想好,也许是写一个对数独求解的程序,如果你对这个感兴趣,请关注我的公众号!

算法是永无止境的,希望本文能帮你了解一些常见的算法范式。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 6 收藏 5 评论 0

疯狂的技术宅 收藏了文章 · 10月13日

一道 React 面试题:在浏览器、组件和元素中都渲染了些什么?

作者:Samer Buna

翻译:疯狂的技术宅

原文:https://medium.com/edge-coder...

未经允许严禁转载

这道题的答案有点复杂。

首先要搞清楚 elementcomponent 是不是一回事?

Image for post

从技术上来说,ReactDOM 不会在 DOM 中渲染 React 组件或 React 元素。它渲染由其组件实例支持的 DOM 元素。对于类组件来说这是正确的。但是对于函数组件,ReactDOM 仅渲染 DOM 元素。函数组件没有实例(可以通过 this 访问),因此在使用函数组件时,ReactDOM 会渲染由函数返回的元素所生成的 DOM 元素。

你需要在这里理解的是,React 元素不同于 DOM 元素。 React 元素只是 HTML 元素、React 组件或它们的混合的“描述”。

好吧,一个更好的面试题可能应该这样问:当你在JSX中使用 <MyComponent/> 之类的东西时,它是组件、元素还是实例?

这是一个元素,但不是 DOM 元素,而是一个 React元 素。因为任何 JSX 标签都会被转换为 React.createElement 再去调用。

但是要想让 React 继续使用这个 React 元素的话,必须调用一个函数或从一个类中创建实例。

你可能会在一些 React 教程中看到 组件(component)、元素(element)实例(instance) 这些词。我在这里混用这些词是不对的,但是我认为 React 的初学者需要了解它们的区别。 React 官方博客上有一篇文章专门说明这些概念,但我认为这些内容对于初学者来说还远远不够。

以下是这些术语的简单定义:

  • React Component 是模板,蓝图,全局定义的。可以是函数(带有渲染功能)。
  • React Element 是从组件中返回的东西。这个对象实际上描述了组件所代表的 DOM 节点。对于函数组件来说,此元素是函数返回的对象。对于类组件,元素是组件的渲染函数返回的对象。 React 元素不是我们在浏览器中所看到的。它们只是内存中的对象,我们无法对其进行任何更改。
  • React 在其内部通过创建、更新和销毁 instance 来找出需要渲染给浏览器的 DOM 元素树。使用类组件时,通常将其浏览器渲染的 DOM 元素称为组件实例。你可以渲染同一组件的多个实例。实例是你在基于类的组件内部使用的 this 关键字。你不需要手动从类创建实例,只需要记住它就在 React 的内存中即可。
  • 基于函数的 React 元素没有实例。一个函数组件仍然可以被多次渲染,但是 React 不会将本地实例与每个渲染相关联。它只是用函数的调用来确定要为该函数渲染的 DOM 元素。

最重要的是,ReactDOM 不会在浏览器中渲染组件,也不会渲染元素(这里的术语元素代表 React.createElement 的返回值)。它也不渲染实例。 它只渲染 DOM 元素。

不幸的是,使用术语组件既指模板又指通过模板使用的任何一种实例或者调用,这似乎是很普遍的。人们对此感到困惑很正常,这挺痛苦的。

What’s the story here?

这是什么故事?

每个 React 应用都从一个使用 React elementrender 调用开始。下面以 reactjs.org 官网提供的 HelloMessage 案例作为例子,我对这个例子稍微做了一些修改,使其具有了函数组件:

const Today = () => (
  <div>Today is {new Date().toDateString()}</div>
);class HelloMessage extends React.Component {
  render() {
    return (
      <React.Fragment>
        <div>Hello {this.props.name}</div>
        <Today />
      </React.Fragment>
    );
  }
}ReactDOM.render(
  <HelloMessage name="Taylor" />,
  mountNode
);

第一个 React 元素是我们在 ReactDOM.render 调用中开始的元素:

<HelloMessage name="Taylor" /> // 这是 React element

这个 React 元素描述了要渲染的 DOM 树应该以 HelloMessage 组件和值等于 Taylor 的 prop name 开始。

现在回答问题:HelloMessage 是什么?

每当 React 元素描述一个 React 组件时(就像上面的 React 元素一样),React 使用该组件将描述替换为组件返回的内容。这时它将会为基于类的组件创建一个实例,并将该实例的引用保留在内存中。它不会为基于函数的组件创建任何内容。它只是调用它们。

HelloMessage 组件返回的是一个描述 React.Fragment 组件的 React 元素。

回答问题: React.Fragment 是什么?

React 会持续不断的减少这些组件的未知描述,直到只存在有效的 DOM 节点。 React.Fragment 的描述被翻译成 2 个React 元素,一个描述 div ,另一个描述 Today 组件。

回答问题:代码中的 Today 什么是?

它调用 Today 函数来找出最后一个问题。 Today 函数返回描述一个 div 的 React 元素。

至此,virtual 树中包含了所有描述 DOM 节点的 React 元素。 React 通过一致性比较算法找出要在浏览器中更新的内容。用组件实例所转换的树节点保留修改该实例的能力。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 发布了文章 · 10月13日

一道 React 面试题:在浏览器、组件和元素中都渲染了些什么?

作者:Samer Buna

翻译:疯狂的技术宅

原文:https://medium.com/edge-coder...

未经允许严禁转载

这道题的答案有点复杂。

首先要搞清楚 elementcomponent 是不是一回事?

Image for post

从技术上来说,ReactDOM 不会在 DOM 中渲染 React 组件或 React 元素。它渲染由其组件实例支持的 DOM 元素。对于类组件来说这是正确的。但是对于函数组件,ReactDOM 仅渲染 DOM 元素。函数组件没有实例(可以通过 this 访问),因此在使用函数组件时,ReactDOM 会渲染由函数返回的元素所生成的 DOM 元素。

你需要在这里理解的是,React 元素不同于 DOM 元素。 React 元素只是 HTML 元素、React 组件或它们的混合的“描述”。

好吧,一个更好的面试题可能应该这样问:当你在JSX中使用 <MyComponent/> 之类的东西时,它是组件、元素还是实例?

这是一个元素,但不是 DOM 元素,而是一个 React元 素。因为任何 JSX 标签都会被转换为 React.createElement 再去调用。

但是要想让 React 继续使用这个 React 元素的话,必须调用一个函数或从一个类中创建实例。

你可能会在一些 React 教程中看到 组件(component)、元素(element)实例(instance) 这些词。我在这里混用这些词是不对的,但是我认为 React 的初学者需要了解它们的区别。 React 官方博客上有一篇文章专门说明这些概念,但我认为这些内容对于初学者来说还远远不够。

以下是这些术语的简单定义:

  • React Component 是模板,蓝图,全局定义的。可以是函数(带有渲染功能)。
  • React Element 是从组件中返回的东西。这个对象实际上描述了组件所代表的 DOM 节点。对于函数组件来说,此元素是函数返回的对象。对于类组件,元素是组件的渲染函数返回的对象。 React 元素不是我们在浏览器中所看到的。它们只是内存中的对象,我们无法对其进行任何更改。
  • React 在其内部通过创建、更新和销毁 instance 来找出需要渲染给浏览器的 DOM 元素树。使用类组件时,通常将其浏览器渲染的 DOM 元素称为组件实例。你可以渲染同一组件的多个实例。实例是你在基于类的组件内部使用的 this 关键字。你不需要手动从类创建实例,只需要记住它就在 React 的内存中即可。
  • 基于函数的 React 元素没有实例。一个函数组件仍然可以被多次渲染,但是 React 不会将本地实例与每个渲染相关联。它只是用函数的调用来确定要为该函数渲染的 DOM 元素。

最重要的是,ReactDOM 不会在浏览器中渲染组件,也不会渲染元素(这里的术语元素代表 React.createElement 的返回值)。它也不渲染实例。 它只渲染 DOM 元素。

不幸的是,使用术语组件既指模板又指通过模板使用的任何一种实例或者调用,这似乎是很普遍的。人们对此感到困惑很正常,这挺痛苦的。

What’s the story here?

这是什么故事?

每个 React 应用都从一个使用 React elementrender 调用开始。下面以 reactjs.org 官网提供的 HelloMessage 案例作为例子,我对这个例子稍微做了一些修改,使其具有了函数组件:

const Today = () => (
  <div>Today is {new Date().toDateString()}</div>
);class HelloMessage extends React.Component {
  render() {
    return (
      <React.Fragment>
        <div>Hello {this.props.name}</div>
        <Today />
      </React.Fragment>
    );
  }
}ReactDOM.render(
  <HelloMessage name="Taylor" />,
  mountNode
);

第一个 React 元素是我们在 ReactDOM.render 调用中开始的元素:

<HelloMessage name="Taylor" /> // 这是 React element

这个 React 元素描述了要渲染的 DOM 树应该以 HelloMessage 组件和值等于 Taylor 的 prop name 开始。

现在回答问题:HelloMessage 是什么?

每当 React 元素描述一个 React 组件时(就像上面的 React 元素一样),React 使用该组件将描述替换为组件返回的内容。这时它将会为基于类的组件创建一个实例,并将该实例的引用保留在内存中。它不会为基于函数的组件创建任何内容。它只是调用它们。

HelloMessage 组件返回的是一个描述 React.Fragment 组件的 React 元素。

回答问题: React.Fragment 是什么?

React 会持续不断的减少这些组件的未知描述,直到只存在有效的 DOM 节点。 React.Fragment 的描述被翻译成 2 个React 元素,一个描述 div ,另一个描述 Today 组件。

回答问题:代码中的 Today 什么是?

它调用 Today 函数来找出最后一个问题。 Today 函数返回描述一个 div 的 React 元素。

至此,virtual 树中包含了所有描述 DOM 节点的 React 元素。 React 通过一致性比较算法找出要在浏览器中更新的内容。用组件实例所转换的树节点保留修改该实例的能力。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 6 收藏 4 评论 0

疯狂的技术宅 收藏了文章 · 10月12日

怎样设计一个 JavaScript 插件系统

作者:Bryan Braun

翻译:疯狂的技术宅

原文:https://css-tricks.com/design...

未经允许严禁转载

不管是 jQuery、 Vue 还是 React,它们都支持插件。

插件是库和框架中很常见的功能,并且有一个充分的理由:它们允许开发人员以安全、可扩展的方式添加新的功能。这能够使你的项目具有更高的价值,而且也不会增加太多额外的维护负担。

那么该如何构建插件系统呢?在本文中我们用 JavaScript 构建一个自己的插件系统。

在这里我用的是插件(plugin)这个词,有时也被称做是扩展(extensions),附加组件(add-ons)或 模块(modules)。无论叫什么名字,概念都是一样的。

构建一个插件系统

先从一个简单的 JavaScript 计算器项目 betaCalc 开始,其他人可以为它添加新的功能。下面是基本的代码:

// 计算器项目
const betaCalc = {
  currentValue: 0,
  
  setValue(newValue) {
    this.currentValue = newValue;
    console.log(this.currentValue);
  },
  
  plus(addend) {
    this.setValue(this.currentValue + addend);
  },
  
  minus(subtrahend) {
    this.setValue(this.currentValue - subtrahend);
  }
};

// 使用计算器
betaCalc.setValue(3); // => 3
betaCalc.plus(3);     // => 6
betaCalc.minus(2);    // => 4

我们先把计算器定义为一种客观事物,这样可以使事情变得简单。计算器的工作原理是通过 console.log 把结果输出到控制台。

目前功能很有限。在代码中有一个 setValue 方法,能够接受一个数字并将其显示在“屏幕”上。还有 plusminus 方法,它们对当前显示的值执行对应的操作。

接下来就要添加更多的功能了。首先创建一个插件系统:

世界上最小的插件系统

先创建一个 register 方法,这样其他人就可以向 BetaCalc 注册插件了。它要做的工作很简单:得到一个外部插件,获取它的 exec 函数,并将其作为新方法附加到计算器上:

// 计算器项目
const betaCalc = {
  // 计算器相关的其他代码

  register(plugin) {
    const { name, exec } = plugin;
    this[name] = exec;
  }
};

下面的例子为计算器提供了一个计算平方的 squared 函数:

// 定义插件
const squaredPlugin = {
  name: 'squared',
  exec: function() {
    this.setValue(this.currentValue * this.currentValue)
  }
};

// 注册插件
betaCalc.register(squaredPlugin);

在大多数插件系统中,插件通常被分为两个部分:

  1. 要被执行的代码
  2. 元数据(包括名称、描述、版本号、依赖项等)

在上面定义的插件中,exec 函数中包含我们的代码,而 name 是元数据。在注册插件之后,exec 函数将会作为一种方法直接附加到我们的 betaCalc 对象上,从而使其可以访问 BetaCalc 的 this

现在 BetaCalc 多了一个计算平方的 squared 函数,可以直接调用:

betaCalc.setValue(3); // => 3
betaCalc.plus(2);     // => 5
betaCalc.squared();   // => 25
betaCalc.squared();   // => 625

这个系统的优点很多。该插件是一种简单的对象字面量,可以传递给我们的函数。这意味着可以通过 npm 去下载插件并将其作为 ES6 模块导入。轻松分发是非常重要的。

不过这个插件系统有一些缺陷。

通过为插件提供对 BetaCalc 的 this 的访问权限,他们可以对所有 BetaCalc 的代码进行读写访问。虽然这样对于获取和设置 currentValue 很轻松,但是也很危险。如果一个插件要重新定义一个内部函数(如 setValue),它会给 BetaCalc 和其他插件带来意想不到的后果。这违背了开放封闭原则,开放封闭原则的核心思想是软件实体是可扩展而不可修改的。

另外 squared 函数是通过产生副作用) 发挥作用的。这在 JavaScript 中很常见,但是感觉并不好,特别是当其他插件可能处在同一内部状态的情况下。我们需要一种更加实用的方法使我们的系统更安全、更可预测。

更好的插件架构

让我们换一种更好的插件架构。下面的代码更改了计算器及其插件 API:

// 计算器项目
const betaCalc = {
  currentValue: 0,
  
  setValue(value) {
    this.currentValue = value;
    console.log(this.currentValue);
  },
 
  core: {
    'plus': (currentVal, addend) => currentVal + addend,
    'minus': (currentVal, subtrahend) => currentVal - subtrahend
  },

  plugins: {},    

  press(buttonName, newVal) {
    const func = this.core[buttonName] || this.plugins[buttonName];
    this.setValue(func(this.currentValue, newVal));
  },

  register(plugin) {
    const { name, exec } = plugin;
    this.plugins[name] = exec;
  }
};
  
// 我们的插件
const squaredPlugin = { 
  name: 'squared',
  exec: function(currentValue) {
    return currentValue * currentValue;
  }
};

betaCalc.register(squaredPlugin);

// 使用计算器
betaCalc.setValue(3);      // => 3
betaCalc.press('plus', 2); // => 5
betaCalc.press('squared'); // => 25
betaCalc.press('squared'); // => 625

我们在代码中做了如下修改:

首先把插件与计算器的“核心”方法(如 plusminus)分开,做法是将其放入它自己的插件对象中。将我们的插件存储在一个 plugin 对象中可以使系统更加安全。现在此插件访问 this 时看不到 BetaCalc 的属性,只能得到 betaCalc.plugins 属性。

其次,我们实现了一个 press 方法,该方法按名称查找功能对应的函数,然后调用。现在,当我们调用插件的exec 函数时,会把计算器当前的值(currentValue)传给它,并得到新的值。

从本质上来说,新增加的 press 方法把所有的计算器功能都转换为了纯函数(pure functions),它们返回结果只依赖其参数,并且在执行过程中没有副作用。这样做有很多好处:

  • 简化了API。
  • 简化了测试(对于 BetaCalc 和插件本身)。
  • 它减少了系统的依赖性,也就是实现了松耦合。

这种新的架构与第一个例子相比受到了更多的限制,但是效果很好。实际上,我们为插件的作者设置了防护边界,限制他们只能做我们允许的事。

不过它可能过于严格了,现在我们的计算器插件只能对 currentValue 进行操作。如果插件的作者想要添加一些高级的功能,例如“暂存结果”或跟踪历史记录的功能,就无能为力了。

从另外一个角度来看,也许没什么关系。因为你赋予插件作者的力量是一种微妙的平衡。给他们开放过多的功能由可能会影响项目的稳定性,但是反过来,给他们的功能过少会也使他们很难解决自己的问题,如果这样的话你还不如没有插件。

还需要做些什么

为了改善我们系统,还需要做很多工作。

如果插件作者忘了定义名称或返回值,可以通过添加错误处理机制来通知插件作者。需要像 QA 那样思考问题,并想象在什么情况下会使我们的系统崩溃,这样才能使我们为这些情况添加容错机制并避免崩溃。

我们还可以扩展插件的功能范围。现在一个 BetaCalc 插件可以添加一个功能。不过如果它还可以为某些生命周期事件注册回调,例如计算器将要显示结果值时,该怎么办?或者,如果有一个专用的位置来存储多个交互中的状态该怎么办?

我们还可以扩展插件注册。如果需要使用一些初始设置来注册插件怎么办?可以使插件更灵活吗?如果插件作者希望注册整个功能套件而不是一个功能该怎么办?为了支持这一点需要做哪些更改?

你自己的插件系统

BetaCalc 及其插件系统都非常简单。如果你的项目比较大,那就需要对其他的插件架构做一些探索。

一个很好的途径是参考现有成功项目的插件系统。对于 JavaScript 项目来说,你可以参考 jQuery、D3、CKEditor 等。

你还需要熟悉各种 JavaScript 设计模式。Addy Osmani 的《javascript设计模式》这本书就挺不错的。每种设计模式都提供了不同的接口和耦合度,你可以为自己的插件系统挑选合适的体系结构。了解这些可以帮你更好地平衡每个人的需求。

除了设计模式本身之外,还可以借鉴许多好的软件开发原则来做出这类决策。除了我在前面提到过的一些方法(例如:开闭原理和松耦合)之外,还包括一些其他的方法,例如 Demeter 法则和 dependency 注入。

看上去需要了解的知识有很多,但是你必须研究它们。让每个人都重写他们的插件是最痛苦的一件事,因为你需要改变插件的架构。这会让他人失去对你的信任,并且会阻止他们在未来为你的系统做出贡献。

总结

从零开始写一个好的插件架构是非常困难的,你必须考虑并权衡很多因素来构建满足所有人的需求。它足够简单吗?足够强大吗?可以长期工作吗?

这种努力的付出是值得的,拥有一个好的插件系统可以帮助所有人。开发人员可以自由的解决问题,最终用户可以拥有大量的选择,这样你就可以在围绕自己的项目发展生态系统和社区。这是一种双赢的局面。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 发布了文章 · 10月12日

怎样设计一个 JavaScript 插件系统

作者:Bryan Braun

翻译:疯狂的技术宅

原文:https://css-tricks.com/design...

未经允许严禁转载

不管是 jQuery、 Vue 还是 React,它们都支持插件。

插件是库和框架中很常见的功能,并且有一个充分的理由:它们允许开发人员以安全、可扩展的方式添加新的功能。这能够使你的项目具有更高的价值,而且也不会增加太多额外的维护负担。

那么该如何构建插件系统呢?在本文中我们用 JavaScript 构建一个自己的插件系统。

在这里我用的是插件(plugin)这个词,有时也被称做是扩展(extensions),附加组件(add-ons)或 模块(modules)。无论叫什么名字,概念都是一样的。

构建一个插件系统

先从一个简单的 JavaScript 计算器项目 betaCalc 开始,其他人可以为它添加新的功能。下面是基本的代码:

// 计算器项目
const betaCalc = {
  currentValue: 0,
  
  setValue(newValue) {
    this.currentValue = newValue;
    console.log(this.currentValue);
  },
  
  plus(addend) {
    this.setValue(this.currentValue + addend);
  },
  
  minus(subtrahend) {
    this.setValue(this.currentValue - subtrahend);
  }
};

// 使用计算器
betaCalc.setValue(3); // => 3
betaCalc.plus(3);     // => 6
betaCalc.minus(2);    // => 4

我们先把计算器定义为一种客观事物,这样可以使事情变得简单。计算器的工作原理是通过 console.log 把结果输出到控制台。

目前功能很有限。在代码中有一个 setValue 方法,能够接受一个数字并将其显示在“屏幕”上。还有 plusminus 方法,它们对当前显示的值执行对应的操作。

接下来就要添加更多的功能了。首先创建一个插件系统:

世界上最小的插件系统

先创建一个 register 方法,这样其他人就可以向 BetaCalc 注册插件了。它要做的工作很简单:得到一个外部插件,获取它的 exec 函数,并将其作为新方法附加到计算器上:

// 计算器项目
const betaCalc = {
  // 计算器相关的其他代码

  register(plugin) {
    const { name, exec } = plugin;
    this[name] = exec;
  }
};

下面的例子为计算器提供了一个计算平方的 squared 函数:

// 定义插件
const squaredPlugin = {
  name: 'squared',
  exec: function() {
    this.setValue(this.currentValue * this.currentValue)
  }
};

// 注册插件
betaCalc.register(squaredPlugin);

在大多数插件系统中,插件通常被分为两个部分:

  1. 要被执行的代码
  2. 元数据(包括名称、描述、版本号、依赖项等)

在上面定义的插件中,exec 函数中包含我们的代码,而 name 是元数据。在注册插件之后,exec 函数将会作为一种方法直接附加到我们的 betaCalc 对象上,从而使其可以访问 BetaCalc 的 this

现在 BetaCalc 多了一个计算平方的 squared 函数,可以直接调用:

betaCalc.setValue(3); // => 3
betaCalc.plus(2);     // => 5
betaCalc.squared();   // => 25
betaCalc.squared();   // => 625

这个系统的优点很多。该插件是一种简单的对象字面量,可以传递给我们的函数。这意味着可以通过 npm 去下载插件并将其作为 ES6 模块导入。轻松分发是非常重要的。

不过这个插件系统有一些缺陷。

通过为插件提供对 BetaCalc 的 this 的访问权限,他们可以对所有 BetaCalc 的代码进行读写访问。虽然这样对于获取和设置 currentValue 很轻松,但是也很危险。如果一个插件要重新定义一个内部函数(如 setValue),它会给 BetaCalc 和其他插件带来意想不到的后果。这违背了开放封闭原则,开放封闭原则的核心思想是软件实体是可扩展而不可修改的。

另外 squared 函数是通过产生副作用) 发挥作用的。这在 JavaScript 中很常见,但是感觉并不好,特别是当其他插件可能处在同一内部状态的情况下。我们需要一种更加实用的方法使我们的系统更安全、更可预测。

更好的插件架构

让我们换一种更好的插件架构。下面的代码更改了计算器及其插件 API:

// 计算器项目
const betaCalc = {
  currentValue: 0,
  
  setValue(value) {
    this.currentValue = value;
    console.log(this.currentValue);
  },
 
  core: {
    'plus': (currentVal, addend) => currentVal + addend,
    'minus': (currentVal, subtrahend) => currentVal - subtrahend
  },

  plugins: {},    

  press(buttonName, newVal) {
    const func = this.core[buttonName] || this.plugins[buttonName];
    this.setValue(func(this.currentValue, newVal));
  },

  register(plugin) {
    const { name, exec } = plugin;
    this.plugins[name] = exec;
  }
};
  
// 我们的插件
const squaredPlugin = { 
  name: 'squared',
  exec: function(currentValue) {
    return currentValue * currentValue;
  }
};

betaCalc.register(squaredPlugin);

// 使用计算器
betaCalc.setValue(3);      // => 3
betaCalc.press('plus', 2); // => 5
betaCalc.press('squared'); // => 25
betaCalc.press('squared'); // => 625

我们在代码中做了如下修改:

首先把插件与计算器的“核心”方法(如 plusminus)分开,做法是将其放入它自己的插件对象中。将我们的插件存储在一个 plugin 对象中可以使系统更加安全。现在此插件访问 this 时看不到 BetaCalc 的属性,只能得到 betaCalc.plugins 属性。

其次,我们实现了一个 press 方法,该方法按名称查找功能对应的函数,然后调用。现在,当我们调用插件的exec 函数时,会把计算器当前的值(currentValue)传给它,并得到新的值。

从本质上来说,新增加的 press 方法把所有的计算器功能都转换为了纯函数(pure functions),它们返回结果只依赖其参数,并且在执行过程中没有副作用。这样做有很多好处:

  • 简化了API。
  • 简化了测试(对于 BetaCalc 和插件本身)。
  • 它减少了系统的依赖性,也就是实现了松耦合。

这种新的架构与第一个例子相比受到了更多的限制,但是效果很好。实际上,我们为插件的作者设置了防护边界,限制他们只能做我们允许的事。

不过它可能过于严格了,现在我们的计算器插件只能对 currentValue 进行操作。如果插件的作者想要添加一些高级的功能,例如“暂存结果”或跟踪历史记录的功能,就无能为力了。

从另外一个角度来看,也许没什么关系。因为你赋予插件作者的力量是一种微妙的平衡。给他们开放过多的功能由可能会影响项目的稳定性,但是反过来,给他们的功能过少会也使他们很难解决自己的问题,如果这样的话你还不如没有插件。

还需要做些什么

为了改善我们系统,还需要做很多工作。

如果插件作者忘了定义名称或返回值,可以通过添加错误处理机制来通知插件作者。需要像 QA 那样思考问题,并想象在什么情况下会使我们的系统崩溃,这样才能使我们为这些情况添加容错机制并避免崩溃。

我们还可以扩展插件的功能范围。现在一个 BetaCalc 插件可以添加一个功能。不过如果它还可以为某些生命周期事件注册回调,例如计算器将要显示结果值时,该怎么办?或者,如果有一个专用的位置来存储多个交互中的状态该怎么办?

我们还可以扩展插件注册。如果需要使用一些初始设置来注册插件怎么办?可以使插件更灵活吗?如果插件作者希望注册整个功能套件而不是一个功能该怎么办?为了支持这一点需要做哪些更改?

你自己的插件系统

BetaCalc 及其插件系统都非常简单。如果你的项目比较大,那就需要对其他的插件架构做一些探索。

一个很好的途径是参考现有成功项目的插件系统。对于 JavaScript 项目来说,你可以参考 jQuery、D3、CKEditor 等。

你还需要熟悉各种 JavaScript 设计模式。Addy Osmani 的《javascript设计模式》这本书就挺不错的。每种设计模式都提供了不同的接口和耦合度,你可以为自己的插件系统挑选合适的体系结构。了解这些可以帮你更好地平衡每个人的需求。

除了设计模式本身之外,还可以借鉴许多好的软件开发原则来做出这类决策。除了我在前面提到过的一些方法(例如:开闭原理和松耦合)之外,还包括一些其他的方法,例如 Demeter 法则和 dependency 注入。

看上去需要了解的知识有很多,但是你必须研究它们。让每个人都重写他们的插件是最痛苦的一件事,因为你需要改变插件的架构。这会让他人失去对你的信任,并且会阻止他们在未来为你的系统做出贡献。

总结

从零开始写一个好的插件架构是非常困难的,你必须考虑并权衡很多因素来构建满足所有人的需求。它足够简单吗?足够强大吗?可以长期工作吗?

这种努力的付出是值得的,拥有一个好的插件系统可以帮助所有人。开发人员可以自由的解决问题,最终用户可以拥有大量的选择,这样你就可以在围绕自己的项目发展生态系统和社区。这是一种双赢的局面。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 12 收藏 8 评论 0

疯狂的技术宅 收藏了文章 · 10月10日

在 JavaScript 中定义函数的不同方法

作者:Samer Buna

翻译:疯狂的技术宅

原文:https://medium.com/edge-coder...

未经允许严禁转载

你知道在 JavaScript 中创建一个函数有多少种方式吗?

1. 声明函数

function sum(a, b) { return a + b; } 

2. 表达式函数

// 可以命名:
(function sum(a, b) { return a + b; });

// 也可匿名 (AVOID):
(function(a, b) { return a + b; });

// 也能分配给变量:
const sum = function sum(a, b) { return a + b; })

3. 箭头函数

// 一般形式:
(a, b) => { return a + b };

// 单参数,一行返回:
name => name.split(' ')

// 多参数,一行返回:
(a, b) => a + b

// 单参数,带函数体
name => { return name.split(' '); }

4. 生成器函数

function *sum(a, b) { yield a + b; }

5. 异步函数

async function sum(a, b) { return await a + b; }

6. 构造函数(AVOID)

new Function(‘a’, ‘b’, ‘return a + b;’);

7. 导出函数

// 默认导出
export default function(a, b) { return a + b; };

// 命名导出
export function sum(a, b) { return a + b; };

8. 对象属性函数

// 一般形式:
const object = {
  sum: function(a, b) { return a + b; },
};

// 简写:
const object = {
  sum(a, b) { return a + b; },
};

9. 对象动态属性函数

const functionName = "sum";
const object = {
  [functionName]: function(a, b) { return a + b; },
};

10. 对象属性的 Getter/Setter 函数

// 一般形式:
const object = {
  get answer { return 42; },
  set answer(value) { /* 一些操作value的代码 */ },
};

//  使用 defineProperty
const obj = {};
Object.defineProperty(obj, "answer", {
  get() { return 42; },
  set(value) { /* 一些操作value的代码 */ },
});

11. 对象动态属性的 Getter/Setter 函数

const functionName = "answer";
const object = {
  get [functionName]() { return 42; },
  set [functionName](value) { /* 一些操作value的代码 */ },
};

12. 类方法函数

class Compute {
  // 一般形式:
  sum(a, b) { return a + b; }
}

class Compute {
  // 静态:
  static sum(a, b) { return a + b; };
}

13. 类属性函数

class Compute {
  // 一般形式:
  sum = function (a, b) { return a + b; };
}class Compute {
  // 静态:
  static sum = function(a, b) { return a + b; };
}

14. 类私有函数

class Compute {
  // 一般形式:
  #sum(a, b) {
    return a + b;
  }  // 静态:
  static #sum(a, b) {
    return a + b;
  }
}

总结

如果你把这些方式中的某几个结合起来,还会有更多的可能性。 你知道还有哪些方法吗,请留言告诉我。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 发布了文章 · 10月10日

在 JavaScript 中定义函数的不同方法

作者:Samer Buna

翻译:疯狂的技术宅

原文:https://medium.com/edge-coder...

未经允许严禁转载

你知道在 JavaScript 中创建一个函数有多少种方式吗?

1. 声明函数

function sum(a, b) { return a + b; } 

2. 表达式函数

// 可以命名:
(function sum(a, b) { return a + b; });

// 也可匿名 (AVOID):
(function(a, b) { return a + b; });

// 也能分配给变量:
const sum = function sum(a, b) { return a + b; })

3. 箭头函数

// 一般形式:
(a, b) => { return a + b };

// 单参数,一行返回:
name => name.split(' ')

// 多参数,一行返回:
(a, b) => a + b

// 单参数,带函数体
name => { return name.split(' '); }

4. 生成器函数

function *sum(a, b) { yield a + b; }

5. 异步函数

async function sum(a, b) { return await a + b; }

6. 构造函数(AVOID)

new Function(‘a’, ‘b’, ‘return a + b;’);

7. 导出函数

// 默认导出
export default function(a, b) { return a + b; };

// 命名导出
export function sum(a, b) { return a + b; };

8. 对象属性函数

// 一般形式:
const object = {
  sum: function(a, b) { return a + b; },
};

// 简写:
const object = {
  sum(a, b) { return a + b; },
};

9. 对象动态属性函数

const functionName = "sum";
const object = {
  [functionName]: function(a, b) { return a + b; },
};

10. 对象属性的 Getter/Setter 函数

// 一般形式:
const object = {
  get answer { return 42; },
  set answer(value) { /* 一些操作value的代码 */ },
};

//  使用 defineProperty
const obj = {};
Object.defineProperty(obj, "answer", {
  get() { return 42; },
  set(value) { /* 一些操作value的代码 */ },
});

11. 对象动态属性的 Getter/Setter 函数

const functionName = "answer";
const object = {
  get [functionName]() { return 42; },
  set [functionName](value) { /* 一些操作value的代码 */ },
};

12. 类方法函数

class Compute {
  // 一般形式:
  sum(a, b) { return a + b; }
}

class Compute {
  // 静态:
  static sum(a, b) { return a + b; };
}

13. 类属性函数

class Compute {
  // 一般形式:
  sum = function (a, b) { return a + b; };
}class Compute {
  // 静态:
  static sum = function(a, b) { return a + b; };
}

14. 类私有函数

class Compute {
  // 一般形式:
  #sum(a, b) {
    return a + b;
  }  // 静态:
  static #sum(a, b) {
    return a + b;
  }
}

总结

如果你把这些方式中的某几个结合起来,还会有更多的可能性。 你知道还有哪些方法吗,请留言告诉我。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 18 收藏 14 评论 0

疯狂的技术宅 收藏了文章 · 10月9日

JavaScript计时器:你需要了解的所有内容

作者:Samer Buna

翻译:疯狂的技术宅

原文:https://medium.com/edge-coder...

未经允许严禁转载

先从一个面试题开始:

JavaScript 面试题:

setTimeout 和 setInterval 的源代码是在哪里实现的? (不能百度和谷歌😎 )

在继续往下看之前,请先在你的头脑中回答问题


你的答案可能会是 V8(或其他VM),但很遗憾,这是错的。尽管 “JavaScript Timers” 的应用很广泛,但是 setTimeoutsetInterval 之类的函数并不是 ECMAScript 规范或任何一种 JavaScript 引擎实现的一部分。Timer 函数是由浏览器实现的,不同浏览器的实现方式会有所不同。同时 Timer 也是由 Node.js 运行时本身实现的。

在浏览器中,主要的计时器函数是 Window 接口的一部分,这个接口还有一些其他函数和对象。该接口使其所有元素在主 JavaScript 作用域内全局可用。这就是为什么你可以直接在浏览器的控制台中执行 setTimeout 的原因。

在 Node 中,计时器是 global 对象的一部分,该对象的行为类似于浏览器的 window 。你可以在 Node 的源代码中找到它的实现

有些人可能认为这个面试题不咋样,但是我认为你应该了解这个,因为如果你不了解这一点,则可能表明你并不完全了解 V8(及其他VM)是如何与浏览器和 Node 交互的。

以下是一些关于计时器函数的例子和编码挑战的练习:

延迟函数的执行

定时器函数是高阶函数,可用于延迟或重复其他函数的执行(它们作为第一个参数)。

以下是延迟执行的例子:

// example1.js

setTimeout(
  () => {
    console.log('Hello after 4 seconds');
  },
  4 * 1000
);

本例用 setTimeoutconsole.log 的输出延迟4秒。 setTimeout 的第二个参数是延迟时间(以毫秒为单位)。这就是为什么要用 4 乘以 1000 的原因。

setTimeout 的第一个参数是你想要延迟执行的函数。

如果用 node 命令执行 example1.js 文件,Node 会暂停 4。秒钟,然后输出一行消息,之后退出。

注意,setTimeout 的第一个参数只是一个函数引用。也可以不像 example1.js 那样使用内联函数。下面是不用内联函数的相同功能的代码:

const func = () => {
  console.log('Hello after 4 seconds');
};

setTimeout(func, 4 * 1000);

传递参数

如果要让用 setTimeout 延迟执行的函数接受参数,可以将 setTimeout 本身其余的参数用于将参数值传递给所延迟的函数。

// 函数: func(arg1, arg2, arg3, ...)
// 可以用: setTimeout(func, delay, arg1, arg2, arg3, ...)

这是一个例子:

// example2.js

const rocks = who => {
  console.log(who + ' rocks');
};

setTimeout(rocks, 2 * 1000, 'Node.js');

上面的 rocks 函数延迟了 2 秒,它接受参数 who,而 setTimeout 调用将值 “Node.js” 用于 who 参数。

node 命令执行 example2.js 将会在 2 秒钟后打印出 “Node.js rocks”。

计时器编码挑战 #1

现在用你在前面所学到的关于 setTimeout 的知识,在要求的延迟时间后输出以下 2 条内容。

  • 4 秒钟后输出 “Hello after 4 seconds
  • 8 秒钟后输出 “Hello after 8 seconds”。

要求

你只能定义一个函数,这其中包括内联函数。这意味着你所有的 setTimeout 调用将必须使用完全相同的函数。

解决方案

这是我的方法:

// solution1.js

const theOneFunc = delay => {
  console.log('Hello after ' + delay + ' seconds');
};

setTimeout(theOneFunc, 4 * 1000, 4);
setTimeout(theOneFunc, 8 * 1000, 8);

我已经使 theOneFunc 接收了一个 delay 参数,并在输出的消息中使用了 delay 参数的值。这样该函数可以根据传递给它的延迟值来输出不同的消息。

然后,我在两个 setTimeout 调用中使用了 theOneFunc,一个在 4 秒后触发,另一个在 8 秒后触发。这两个setTimeout 调用都用到了第三个参数来表示 theOneFuncdelay 参数。

最后用 node 命令执行 solution1.js 文件,第一条消息输出在 4 秒钟后,第二条消息在 8 秒钟后。

重复执行一个函数

如果要求你一直每隔 4 秒钟输出一条消息怎么办?

尽管你可以将 setTimeout 放入循环中,但是计时器 API也提供了 setInterval 函数,这能够满足一直做某件事的要求。

下面是 setInterval 的例子:

// example3.js

setInterval(
  () => console.log('Hello every 3 seconds'),
  3000
);

本例将会每 3 秒输出一次消息。用 node 命令执行 example3.js 将会使 Node 一直输出这个消息,直到你用 CTRL + C 终止进程为止。

取消计时器

因为调用计时器函数会实现计划一个动作,所以该动作也可以在执行之前取消。

调用 setTimeout 会返回一个计时器 ID,可以把计时器 ID 当做参数传给 clearTimeout 函数来取消它。下面一个例子:

// example4.js

const timerId = setTimeout(
  () => console.log('你看不到这行输出!'),
  0
);

clearTimeout(timerId);

这个简单的计时器应该在 0 毫秒后被触发(使其立即生效),但实际上并不会,因为此时我们正在获取 timerId 值,并在调用 clearTimeout 之后立即将其取消。

node 命令执行 example4.js 时,Node 不会输出任何内容,而程序将会退出。

顺便说一句,在 Node.js 中,还有另一种方法对 0 ms 进行 setTimeout 。 Node.js 计时器 API 还有一个名为 setImmediate 的函数,它与前面 0 毫秒的 setTimeout 基本上相同,但是不用指定延迟时间:

setImmediate(
  () => console.log('我等效于 0 毫秒的 setTimeout'),
);

setImmediate 函数并非在所有浏览器中都可用。千万不要用在前端代码中。

clearTimeout 类似,还有一个 clearInterval 函数,除了对 setInerval 的调用外,它们的功能相同,而且也有 clearImmediate 的调用。

定时器延迟是不能够完全保证的

在上一个例子中,你可能注意到了,如果用 setTimeout0 毫秒之后执行某个操作,并不意味着会马上执行它(在 setTimeout 这一行之后),而是在脚本中的所有其他内容( clearTimeout 这一行)之后才会执行它的调用。

// example5.js

setTimeout(
  () => console.log('Hello after 0.5 seconds. MAYBE!'),
  500,
);

for (let i = 0; i < 1e10; i++) {
  // 同步阻塞
}

在这个例子中定义了计时器之后,我们立即通过一个大的 for 循环来阻塞运行。 1e10 的意思是 1 前面有 10 个零,所以这个循环是 100 亿次循环(基本上模拟了繁忙的 CPU)。在循环时 Node 无法执行任何操作。

当然,这在实际开发中非常糟糕,但是它能帮你了解 setTimeout 延迟是无法保证马上就开始的事实。 500 ms 表示最小延迟为 500 ms。实际上,这段脚本将会执行很长的时间。它必须先等待阻塞循环才能开始。

计时器编码挑战 #2

编写一段脚本,每秒输出一次消息 “Hello World”,但仅输出 5 次。 5 次后,脚本应输出消息 “Done” 并退出。

要求:不能用 setTimeout

提示:你需要一个计数器。

解决方案

这是我的方法:

let counter = 0;
const intervalId = setInterval(() => {
  console.log('Hello World');
  counter += 1;if (counter === 5) {
    console.log('Done');
    clearInterval(intervalId);
  }
}, 1000);

counter 的值初始化为 0,然后通过 setInterval 得到其 ID。

延迟函数将输出消息并使计数器加 1。在函数内部的 if 语句中检查现在是否已经输出 5 次了,如果是的话,则输出“Done”并用 intervalId 常数清理。间隔延迟为 1000 毫秒。

究竟是谁调用了延迟函数?

当在常规函数中使用 JavaScript 的 this 关键字时,如下所示:

function whoCalledMe() {
  console.log('Caller is', this);
}

在关键字 this 中的值将代表函数的调用者。如果你在 Node REPL 内定义以上函数,则调用方将是 global 对象。如果在浏览器的控制台中定义函数,则调用方将是 window 对象。

下面把函数定义为对象的属性,这样可以看的更加清楚:

const obj = { 
  id: '42',
  whoCalledMe() {
    console.log('Caller is', this);
  }
}; // 现在,函数引用为:obj.whoCallMe

现在,当你直接用其引用去调用 obj.whoCallMe 函数时,调用者将是 obj 对象(由其 ID 进行标识):

image.png

现在的问题是,如果把 obj.whoCallMe 的引用传递给 setTimetout 调用,调用者将会是谁?

// 将会输出什么?
setTimeout(obj.whoCalledMe, 0);

在这种情况下,调用者会是谁?

根据执行计时器函数的位置不同,答案也不一样。在当前这种情况下,根本无法确定调用者是谁。你会失去对调用者的控制,因为计时器只是其中的一种可能。如果你在 Node REPL 中对其进行测试,则会看到调用者是一个 Timetout 对象:

image.png

注意,在常规函数中使用 JavaScript 的 this 关键字时这非常重要。如果你使用箭头函数的话,则无需担心调用者是谁。

计时器编码挑战 #3

编写一段脚本,连续输出消息 “Hello World”,但是每次延迟都不一致。从 1 秒开始,然后每次增加 1 秒。即第二次会有 2 秒的延迟,第三时间会有3秒的延迟,依此类推。

如果在输出的消息中包含延迟。预期的输出如下:

Hello World. 1
Hello World. 2
Hello World. 3
...

要求: 你只能用 const 来定义变量,不能用 letvar

解决方案

由于延迟量是这项挑战中的变量,因此在这里不能用 setInterval,但是可以在递归调用中使用 setTimeout 手动创建执行间隔。第一个使用 setTimeout 执行的函数将会创建另一个计时器,依此类推。

另外,因为不能用 letvar,所以我们没有办法用计数器来增加每次递归调用中的延迟,但是可以使递归函数的参数在递归调用中递增。

下面的方法供你参考:

const greeting = delay =>
  setTimeout(() => {
    console.log('Hello World. ' + delay);
    greeting(delay + 1);
  }, delay * 1000);
greeting(1);

计时器编码挑战 #4

编写一段脚本,用和挑战#3 相同的可变延迟概念连续输出消息 “Hello World”,但是这次,每个主延迟间隔以 5 条消息为一组。前 5 条消息的延迟为 100ms,然后是下 5 条消息的延迟为 200ms,然后是 300ms,依此类推。

脚本的行为如下:

  • 在 100 毫秒时,脚本开始输出 “Hello World”,并以 100 毫秒的间隔执行 5 次。第一条消息将在 100 毫秒显示,第二条消息在 200 毫秒出现,依此类推。
  • 在输出前 5。条消息之后,脚本应将主延迟增加到 200ms。所以第 6 条消息将在 500 ms + 200 ms(700ms)时输出,第 7 条消息将在 900ms 输出,第 8。条消息将在 1100ms 输出,依此类推。
  • 10 条消息后,脚本把主延迟增加到 300ms。所以第 11 条消息应该在 500ms + 1000ms + 300ms(18000ms)时输出。第 12 条消息在 21000ms 输出,依此类推。
  • 一直延续这个模式。

在输出的消息中包含延迟。预期的输出如下所示(不带注释):

Hello World. 100  // At 100ms
Hello World. 100  // At 200ms
Hello World. 100  // At 300ms
Hello World. 100  // At 400ms
Hello World. 100  // At 500ms
Hello World. 200  // At 700ms
Hello World. 200  // At 900ms
Hello World. 200  // At 1100ms
...

要求: 只能用 setInterval (不能用 setTimeout),并且只能用一个 if 语句。

解决方案

因为只能用 setInterval ,所以在这里需要通过递归来增加下一次 setInterval 调用的延迟。另外还需要一个 if 语句,用来控制在对该递归函数的 5 次调用之后执行该操作。

下面的解决方案供你参考:

let lastIntervalId, counter = 5;const greeting = delay => {
  if (counter === 5) {
    clearInterval(lastIntervalId);
    lastIntervalId = setInterval(() => {
      console.log('Hello World. ', delay);
      greeting(delay + 100);
    }, delay);
    counter = 0;
  }counter += 1;
};greeting(100);

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 发布了文章 · 10月9日

JavaScript计时器:你需要了解的所有内容

作者:Samer Buna

翻译:疯狂的技术宅

原文:https://medium.com/edge-coder...

未经允许严禁转载

先从一个面试题开始:

JavaScript 面试题:

setTimeout 和 setInterval 的源代码是在哪里实现的? (不能百度和谷歌😎 )

在继续往下看之前,请先在你的头脑中回答问题


你的答案可能会是 V8(或其他VM),但很遗憾,这是错的。尽管 “JavaScript Timers” 的应用很广泛,但是 setTimeoutsetInterval 之类的函数并不是 ECMAScript 规范或任何一种 JavaScript 引擎实现的一部分。Timer 函数是由浏览器实现的,不同浏览器的实现方式会有所不同。同时 Timer 也是由 Node.js 运行时本身实现的。

在浏览器中,主要的计时器函数是 Window 接口的一部分,这个接口还有一些其他函数和对象。该接口使其所有元素在主 JavaScript 作用域内全局可用。这就是为什么你可以直接在浏览器的控制台中执行 setTimeout 的原因。

在 Node 中,计时器是 global 对象的一部分,该对象的行为类似于浏览器的 window 。你可以在 Node 的源代码中找到它的实现

有些人可能认为这个面试题不咋样,但是我认为你应该了解这个,因为如果你不了解这一点,则可能表明你并不完全了解 V8(及其他VM)是如何与浏览器和 Node 交互的。

以下是一些关于计时器函数的例子和编码挑战的练习:

延迟函数的执行

定时器函数是高阶函数,可用于延迟或重复其他函数的执行(它们作为第一个参数)。

以下是延迟执行的例子:

// example1.js

setTimeout(
  () => {
    console.log('Hello after 4 seconds');
  },
  4 * 1000
);

本例用 setTimeoutconsole.log 的输出延迟4秒。 setTimeout 的第二个参数是延迟时间(以毫秒为单位)。这就是为什么要用 4 乘以 1000 的原因。

setTimeout 的第一个参数是你想要延迟执行的函数。

如果用 node 命令执行 example1.js 文件,Node 会暂停 4。秒钟,然后输出一行消息,之后退出。

注意,setTimeout 的第一个参数只是一个函数引用。也可以不像 example1.js 那样使用内联函数。下面是不用内联函数的相同功能的代码:

const func = () => {
  console.log('Hello after 4 seconds');
};

setTimeout(func, 4 * 1000);

传递参数

如果要让用 setTimeout 延迟执行的函数接受参数,可以将 setTimeout 本身其余的参数用于将参数值传递给所延迟的函数。

// 函数: func(arg1, arg2, arg3, ...)
// 可以用: setTimeout(func, delay, arg1, arg2, arg3, ...)

这是一个例子:

// example2.js

const rocks = who => {
  console.log(who + ' rocks');
};

setTimeout(rocks, 2 * 1000, 'Node.js');

上面的 rocks 函数延迟了 2 秒,它接受参数 who,而 setTimeout 调用将值 “Node.js” 用于 who 参数。

node 命令执行 example2.js 将会在 2 秒钟后打印出 “Node.js rocks”。

计时器编码挑战 #1

现在用你在前面所学到的关于 setTimeout 的知识,在要求的延迟时间后输出以下 2 条内容。

  • 4 秒钟后输出 “Hello after 4 seconds
  • 8 秒钟后输出 “Hello after 8 seconds”。

要求

你只能定义一个函数,这其中包括内联函数。这意味着你所有的 setTimeout 调用将必须使用完全相同的函数。

解决方案

这是我的方法:

// solution1.js

const theOneFunc = delay => {
  console.log('Hello after ' + delay + ' seconds');
};

setTimeout(theOneFunc, 4 * 1000, 4);
setTimeout(theOneFunc, 8 * 1000, 8);

我已经使 theOneFunc 接收了一个 delay 参数,并在输出的消息中使用了 delay 参数的值。这样该函数可以根据传递给它的延迟值来输出不同的消息。

然后,我在两个 setTimeout 调用中使用了 theOneFunc,一个在 4 秒后触发,另一个在 8 秒后触发。这两个setTimeout 调用都用到了第三个参数来表示 theOneFuncdelay 参数。

最后用 node 命令执行 solution1.js 文件,第一条消息输出在 4 秒钟后,第二条消息在 8 秒钟后。

重复执行一个函数

如果要求你一直每隔 4 秒钟输出一条消息怎么办?

尽管你可以将 setTimeout 放入循环中,但是计时器 API也提供了 setInterval 函数,这能够满足一直做某件事的要求。

下面是 setInterval 的例子:

// example3.js

setInterval(
  () => console.log('Hello every 3 seconds'),
  3000
);

本例将会每 3 秒输出一次消息。用 node 命令执行 example3.js 将会使 Node 一直输出这个消息,直到你用 CTRL + C 终止进程为止。

取消计时器

因为调用计时器函数会实现计划一个动作,所以该动作也可以在执行之前取消。

调用 setTimeout 会返回一个计时器 ID,可以把计时器 ID 当做参数传给 clearTimeout 函数来取消它。下面一个例子:

// example4.js

const timerId = setTimeout(
  () => console.log('你看不到这行输出!'),
  0
);

clearTimeout(timerId);

这个简单的计时器应该在 0 毫秒后被触发(使其立即生效),但实际上并不会,因为此时我们正在获取 timerId 值,并在调用 clearTimeout 之后立即将其取消。

node 命令执行 example4.js 时,Node 不会输出任何内容,而程序将会退出。

顺便说一句,在 Node.js 中,还有另一种方法对 0 ms 进行 setTimeout 。 Node.js 计时器 API 还有一个名为 setImmediate 的函数,它与前面 0 毫秒的 setTimeout 基本上相同,但是不用指定延迟时间:

setImmediate(
  () => console.log('我等效于 0 毫秒的 setTimeout'),
);

setImmediate 函数并非在所有浏览器中都可用。千万不要用在前端代码中。

clearTimeout 类似,还有一个 clearInterval 函数,除了对 setInerval 的调用外,它们的功能相同,而且也有 clearImmediate 的调用。

定时器延迟是不能够完全保证的

在上一个例子中,你可能注意到了,如果用 setTimeout0 毫秒之后执行某个操作,并不意味着会马上执行它(在 setTimeout 这一行之后),而是在脚本中的所有其他内容( clearTimeout 这一行)之后才会执行它的调用。

// example5.js

setTimeout(
  () => console.log('Hello after 0.5 seconds. MAYBE!'),
  500,
);

for (let i = 0; i < 1e10; i++) {
  // 同步阻塞
}

在这个例子中定义了计时器之后,我们立即通过一个大的 for 循环来阻塞运行。 1e10 的意思是 1 前面有 10 个零,所以这个循环是 100 亿次循环(基本上模拟了繁忙的 CPU)。在循环时 Node 无法执行任何操作。

当然,这在实际开发中非常糟糕,但是它能帮你了解 setTimeout 延迟是无法保证马上就开始的事实。 500 ms 表示最小延迟为 500 ms。实际上,这段脚本将会执行很长的时间。它必须先等待阻塞循环才能开始。

计时器编码挑战 #2

编写一段脚本,每秒输出一次消息 “Hello World”,但仅输出 5 次。 5 次后,脚本应输出消息 “Done” 并退出。

要求:不能用 setTimeout

提示:你需要一个计数器。

解决方案

这是我的方法:

let counter = 0;
const intervalId = setInterval(() => {
  console.log('Hello World');
  counter += 1;if (counter === 5) {
    console.log('Done');
    clearInterval(intervalId);
  }
}, 1000);

counter 的值初始化为 0,然后通过 setInterval 得到其 ID。

延迟函数将输出消息并使计数器加 1。在函数内部的 if 语句中检查现在是否已经输出 5 次了,如果是的话,则输出“Done”并用 intervalId 常数清理。间隔延迟为 1000 毫秒。

究竟是谁调用了延迟函数?

当在常规函数中使用 JavaScript 的 this 关键字时,如下所示:

function whoCalledMe() {
  console.log('Caller is', this);
}

在关键字 this 中的值将代表函数的调用者。如果你在 Node REPL 内定义以上函数,则调用方将是 global 对象。如果在浏览器的控制台中定义函数,则调用方将是 window 对象。

下面把函数定义为对象的属性,这样可以看的更加清楚:

const obj = { 
  id: '42',
  whoCalledMe() {
    console.log('Caller is', this);
  }
}; // 现在,函数引用为:obj.whoCallMe

现在,当你直接用其引用去调用 obj.whoCallMe 函数时,调用者将是 obj 对象(由其 ID 进行标识):

image.png

现在的问题是,如果把 obj.whoCallMe 的引用传递给 setTimetout 调用,调用者将会是谁?

// 将会输出什么?
setTimeout(obj.whoCalledMe, 0);

在这种情况下,调用者会是谁?

根据执行计时器函数的位置不同,答案也不一样。在当前这种情况下,根本无法确定调用者是谁。你会失去对调用者的控制,因为计时器只是其中的一种可能。如果你在 Node REPL 中对其进行测试,则会看到调用者是一个 Timetout 对象:

image.png

注意,在常规函数中使用 JavaScript 的 this 关键字时这非常重要。如果你使用箭头函数的话,则无需担心调用者是谁。

计时器编码挑战 #3

编写一段脚本,连续输出消息 “Hello World”,但是每次延迟都不一致。从 1 秒开始,然后每次增加 1 秒。即第二次会有 2 秒的延迟,第三时间会有3秒的延迟,依此类推。

如果在输出的消息中包含延迟。预期的输出如下:

Hello World. 1
Hello World. 2
Hello World. 3
...

要求: 你只能用 const 来定义变量,不能用 letvar

解决方案

由于延迟量是这项挑战中的变量,因此在这里不能用 setInterval,但是可以在递归调用中使用 setTimeout 手动创建执行间隔。第一个使用 setTimeout 执行的函数将会创建另一个计时器,依此类推。

另外,因为不能用 letvar,所以我们没有办法用计数器来增加每次递归调用中的延迟,但是可以使递归函数的参数在递归调用中递增。

下面的方法供你参考:

const greeting = delay =>
  setTimeout(() => {
    console.log('Hello World. ' + delay);
    greeting(delay + 1);
  }, delay * 1000);
greeting(1);

计时器编码挑战 #4

编写一段脚本,用和挑战#3 相同的可变延迟概念连续输出消息 “Hello World”,但是这次,每个主延迟间隔以 5 条消息为一组。前 5 条消息的延迟为 100ms,然后是下 5 条消息的延迟为 200ms,然后是 300ms,依此类推。

脚本的行为如下:

  • 在 100 毫秒时,脚本开始输出 “Hello World”,并以 100 毫秒的间隔执行 5 次。第一条消息将在 100 毫秒显示,第二条消息在 200 毫秒出现,依此类推。
  • 在输出前 5。条消息之后,脚本应将主延迟增加到 200ms。所以第 6 条消息将在 500 ms + 200 ms(700ms)时输出,第 7 条消息将在 900ms 输出,第 8。条消息将在 1100ms 输出,依此类推。
  • 10 条消息后,脚本把主延迟增加到 300ms。所以第 11 条消息应该在 500ms + 1000ms + 300ms(18000ms)时输出。第 12 条消息在 21000ms 输出,依此类推。
  • 一直延续这个模式。

在输出的消息中包含延迟。预期的输出如下所示(不带注释):

Hello World. 100  // At 100ms
Hello World. 100  // At 200ms
Hello World. 100  // At 300ms
Hello World. 100  // At 400ms
Hello World. 100  // At 500ms
Hello World. 200  // At 700ms
Hello World. 200  // At 900ms
Hello World. 200  // At 1100ms
...

要求: 只能用 setInterval (不能用 setTimeout),并且只能用一个 if 语句。

解决方案

因为只能用 setInterval ,所以在这里需要通过递归来增加下一次 setInterval 调用的延迟。另外还需要一个 if 语句,用来控制在对该递归函数的 5 次调用之后执行该操作。

下面的解决方案供你参考:

let lastIntervalId, counter = 5;const greeting = delay => {
  if (counter === 5) {
    clearInterval(lastIntervalId);
    lastIntervalId = setInterval(() => {
      console.log('Hello World. ', delay);
      greeting(delay + 100);
    }, delay);
    counter = 0;
  }counter += 1;
};greeting(100);

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 14 收藏 11 评论 0

疯狂的技术宅 收藏了文章 · 9月24日

Vuex 映射指南

作者:Moses Anumadu

翻译:疯狂的技术宅

原文:https://blog.logrocket.com/a-...

未经允许严禁转载

Vuex 是一把双刃剑。如果使用得当,使用 Vue 可以使你的工作更加轻松。如果不小心,它也会使你的代码混乱不堪。

使用 Vuex 之前,你应该先了解四个主要概念:state、getter、mutation 和 action。一个简单的 Vuex 状态在 store 中的这些概念中操作数据。 Vuex 中的映射提供了一种从中检索数据的好方法。

在文中,我将演示如何映射 Vuex 存储中的数据。如果你熟悉 Vuex 基础,那么这些内容将会帮你编写更简洁、更便于维护的代码。

本文假设你了解 Vue.js 和 Vuex 的基础知识。

Vuex 中的映射是什么?

Vuex 中的映射使你可以将 state 中的任何一种属性(state、getter、mutation 和 action)绑定到组件中的计算属性,并直接使用 state 中的数据。

下面是一个简单的 Vuex store 例子,其中测试数据位于 state 中。

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    data: "test data"
  }
})

如果要从 state 中访问 data 的值,则可以在 Vue.js 组件中执行以下操作。

computed: {
        getData(){
          return this.$store.state.data
        }
    }

上面的代码可以工作,但是随着 state 数据量的开始增长,很快就会变得很难看。

例如:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    user: {
        id:1,
        age:23,
        role:user
        data:{
          name:"user name",
          address:"user address"
        }
    },
    services: {},
    medical_requests: {},
    appointments: {},
    }
  }
})

要从处于 state 中的用户对象获取用户名:

computed: {
        getUserName(){
          return this.$store.state.user.data.name
        }
    }

这样可以完成工作,但是还有更好的方法。

映射 state

要将 state 映射到 Vue.js 组件中的计算属性,可以运行以下命令。

import { mapGetters } from 'vuex';

export default{
    computed: {
        ...mapState([
            'user',
        ])
    }
}

现在你可以访问组件中的整个用户对象。

你还可以做更多的事,例如把对象从 state 添加到 mapState 方法。

import { mapGetters } from 'vuex';

export default{
    computed: {
        ...mapState([
            'user',
            'services'
        ])
    }
}

如你所见,这要干净得多。可以通过以下方式轻松访问用户名:

{{user.data.name}}

services 对象和映射的许多其他的值也是如此。

你注意到我们是如何将数组传递给 mapState() 的吗?如果需要为值指定其他名称,则可以传入一个对象。

import { mapGetters } from 'vuex';

export default{
    computed: {
        ...mapState({
            userDetails:'user',
            userServices:'services'
        })
    }
}

现在可以通过简单地调用 userDetails 来引用 user

何时映射整个 state

根据经验,仅当 state 中有大量数据,并且组件中全部需要它们时,才应该映射。

在上面的例子中,如果我们只需要一个值(例如 username),则映射整个用户对象就没有多大意义。

在映射时,整个对象将会全部加载到内存中。实际上我们并不想继续把不需要的数据加载到内存中,因为这将是多余的,并且从长远来看会影响性能。

映射 getter

映射 getter 的语法与 mapState 函数相似。

import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters([
      'firstCount',
      'anotherGetter',
    ])
  }
}

与映射 state 类似,如果你打算使用其他名称,则可以把对象传递给 mapGetters 函数。

import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters([
      first:'firstCount',
      another:'anotherGetter',
    ])
  }
}

映射 mutation

映射 Mutation 时,可以在用以下语法来提交 Mutation。

this.$store.commit('mutationName`)

例如:

import { mapMutations } from 'vuex'

export default {
  methods: {
    ...mapMutations([
      'search', // 映射 `this.increment()` 到 `this.$store.commit('search')`

      // `mapMutations` 也支持 payloads:
      'searchBy' // 映射 `this.incrementBy(amount)` 到 `this.$store.commit('searchBy', amount)`
    ]),
    ...mapMutations({
      find: 'search' // 映射 `this.add()` 到 `this.$store.commit('search')`
    })
  }
}

映射 action

映射 action 与映射 mutation 非常相似,因为它也可以在方法中完成。使用映射器会把 this.$store.dispatch('actionName') 绑定到映射器数组中的名称或对象的键。

import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // 映射 `this.increment()` 到 `this.$store.dispatch('increment')`

      // `mapActions` 也支持 payloads:
      'incrementBy' // 映射 `this.incrementBy(amount)` 到 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment' // 映射 `this.add()` 到 `this.$store.dispatch('increment')`
    })
  }
}

总结

看到这里,你应该能够学到:

  • 对 Vuex 中的映射是如何工作的以及为什么要使用它有了深刻的了解
  • 能够映射 Vuex store 中的所有组件(state、 getter、 mutation、action)
  • 知道什么时候应该映射 store

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文