2023年4月

因为没看见答案,所以也不知道对不对。


JavaScript 的垃圾回收机制是由 JavaScript 引擎自动管理的,通常情况下我们无法控制垃圾回收机制的执行时间和频率。

然而,我们可以采取一些优化策略来减少垃圾回收的性能开销,从而提高代码执行速度。

  1. 减少全局变量
    :全局变量不易被垃圾回收,因为它们始终可达。尽量减少全局变量的使用,并使用局部变量和函数封装。
  2. 及时解除引用
    :当你不再需要一个对象时,及时解除对它的引用,使其不可达,从而让垃圾回收器可以回收其内存。例如,可以将变量设置为
    null
    ,或者可以使用
    delete
    操作符删除对象属性或将数组长度设置为 0,使它们变成空对象。
  3. 避免循环引用
    :循环引用可能导致内存泄漏。虽然现代垃圾回收算法可以处理循环引用,但最好避免产生循环引用。尤其在涉及DOM元素时,确保在移除元素前解除事件监听器等引用。
  4. 使用对象池
    :对于频繁创建和销毁的对象,可以使用对象池来减少垃圾回收的开销。对象池是一种管理对象生命周期的策略,可以重用不再使用的对象,减少内存分配和释放的次数。
  5. 避免内存泄漏
    :确保在编写代码时没有导致内存泄漏。内存泄漏会导致内存使用量持续增加,影响性能。使用开发者工具定期检查内存泄漏,并修复相关问题。
  6. 合理使用计时器
    :在代码中合理使用
    requestAnimationFrame

    setTimeout/setInterval
    等异步操作,以让垃圾回收器在空闲时间内执行。
  7. 优化数据结构和算法
    :使用更高效的数据结构和算法可以降低内存使用,减少垃圾回收的频率。例如,对于大型数据集合,可以使用分批处理的方式,减少一次性处理过多的数据量,以免导致内存占用过高。



下面是一个示例,演示如何避免创建过多的临时对象和变量,尽可能重用已有的对象和变量:

//创建一个数组,其中包含 1000 个对象
const arr = new Array(1000).fill({});//每次循环都会创建一个临时对象
for (let i = 0; i < arr.length; i++) {
const obj
= arr[i]; //每次都创建一个新的对象 //处理 obj }//重用对象 const obj ={};for (let i = 0; i < arr.length; i++) {
Object.assign(obj, arr[i]);
//将 arr[i] 中的属性复制到 obj 中 //处理 obj Object.keys(obj).forEach((key) => delete obj[key]); //清空 obj 中的属性,以便下次循环重用 }

在这个示例中,第一个循环每次都会创建一个新的对象
obj
,而第二个循环则重用了一个对象
obj
,避免了过多的临时对象和变量的创建,减少了内存分配和回收的次数,从而优化了垃圾回收机制的执行。



然后,还有一个例子展示了如何使用对象池优化内存管理。

我们将创建一个简单的粒子系统,粒子在屏幕上随机移动。在没有对象池的情况下,我们会不断创建和销毁粒子对象;使用对象池后,我们可以重用不再需要的粒子对象,从而减少垃圾回收的开销。

首先,我们创建一个粒子类:

class Particle {
constructor(x, y) {
this.x =x;this.y =y;this.vx = Math.random() * 2 - 1;this.vy = Math.random() * 2 - 1;
}

move() {
this.x += this.vx;this.y += this.vy;
}
}

接下来,我们创建一个对象池类:

class ObjectPool {
constructor(createFn) {
this.createFn =createFn;this.pool =[];
}

get(...args) {
if (this.pool.length > 0) {
const item
= this.pool.pop();this.createFn.apply(item, args);returnitem;
}
else{return new this.createFn(...args);
}
}

release(item) {
this.pool.push(item);
}
}

现在,我们使用对象池创建粒子:

const particlePool = newObjectPool(Particle);//创建粒子
const particle = particlePool.get(Math.random() * 100, Math.random() * 100);//更新粒子位置
particle.move();//粒子不再需要时,释放到对象池
particlePool.release(particle);

在这个例子中,我们使用对象池来创建和管理粒子对象。当需要新粒子时,我们从对象池中获取;当粒子不再需要时,我们将其释放回对象池。这样可以避免频繁创建和销毁对象,减少垃圾回收的性能开销。当然,这只是一个简单示例,实际项目中可能会涉及更多的优化措施。


虽然你不能直接控制垃圾回收过程,但采用以上策略可以优化内存使用,减少垃圾回收的性能开销,提高代码执行速度。请注意,不同的JavaScript引擎可能具有不同的垃圾回收策略,因此实际效果可能会有所不同。

哈喽大家好,我是咸鱼

我相信大家在面试过程中或多或少都会被问到这样一个问题:
你能解释一下什么是 socket 吗

我记得我当初的回答很是浅显:socket 也叫套接字,用来负责不同主机程序之间的网络通信连接,socket 的表现方式由四元组(ip地址:端口)组成

那么今天,咸鱼将跟大家打开 socket 的神秘大门,不但要搞清楚 socket 的概念,最好还能够了解它的底层实现

我们首先查看一下 socket 的翻译

我们看到,socket 可以翻译成插座、插头

那现在请想象这么一个场景:
给手机充电时,你将充电插头插入电源插座里面,是不是意味着插座与充电插头连接起来了

在计算机世界中,socket 翻译成套接字,通过 socket 我们可以与某台服务器进行连接,而建立连接的过程,你可以脑补成将充电插头插进插座的过程

socket 使用场景

假设我们想要将数据从 A 电脑的某个进程传送到 B 电脑的某个进程(比如咸鱼用微信发信息给冰冰)

那么在与对方聊天的过程中,其实就是这两台电脑中的微信进程相互传输数据的过程

在这个过程中,两台电脑各自调用 socket 方法,然后会得到一个 fd 句柄(socket_fd),这个 fd 句柄就相当于 socket 的身份证号

得到 fd 句柄之后:

  1. 服务端执行 bind()、listen()、accept() 方法等待客户端建立连接的请求

  2. 客户端执行 connect() 方法向服务端发起连接

  3. 连接建立起来之后,两端都可以执行 send()、recv() 方法来互相传递数据

PS:
对于不同的传输层协议,上面这个过程是不一样的,详情可以查看我之前的文章《
Python 网络编程

TCP 协议

UDP 协议

socket 底层设计

我们知道了 socket 是用来实现网络传输功能的,它负责不同主机进程之间的网络通信连接

我将上面的问题改一下,把
”socket 是什么“
改成
”如果让你来实现一个网络传输功能,你会怎么设计“

网络传输功能,简单点来讲就是两端服务器之间进行网络通信并互相收发数据,收发数据也就是读写数据

首先我们会遇到第一个问题:
茫茫互联网中你怎么能找到那台梦中情机

聪明的你肯定会想到——ip地址!我们用 ip 地址来定位电脑

找到了你的梦中情机之后,你会发现,
一台电脑上面这么多进程,我怎么才能找到与我通信的那个进程(比如说微信)

聪明的你很快就想到了用端口号(port)

可以这么理解,ip 地址是用来定位街区的,而端口号 port 对应这个街区中的门牌号,通过 ip +port 的组合,你可以在茫茫互联网中找到属于你的梦中情机并且与之通信

所以
你在设计网络传输功能初期,定义了一个数据结构 sock,sock 里面包含了 ip 和 port 字段
(假设用 C 语言实现)


在 Linux 中(以 CentOS 7举例),在头文件
/usr/include/netinet/in.h
可以看到负责套接字地址的 sock 结构体

sin_family 字段为 AF_INET,sin_port 表示端口号,sin_addr 表示 IPv4 地址,是一个
struct in_addr
类型的结构体

sin6_family 字段为 AF_INET6,sin6_port 表示端口号,sin6_addr 表示 IPv6 地址,是一个 struct in6_addr 类型的结构体

解决了定位问题之后,我们知道在计算机网络中有很多协议,这些协议规定了计算机之间的通信方式

比如你是选用可靠的 TCP 协议去进行网络通信,还是相对不可靠的 UDP 协议

不同的网络协议还对应着不同的网络通信场景,如果你选择了 TCP协议,你还得考虑例如滑动窗口、超时重传这些场景

所以有了 ip 和 port 还不行,你还
需要定义新的数据结构用来维护网络协议以及对应的网络场景

又因为不同的网络协议中有一些功能相似的方法(例如收发数据),于是你决定将不同协议中公共的部分提取出来,通过”继承“的方式来实现功能复用

所以可以先定义一个名为 sock 的数据结构,然后定义”继承“ sock 的各类 sock

PS:Linux 内核是用 C 语言实现的,在 C 语言中没有继承这个概念,你可以简单将这个继承理解成 xx_sock 基于 sock 进行了扩展,xx_sock 是 sock 的进阶版

  • sock
    :最基础的结构,用来维护任何网络协议都会用到的
    收发数据缓冲区
    (公用部分)

  • inet_sock
    :负责网络传输功能的 sock,在 sock 基础上加了 TTL(网络生存时间)、ip 和 port 这些跟网络传输相关的字段信息

  • inet_connection_sock
    :面向连接的 sock,在
    inet_sock
    基础上添加了面向连接的协议里相关字段,比如 accept 队列,数据包分片大小,握手失败,重试次数等;虽然我们现在提到面向连接的协议就是指 TCP,但从设计上 Linux 需要支持扩展其他面向连接的新协议,比如 SCTP 协议,所以说
    tcp_sock
    则是在这个基础上实现的真正的 TCP 协议专用 sock 结构

上面例子中的这些 sock 都可以在系统上直接找到,以 CentOS 7 为例

现在你用代码实现了这一堆数据结构——sock,不同的 sock 分别实现自己职责内的功能(负责面向连接的数据结构
inet_connection_sock
、负责 UDP 协议的数据结构
udp_sock
等等)

但是你需要这些 sock 去跟硬件网卡交互才能实现网络传输的功能,既然需要跟硬件交互,那就说明
需要比较高的操作系统权限

同时考虑到性能和安全,这套数据结构不能放在用户态,
需要给它放到
系统内核里面

既然这套数据结构在内核里,处在用户态的程序想要用这套数据结构来实现网络传输功能该怎么办呢?

除此之外,处在用户态的程序并不关心也不知道你这套数据结构在底层内核是怎么操作的,功能是怎么实现的,它只关心结果

于是你想到了用接口调用的方式——你
将一个个功能抽象一个个接口,以后别人只需要调用这些接口,就可以让内核中这一大堆复杂的数据结构去实现指定功能

又因为在
Linux 中一切皆文件
,你索性将这些 sock 封装成文件,当用户态的程序去调用你提供的接口时,需要先创建一个 sock 文件

这个新生成的 sock 文件有一个文件句柄 fd,用户态的程序只需要拿着这个 fd 就可以对内核中的 sock 进行操作

上面有说到,你将不同的数据结构(
inet_sock

tcp_sock
等等)抽象成一个个 API 接口,以后别人只需要调用这些 API 接口就可以驱动我们写好的这一大堆复杂的数据结构去进行网络传输

下面列出了一些常见的接口:

  • send

  • recv

  • bind

  • listen

  • connect

到这里,整个网络传输功能就已经基本实现了。上面列举出来的这些方法,其实就是 socket 提供出来的接口

到这里,我们对 socket 有了一个更深地了解——socket 其实相当于一个接口层,它处在内核态和用户态之间:

  • 向上用户态

    • 为处在用户态的程序提供 API 接口,方便用户态程序实现网络传输功能

  • 向下内核态

    • 对网卡进行操作,负责网络传输工作

或者你也可以这么理解,
处在用户态的程序通过 socket 提供的接口,将网络传输的这部分工作外包给了 Linux 内核

我们以 tcp 协议为例子来看下 python 中是如何操作 socket 的

在客户端中,程序首先调用 socket 提供的 socket 方法创建一个 socket 文件来获得 socket 句柄,然后调用 connect 方法,这时候内核会根据 socket_fd 找到对应的 sock 文件

再根据文件里的信息找到处在内核的 sock 结构,通过 sock 结构与服务端进行三次握手建立连接

连接建立好之后,客户端调用 send 方法来进行数据传输,sock 中定义了一个发送缓冲区和接收缓冲区,其实就是一个链表,链表上面放着一个个等待发送或接收的数据

总结

我们再次回到那个问题——socket 是什么?

sock(或 socket)是操作系统内核提供的一种数据结构,用于实现网络传输功能

基于不同的网络协议以及应用场景,衍生了各种类型的 sock

每个网络层协议都有相应的 sock 结构体来管理该层协议的连接状态和数据传输。各类 sock 操作硬件网卡,就实现了网络传输的功能

为了将这些功能让处在用户态的应用程序使用,不但引入了 socket 层,还将各类功能的实现方式抽象成了 API 接口,供应用程序调用

同时将 sock 封装成文件,应用程序就可以在用户层通过文件句柄(socket fd)来操作内核中 sock 的网络传输功能

这个 socket fd 是一个 int 类型的数字,而 socket 中文翻译叫做套接字,结合这个 socket fd,你是不是可以将其理解成:一套用于连接的数字

而 socket 分 Internet socket 和 UNIX Domain socket,两者都可以用于不同主机进程间的通信和本机进程间的通信

只是前者采用的是基于 IP 协议的网络通信方式,而后者采用的是基于本地文件系统的通信方式

关于 UNIX Domain socket,可以通过
netstat -x
查看

【机器学习入门与实践】数据挖掘-二手车价格交易预测(含EDA探索、特征工程、特征优化、模型融合等)

note:项目链接以及码源见文末

1.赛题简介

了解赛题

  • 赛题概况

  • 数据概况

  • 预测指标

  • 分析赛题

  • 数据读取pandas

  • 分类指标评价计算示例

  • 回归指标评价计算示例

EDA探索

  • 载入各种数据科学以及可视化库
  • 载入数据
  • 总览数据概况
  • 判断数据缺失和异常
  • 了解预测值的分布
  • 特征分为类别特征和数字特征,并对类别特征查看unique分布
  • 数字特征分析
  • 类别特征分析
  • 用pandas_profiling生成数据报告

特征工程

  • 导入数据
  • 删除异常值
  • 特征构造
  • 特征筛选

建模调参,相关原理介绍与推荐

  • 线性回归模型
  • 决策树模型
  • GBDT模型
  • XGBoost模型
  • LightGBM模型
  • 推荐教材
  • 读取数据
  • 线性回归 & 五折交叉验证 & 模拟真实业务情况
  • 多种模型对比
  • 模型调参

模型融合

  • 回归\分类概率-融合
  • 分类模型融合
  • 一些其它方法
  • 本赛题示例

1.1 数据说明

比赛要求参赛选手根据给定的数据集,建立模型,二手汽车的交易价格。

来自 Ebay Kleinanzeigen 报废的二手车,数量超过 370,000,包含 20 列变量信息,为了保证
比赛的公平性,将会从中抽取 10 万条作为训练集,5 万条作为测试集 A,5 万条作为测试集
B。同时会对名称、车辆类型、变速箱、model、燃油类型、品牌、公里数、价格等信息进行
脱敏。

一般而言,对于数据在比赛界面都有对应的数据概况介绍(匿名特征除外),说明列的性质特征。了解列的性质会有助于我们对于数据的理解和后续分析。
Tip:匿名特征,就是未告知数据列所属的性质的特征列。

train.csv

  • name - 汽车编码
  • regDate - 汽车注册时间
  • model - 车型编码
  • brand - 品牌
  • bodyType - 车身类型
  • fuelType - 燃油类型
  • gearbox - 变速箱
  • power - 汽车功率
  • kilometer - 汽车行驶公里
  • notRepairedDamage - 汽车有尚未修复的损坏
  • regionCode - 看车地区编码
  • seller - 销售方
  • offerType - 报价类型
  • creatDate - 广告发布时间
  • price - 汽车价格
  • v_0', 'v_1', 'v_2', 'v_3', 'v_4', 'v_5', 'v_6', 'v_7', 'v_8', 'v_9', 'v_10', 'v_11', 'v_12', 'v_13','v_14'(根据汽车的评论、标签等大量信息得到的embedding向量)【人工构造 匿名特征】

数字全都脱敏处理,都为label encoding形式,即数字形式

1.2预测指标

本赛题的评价标准为MAE(Mean Absolute Error):

$$
MAE=\frac{\sum_{i=1}^{n}\left|y_{i}-\hat{y}
{i}\right|}{n}
$$
其中$y

$代表第$i$个样本的真实值,其中$\hat{y}_{i}$代表第$i$个样本的预测值。

一般问题评价指标说明:

什么是评估指标:

评估指标即是我们对于一个模型效果的数值型量化。(有点类似与对于一个商品评价打分,而这是针对于模型效果和理想效果之间的一个打分)

一般来说分类和回归问题的评价指标有如下一些形式:

分类算法常见的评估指标如下:

  • 对于二类分类器/分类算法,评价指标主要有accuracy, [Precision,Recall,F-score,Pr曲线],ROC-AUC曲线。
  • 对于多类分类器/分类算法,评价指标主要有accuracy, [宏平均和微平均,F-score]。

对于回归预测类常见的评估指标如下:

  • 平均绝对误差(Mean Absolute Error,MAE),均方误差(Mean Squared Error,MSE),平均绝对百分误差(Mean Absolute Percentage Error,MAPE),均方根误差(Root Mean Squared Error), R2(R-Square)

平均绝对误差
平均绝对误差(Mean Absolute Error,MAE)
:平均绝对误差,其能更好地反映预测值与真实值误差的实际情况,其计算公式如下:
$$
MAE=\frac{1}{N} \sum_{i=1}^{N}\left|y_{i}-\hat{y}_{i}\right|
$$

均方误差
均方误差(Mean Squared Error,MSE)
,均方误差,其计算公式为:
$$
MSE=\frac{1}{N} \sum_{i=1}
{N}\left(y_{i}-\hat{y}_{i}\right)
$$

R2(R-Square)的公式为

残差平方和:
$$
SS_{res}=\sum\left(y_{i}-\hat{y}
{i}\right)^{2}
$$
总平均值:
$$
SS

=\sum\left(y_{i}-\overline{y}_{i}\right)^{2}
$$

其中$\overline{y}$表示$y$的平均值
得到$R^2$表达式为:
$$
R
{2}=1-\frac{SS_{res}}{SS_{tot}}=1-\frac{\sum\left(y_{i}-\hat{y}_{i}\right)
{2}}{\sum\left(y_{i}-\overline{y}\right)^{2}}
$$
$R^2$用于度量因变量的变异中可由自变量解释部分所占的比例,取值范围是 0~1,$R
2$越接近1,表明回归平方和占总平方和的比例越大,回归线与各观测点越接近,用x的变化来解释y值变化的部分就越多,回归的拟合程度就越好。所以$R
2$也称为拟合优度(Goodness of Fit)的统计量。

$y_{i}$表示真实值,$\hat{y}
{i}$表示预测值,$\overline{y}
$表示样本均值。得分越高拟合效果越好。

1.3分析赛题

  1. 此题为传统的数据挖掘问题,通过数据科学以及机器学习深度学习的办法来进行建模得到结果。
  2. 此题是一个典型的回归问题。
  3. 主要应用xgb、lgb、catboost,以及pandas、numpy、matplotlib、seabon、sklearn、keras等等数据挖掘常用库或者框架来进行数据挖掘任务。

2.数据探索

# 下载数据
!wget http://tianchi-media.oss-cn-beijing.aliyuncs.com/dragonball/DM/data.zip
# 解压下载好的数据
!unzip data.zip
# 导入函数工具
## 基础工具
import numpy as np
import pandas as pd
import warnings
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.special import jn
from IPython.display import display, clear_output
import time

warnings.filterwarnings('ignore')
%matplotlib inline

## 模型预测的
from sklearn import linear_model
from sklearn import preprocessing
from sklearn.svm import SVR
from sklearn.ensemble import RandomForestRegressor,GradientBoostingRegressor

## 数据降维处理的
from sklearn.decomposition import PCA,FastICA,FactorAnalysis,SparsePCA

import lightgbm as lgb
import xgboost as xgb

## 参数搜索和评价的
from sklearn.model_selection import GridSearchCV,cross_val_score,StratifiedKFold,train_test_split
from sklearn.metrics import mean_squared_error, mean_absolute_error

2.1 数据读取

## 通过Pandas对于数据进行读取 (pandas是一个很友好的数据读取函数库)
Train_data = pd.read_csv('/home/aistudio/dataset/used_car_train_20200313.csv', sep=' ')
TestA_data = pd.read_csv('/home/aistudio/dataset/used_car_testA_20200313.csv', sep=' ')

## 输出数据的大小信息
print('Train data shape:',Train_data.shape)
print('TestA data shape:',TestA_data.shape)
Train data shape: (150000, 31)
TestA data shape: (50000, 30)

2.2 数据简要浏览

## 通过.head() 简要浏览读取数据的形式
Train_data.head()
SaleID name regDate model brand bodyType fuelType gearbox power kilometer ... v_5 v_6 v_7 v_8 v_9 v_10 v_11 v_12 v_13 v_14
0 0 736 20040402 30.0 6 1.0 0.0 0.0 60 12.5 ... 0.235676 0.101988 0.129549 0.022816 0.097462 -2.881803 2.804097 -2.420821 0.795292 0.914762
1 1 2262 20030301 40.0 1 2.0 0.0 0.0 0 15.0 ... 0.264777 0.121004 0.135731 0.026597 0.020582 -4.900482 2.096338 -1.030483 -1.722674 0.245522
2 2 14874 20040403 115.0 15 1.0 0.0 0.0 163 12.5 ... 0.251410 0.114912 0.165147 0.062173 0.027075 -4.846749 1.803559 1.565330 -0.832687 -0.229963
3 3 71865 19960908 109.0 10 0.0 0.0 1.0 193 15.0 ... 0.274293 0.110300 0.121964 0.033395 0.000000 -4.509599 1.285940 -0.501868 -2.438353 -0.478699
4 4 666666080 20120103 110.0 5 1.0 0.0 0.0 68 5.0 ... 0.228036 0.073205 0.091880 0.078819 0.121534 -1.896240 0.910783 0.936666660 2.834518 1.923482

5 rows × 31 columns

2.3 数据信息查看

## 通过 .info() 简要可以看到对应一些数据列名,以及NAN缺失信息
Train_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 150000 entries, 0 to 149999
Data columns (total 31 columns):
 #   Column             Non-Null Count   Dtype  
---  ------             --------------   -----  
 0   SaleID             150000 non-null  int64  
 1   name               150000 non-null  int64  
 2   regDate            150000 non-null  int64  
 3   model              149999 non-null  float64
 4   brand              150000 non-null  int64  
 5   bodyType           145494 non-null  float64
 6   fuelType           141320 non-null  float64
 7   gearbox            144019 non-null  float64
 8   power              150000 non-null  int64  
 9   kilometer          150000 non-null  float64
 10  notRepairedDamage  150000 non-null  object 
 11  regionCode         150000 non-null  int64  
 12  seller             150000 non-null  int64  
 13  offerType          150000 non-null  int64  
 14  creatDate          150000 non-null  int64  
 15  price              150000 non-null  int64  
 16  v_0                150000 non-null  float64
 17  v_1                150000 non-null  float64
 18  v_2                150000 non-null  float64
 19  v_3                150000 non-null  float64
 20  v_4                150000 non-null  float64
 21  v_5                150000 non-null  float64
 22  v_6                150000 non-null  float64
 23  v_7                150000 non-null  float64
 24  v_8                150000 non-null  float64
 25  v_9                150000 non-null  float64
 26  v_10               150000 non-null  float64
 27  v_11               150000 non-null  float64
 28  v_12               150000 non-null  float64
 29  v_13               150000 non-null  float64
 30  v_14               150000 non-null  float64
dtypes: float64(20), int64(10), object(1)
memory usage: 35.5+ MB
## 通过 .columns 查看列名
Train_data.columns
Index(['SaleID', 'name', 'regDate', 'model', 'brand', 'bodyType', 'fuelType',
       'gearbox', 'power', 'kilometer', 'notRepairedDamage', 'regionCode',
       'seller', 'offerType', 'creatDate', 'price', 'v_0', 'v_1', 'v_2', 'v_3',
       'v_4', 'v_5', 'v_6', 'v_7', 'v_8', 'v_9', 'v_10', 'v_11', 'v_12',
       'v_13', 'v_14'],
      dtype='object')
TestA_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 50000 entries, 0 to 49999
Data columns (total 30 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   SaleID             50000 non-null  int64  
 1   name               50000 non-null  int64  
 2   regDate            50000 non-null  int64  
 3   model              50000 non-null  float64
 4   brand              50000 non-null  int64  
 5   bodyType           48587 non-null  float64
 6   fuelType           47107 non-null  float64
 7   gearbox            48090 non-null  float64
 8   power              50000 non-null  int64  
 9   kilometer          50000 non-null  float64
 10  notRepairedDamage  50000 non-null  object 
 11  regionCode         50000 non-null  int64  
 12  seller             50000 non-null  int64  
 13  offerType          50000 non-null  int64  
 14  creatDate          50000 non-null  int64  
 15  v_0                50000 non-null  float64
 16  v_1                50000 non-null  float64
 17  v_2                50000 non-null  float64
 18  v_3                50000 non-null  float64
 19  v_4                50000 non-null  float64
 20  v_5                50000 non-null  float64
 21  v_6                50000 non-null  float64
 22  v_7                50000 non-null  float64
 23  v_8                50000 non-null  float64
 24  v_9                50000 non-null  float64
 25  v_10               50000 non-null  float64
 26  v_11               50000 non-null  float64
 27  v_12               50000 non-null  float64
 28  v_13               50000 non-null  float64
 29  v_14               50000 non-null  float64
dtypes: float64(20), int64(9), object(1)
memory usage: 11.4+ MB

2.4 数据统计信息浏览

## 通过 .describe() 可以查看数值特征列的一些统计信息
Train_data.describe()
SaleID name regDate model brand bodyType fuelType gearbox power kilometer ... v_5 v_6 v_7 v_8 v_9 v_10 v_11 v_12 v_13 v_14
count 150000.000000 150000.000000 1.500000e+05 149999.000000 150000.000000 145494.000000 141320.000000 144019.000000 150000.000000 150000.000000 ... 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000
mean 74999.500000 68349.172873 2.003417e+07 47.129021 8.052733 1.792369 0.375842 0.224943 119.316547 12.597160 ... 0.248204 0.044923 0.124692 0.058144 0.061996 -0.001000 0.009035 0.004813 0.000313 -0.000688
std 43301.414527 61103.875095 5.364988e+04 49.536040 7.864956 1.760640 0.548677 0.417546 177.168419 3.919576 ... 0.045804 0.051743 0.201410 0.029186 0.035692 3.772386 3.286071 2.517478 1.288988 1.038685
min 0.000000 0.000000 1.991000e+07 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.500000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 -9.168192 -5.558207 -9.639552 -4.153899 -6.546556
25% 37499.750000 66666656.000000 1.999091e+07 10.000000 1.000000 0.000000 0.000000 0.000000 75.000000 12.500000 ... 0.243615 0.000038 0.062474 0.035334 0.033930 -3.722303 -1.951543 -1.871846 -1.057789 -0.437034
50% 74999.500000 51638.000000 2.003091e+07 30.000000 6.000000 1.000000 0.000000 0.000000 110.000000 15.000000 ... 0.257798 0.000812 0.095866 0.057014 0.058484 1.624076 -0.358053 -0.130753 -0.036245 0.141246
75% 112499.250000 118841.250000 2.007666666e+07 66.000000 13.000000 3.000000 1.000000 0.000000 150.000000 15.000000 ... 0.265297 0.102009 0.125243 0.079382 0.087491 2.844357 1.255022 1.776933 0.942813 0.680378
max 149999.000000 196812.000000 2.015121e+07 247.000000 39.000000 7.000000 6.000000 1.000000 19312.000000 15.000000 ... 0.291838 0.151420 1.404936 0.160791 0.222787 12.357011 18.819042 13.847792 11.147669 8.658418

8 rows × 30 columns

TestA_data.describe()
SaleID name regDate model brand bodyType fuelType gearbox power kilometer ... v_5 v_6 v_7 v_8 v_9 v_10 v_11 v_12 v_13 v_14
count 50000.000000 50000.000000 5.000000e+04 50000.000000 50000.000000 48587.000000 47107.000000 48090.000000 50000.000000 50000.000000 ... 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000
mean 174999.500000 68542.223280 2.003393e+07 46.844520 8.056240 1.782185 0.373405 0.224350 119.883620 12.595580 ... 0.248669 0.045021 0.122744 0.057997 0.062000 -0.017855 -0.013742 -0.013554 -0.003147 0.001516
std 14433.901067 61052.808133 5.368870e+04 49.469548 7.819477 1.760736 0.546442 0.417158 185.097387 3.908979 ... 0.044601 0.051766 0.195972 0.029211 0.035653 3.747985 3.231258 2.515962 1.286597 1.027360
min 150000.000000 0.000000 1.991000e+07 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.500000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 -9.160049 -5.411964 -8.916949 -4.123333 -6.112667
25% 162499.750000 11203.500000 1.999091e+07 10.000000 1.000000 0.000000 0.000000 0.000000 75.000000 12.500000 ... 0.243762 0.000044 0.062644 0.035084 0.033714 -3.700121 -1.971325 -1.876703 -1.060428 -0.437920
50% 174999.500000 52248.500000 2.003091e+07 29.000000 6.000000 1.000000 0.000000 0.000000 109.000000 15.000000 ... 0.257877 0.000815 0.095828 0.057084 0.058764 1.613212 -0.355843 -0.142779 -0.035956 0.138799
75% 187499.250000 118856.500000 2.007110e+07 65.000000 13.000000 3.000000 1.000000 0.000000 150.000000 15.000000 ... 0.265328 0.102025 0.125438 0.079077 0.087489 2.832708 1.262914 1.764335 0.941469 0.681163
max 199999.000000 196805.000000 2.015121e+07 246.000000 39.000000 7.000000 6.000000 1.000000 20000.000000 15.000000 ... 0.291618 0.153265 1.358813 0.156355 0.214775 12.338872 18.856218 12.950498 5.913273 2.624622

8 rows × 29 columns

3.数据分析

#### 1) 提取数值类型特征列名
numerical_cols = Train_data.select_dtypes(exclude = 'object').columns
print(numerical_cols)
Index(['SaleID', 'name', 'regDate', 'model', 'brand', 'bodyType', 'fuelType',
       'gearbox', 'power', 'kilometer', 'regionCode', 'seller', 'offerType',
       'creatDate', 'price', 'v_0', 'v_1', 'v_2', 'v_3', 'v_4', 'v_5', 'v_6',
       'v_7', 'v_8', 'v_9', 'v_10', 'v_11', 'v_12', 'v_13', 'v_14'],
      dtype='object')
categorical_cols = Train_data.select_dtypes(include = 'object').columns
print(categorical_cols)
Index(['notRepairedDamage'], dtype='object')
#### 2) 构建训练和测试样本
## 选择特征列
feature_cols = [col for col in numerical_cols if col not in ['SaleID','name','regDate','creatDate','price','model','brand','regionCode','seller']]
feature_cols = [col for col in feature_cols if 'Type' not in col]

## 提前特征列,标签列构造训练样本和测试样本
X_data = Train_data[feature_cols]
Y_data = Train_data['price']

X_test  = TestA_data[feature_cols]

print('X train shape:',X_data.shape)
print('X test shape:',X_test.shape)
X train shape: (150000, 18)
X test shape: (50000, 18)
## 定义了一个统计函数,方便后续信息统计
def Sta_inf(data):
    print('_min',np.min(data))
    print('_max:',np.max(data))
    print('_mean',np.mean(data))
    print('_ptp',np.ptp(data))
    print('_std',np.std(data))
    print('_var',np.var(data))
#### 3) 统计标签的基本分布信息
print('Sta of label:')
Sta_inf(Y_data)
Sta of label:
_min 11
_max: 99999
_mean 5923.327333333334
_ptp 99988
_std 7501.973469876635
_var 56279605.942732885
## 绘制标签的统计图,查看标签分布
plt.hist(Y_data)
plt.show()
plt.close()

#### 4) 缺省值用-1填补
X_data = X_data.fillna(-1)
X_test = X_test.fillna(-1)

4. 模型训练与预测(特征工程、模型融合)

4.1 利用xgb进行五折交叉验证查看模型的参数效果

## xgb-Model
xgr = xgb.XGBRegressor(n_estimators=120, learning_rate=0.1, gamma=0, subsample=0.8,\
        colsample_bytree=0.9, max_depth=7) #,objective ='reg:squarederror'

scores_train = []
scores = []

## 5折交叉验证方式
sk=StratifiedKFold(n_splits=5,shuffle=True,random_state=0)
for train_ind,val_ind in sk.split(X_data,Y_data):
    
    train_x=X_data.iloc[train_ind].values
    train_y=Y_data.iloc[train_ind]
    val_x=X_data.iloc[val_ind].values
    val_y=Y_data.iloc[val_ind]
    
    xgr.fit(train_x,train_y)
    pred_train_xgb=xgr.predict(train_x)
    pred_xgb=xgr.predict(val_x)
    
    score_train = mean_absolute_error(train_y,pred_train_xgb)
    scores_train.append(score_train)
    score = mean_absolute_error(val_y,pred_xgb)
    scores.append(score)

print('Train mae:',np.mean(score_train))
print('Val mae',np.mean(scores))

4.2 定义xgb和lgb模型函数

def build_model_xgb(x_train,y_train):
    model = xgb.XGBRegressor(n_estimators=150, learning_rate=0.1, gamma=0, subsample=0.8,\
        colsample_bytree=0.9, max_depth=7) #, objective ='reg:squarederror'
    model.fit(x_train, y_train)
    return model

def build_model_lgb(x_train,y_train):
    estimator = lgb.LGBMRegressor(num_leaves=127,n_estimators = 150)
    param_grid = {
        'learning_rate': [0.01, 0.05, 0.1, 0.2],
    }
    gbm = GridSearchCV(estimator, param_grid)
    gbm.fit(x_train, y_train)
    return gbm

4.3 切分数据集(Train,Val)进行模型训练,评价和预测

## Split data with val
x_train,x_val,y_train,y_val = train_test_split(X_data,Y_data,test_size=0.3)
print('Train lgb...')
model_lgb = build_model_lgb(x_train,y_train)
val_lgb = model_lgb.predict(x_val)
MAE_lgb = mean_absolute_error(y_val,val_lgb)
print('MAE of val with lgb:',MAE_lgb)

print('Predict lgb...')
model_lgb_pre = build_model_lgb(X_data,Y_data)
subA_lgb = model_lgb_pre.predict(X_test)
print('Sta of Predict lgb:')
Sta_inf(subA_lgb)
print('Train xgb...')
model_xgb = build_model_xgb(x_train,y_train)
val_xgb = model_xgb.predict(x_val)
MAE_xgb = mean_absolute_error(y_val,val_xgb)
print('MAE of val with xgb:',MAE_xgb)

print('Predict xgb...')
model_xgb_pre = build_model_xgb(X_data,Y_data)
subA_xgb = model_xgb_pre.predict(X_test)
print('Sta of Predict xgb:')
Sta_inf(subA_xgb)

4.4进行两模型的结果加权融合

## 这里我们采取了简单的加权融合的方式
val_Weighted = (1-MAE_lgb/(MAE_xgb+MAE_lgb))*val_lgb+(1-MAE_xgb/(MAE_xgb+MAE_lgb))*val_xgb
val_Weighted[val_Weighted<0]=10 # 由于我们发现预测的最小值有负数,而真实情况下,price为负是不存在的,由此我们进行对应的后修正
print('MAE of val with Weighted ensemble:',mean_absolute_error(y_val,val_Weighted))
sub_Weighted = (1-MAE_lgb/(MAE_xgb+MAE_lgb))*subA_lgb+(1-MAE_xgb/(MAE_xgb+MAE_lgb))*subA_xgb

## 查看预测值的统计进行
plt.hist(Y_data)
plt.show()
plt.close()

4.5.输出结果

sub = pd.DataFrame()
sub['SaleID'] = TestA_data.SaleID
sub['price'] = sub_Weighted
sub.to_csv('./sub_Weighted.csv',index=False)
sub.head()

5. 项目详细展开

因篇幅内容限制,将原学习项目拆解成多个notebook方便学习,只需一键fork。

5.1 数据分析详解

  1. 载入各种数据科学以及可视化库:
    • 数据科学库 pandas、numpy、scipy;
    • 可视化库 matplotlib、seabon;
    • 其他;
  2. 载入数据:
    • 载入训练集和测试集;
    • 简略观察数据(head()+shape);
  3. 数据总览:
    • 通过describe()来熟悉数据的相关统计量
    • 通过info()来熟悉数据类型
  4. 判断数据缺失和异常
    • 查看每列的存在nan情况
    • 异常值检测
  5. 了解预测值的分布
    • 总体分布概况(无界约翰逊分布等)
    • 查看skewness and kurtosis
    • 查看预测值的具体频数
  6. 特征分为类别特征和数字特征,并对类别特征查看unique分布
  7. 数字特征分析
    • 相关性分析
    • 查看几个特征得 偏度和峰值
    • 每个数字特征得分布可视化
    • 数字特征相互之间的关系可视化
    • 多变量互相回归关系可视化
  8. 类型特征分析
    • unique分布
    • 类别特征箱形图可视化
    • 类别特征的小提琴图可视化
    • 类别特征的柱形图可视化类别
    • 特征的每个类别频数可视化(count_plot)
  9. 用pandas_profiling生成数据报告

5.2 特征工程

  1. 异常处理:
    • 通过箱线图(或 3-Sigma)分析删除异常值;
    • BOX-COX 转换(处理有偏分布);
    • 长尾截断;
  2. 特征归一化/标准化:
    • 标准化(转换为标准正态分布);
    • 归一化(抓换到 [0,1] 区间);
    • 针对幂律分布,可以采用公式: $log(\frac{1+x}{1+median})$
  3. 数据分桶:
    • 等频分桶;
    • 等距分桶;
    • Best-KS 分桶(类似利用基尼指数进行二分类);
    • 卡方分桶;
  4. 缺失值处理:
    • 不处理(针对类似 XGBoost 等树模型);
    • 删除(缺失数据太多);
    • 插值补全,包括均值/中位数/众数/建模预测/多重插补/压缩感知补全/矩阵补全等;
    • 分箱,缺失值一个箱;
  5. 特征构造:
    • 构造统计量特征,报告计数、求和、比例、标准差等;
    • 时间特征,包括相对时间和绝对时间,节假日,双休日等;
    • 地理信息,包括分箱,分布编码等方法;
    • 非线性变换,包括 log/ 平方/ 根号等;
    • 特征组合,特征交叉;
    • 仁者见仁,智者见智。
  6. 特征筛选
    • 过滤式(filter):先对数据进行特征选择,然后在训练学习器,常见的方法有 Relief/方差选择发/相关系数法/卡方检验法/互信息法;
    • 包裹式(wrapper):直接把最终将要使用的学习器的性能作为特征子集的评价准则,常见方法有 LVM(Las Vegas Wrapper) ;
    • 嵌入式(embedding):结合过滤式和包裹式,学习器训练过程中自动进行了特征选择,常见的有 lasso 回归;
  7. 降维
    • PCA/ LDA/ ICA;
    • 特征选择也是一种降维。

5.3 模型优化

  1. 线性回归模型:
    • 线性回归对于特征的要求;
    • 处理长尾分布;
    • 理解线性回归模型;
  2. 模型性能验证:
    • 评价函数与目标函数;
    • 交叉验证方法;
    • 留一验证方法;
    • 针对时间序列问题的验证;
    • 绘制学习率曲线;
    • 绘制验证曲线;
  3. 嵌入式特征选择:
    • Lasso回归;
    • Ridge回归;
    • 决策树;
  4. 模型对比:
    • 常用线性模型;
    • 常用非线性模型;
  5. 模型调参:
    • 贪心调参方法;
    • 网格调参方法;
    • 贝叶斯调参方法;

5.4模型融合

  1. 简单加权融合:


    • 回归(分类概率):算术平均融合(Arithmetic mean),几何平均融合(Geometric mean);
    • 分类:投票(Voting)
    • 综合:排序融合(Rank averaging),log融合
  2. stacking/blending:


    • 构建多层模型,并利用预测结果再拟合预测。
  3. boosting/bagging(在xgboost,Adaboost,GBDT中已经用到):


    • 多树的提升方法

    训练:

预测:

6.总结

二手车预测项目是非常经典项目,数据挖掘实践(二手车价格预测)的内容来自 Datawhale与天池联合发起的,现在通过整理和调整让更多对机器学习感兴趣可以上手实战一下

因篇幅内容限制,将原学习项目拆解成多个notebook方便学习,只需一键fork。

项目链接:

一键fork直接运行,所有项目码源都在里面

https://www.heywhale.com/mw/project/64367e0a2a3d6dc93d22054f

机器学习数据挖掘专栏:
https://www.heywhale.com/home/column/64141d6b1c8c8b518ba97dcc

参考链接:

https://github.com/datawhalechina/team-learning-data-mining/tree/master/SecondHandCarPriceForecast

小仙男前端代码风格规范指南v1.0

JavaScript Standard Style
npm version
eslint
standard
standard
npm version
npm version
version
update

概述

  • 本规范是适用于小仙男团队及前端团队所搭建的各种前端框架代码的通用风格规范指南;
  • 使用时,请遵循指南细则进行代码风格约束,并在提交之前确保进行代码风格的修正操作;
  • 本规范参考WEB行业知名的
    JavaScript Standard Style
    规范指南。并将在后续使用过程中,逐步进行符合团队需求的调整和变更;
  • 掌握本规范的最好方法是安装并在自己的代码中使用它。

安装与使用

安装说明

- 两套方案

按照
Standard
官方建议,项目开启本规范验证后,将不再需要
ESlint

Prettier
进行代码格式验证,可减少各种配置文件(
方案一
)。但为了避免冲突、做好兼容,并保留一定调整余地,本规范中有
方案二
作为灵活集成方案:

  • 【方案一】(以下称
    Standard版
    )
    在开启本规范验证前,删除与
    ESlint

    Prettier
    两个插件相关的依赖注册及项目根目录的相应配置文件;
  • 【方案二】(以下称
    ESLint版
    )
    保留
    ESlint

    Prettier
    两个插件的依赖及配置,在
    ESlint
    中引入
    eslint-config-standard
    配置,并来将个人配置包装在上层。

- 方案说明

  • standard
    规范制定的目的,就是让大家都不再花时间浪费在无谓的代码风格之争上面。因此:
  • 方案一
    将全盘接受
    standard
    标准,并且不支持更改;
  • 方案二
    则支持在将本规范集成到
    ESlint
    之中,并支持自定义个人配置;
  • 本规范建议:方案一仅做记录,不支持项目应用;各框架选择方案二实行。
  • 本规范强烈建议:为保证团队规范的一致性,各项目【不要】自行覆盖与本规范相关的要求。

- 其他说明

  • 对于开启本规范的项目,将会在项目
    package.json
    中,直接按照
    方案一/二
    预先配置好相关操作,前后端开发人员直接使用即可。

  • 下面全局安装方案,仅提供前端开发人员参考,方便在任何地方进行规范检测。

  • 全局安装示例

    npm i -g standard # standard基础包
    npm i -g eslint-plugin-html@3.2.2 #用于检测html或vue文件<script></script>中的代码,必须是3x版本
    npm i -g snazzy #用户对输出错误进行美化(非强制安装)
    

【方案一】Standard版(删除ESlint版方案)

仅做记录,暂不支持项目应用,前期使用请暂时参考
方案二

- 安装依赖

  • 项目安装

    # 进入项目目录
    npm i -D standard # standard基础包
    npm i -D eslint-plugin-html@3.2.2 #用于检测html或vue文件<script></script>中的代码,必须是3x版本
    npm i -D husky@next #安装husky git 钩子,用于强制git提交时进行检查
    npm i -D snazzy #用户对输出错误进行美化(非强制安装)
    

- 配置

  • 安装好依赖资源后,更改项目的 package.json 文件

    // package.json
    {
      "scripts": {
        "standard": "standard ${file} | snazzy",
        "fix": "standard ${file} --fix"
      },
      "standard": {
        // 声明各种挂载到window上使用的全局变量,避免弹出变量undefined问题
        "globals": [
          "localStorage", "screen", "sessionStorage", "alert", "location", "Notification"
        ]
      },
      "husky": {
        "hooks": {
          "pre-commit": "standard \"src/**/*.{js,vue}\" | snazzy",
        }
      }
    }
    

- 使用

  • 略(暂不支持项目应用)

【方案二】ESlitnt版 (集成ESlint版方案)

- 安装依赖

  • 项目安装

    # 进入项目目录
    "babel-eslint": "10.1.0",
    "eslint": "^7.0.0",
    "eslint-config-standard": "^14.1.1",
    "eslint-plugin-vue": "^7.0.0",
    "lint-staged": "8.1.5",
    "husky": "1.3.1",npm
    

- 配置

  • 安装好依赖资源后,更改项目的
    package.json
    文件

    // package.json
    {
      "scripts": {
        "lint": "eslint --ext .js,.vue src",
        "lintfix": "eslint --fix --ext .js,.vue src",
      },
      "lint-staged": {
        "src/**/*.{js,vue}": [
          "eslint --fix",
          "git add"
        ]
      },
      "husky": {
        "hooks": {
          "pre-commit": "lint-staged"
        }
      }
    }
    
  • 更改项目的
    .eslintrc.js
    文件


    • rules
      中调整部分框架关闭的规则,为【试行版】,请勿私自调整。
    • 具体忽略的
      rules
      ,请以各端公共框架的
      .eslintrc.js
      为准

    // .eslintrc.js
    module.exports = {
      root: true,
      parserOptions: {
        parser: "babel-eslint",
        sourceType: "module",
      },
      env: {
        browser: true,
        node: true,
        es6: true,
      },
      // extends: ['plugin:vue/recommended', 'eslint:recommended'],
      extends: ["plugin:vue/essential", "standard"],
    
      rules: {
        // 【试行版】禁用规则,请勿自行调整。
        eqeqeq: ["off"],
        "no-template-curly-in-string": ["off"],
        "vue/script-setup-uses-vars": ["off"],
        "vue/no-mutating-props": ["off"],
        "no-new": ["off"],
      },
    };
    

- 使用

  • 可使用命令行操作,进行规则的检测和修复

    npm run lint # 检测项目语法规范
    npm run lintfix # 修复不合规语法
    
  • 除此之外,还可根据编译器配置自动检测和修复(下文
    FAQ
    以常见的
    vscode

    webstorm
    为例介绍)

FAQ

一、常用编译器配置

(略)

二、如何隐藏某类警告?

  1. JavaScript Standard 代码规范底层使用的是 ESLint。所以如果想隐藏某些警告,方法和使用 ESLint 时一样。
  2. 隐藏代码示例
  // 对某一行禁用所有规则:
  file = 'I know what I am doing' // eslint-disable-line

  // 或者,只禁用 "no-use-before-define" 这条规则:
  file = 'I know what I am doing' // eslint-disable-line no-use-before-define

  // 或者,对多行禁用 "no-use-before-define" 这一规则:
  /* eslint-disable no-use-before-define */
  console.log('offending code goes here...')
  console.log('offending code goes here...')
  console.log('offending code goes here...')
  /* eslint-enable no-use-before-define */

  // 禁用某个文件的所有校验,直接在文件头部添加:
  /* eslint-disable camelcase */

三、如何排除某些文件?

  1. ESLint
    版直接按照
    ESLint
    配置即可
  2. Standard
    版可以在
    package.json
    中增加
    standard.ignore
    属性配置:
  "standard": {
    "ignore": [
      "**/out/",
      "tmp.js",
      "......"
    ]
  }

四、使用全局变量,如何避免 "variable is not defined" 的错误提示?

  1. 经过测试,
    ESLint
    版不会出现这个问题。
  2. Standard
    版可以在
    package.json
    中增加
    standard.globals
    属性配置:
  "standard": {
    "globals": [ "alert", "location", "……" ]
  }

五、如果我不同意某条规则,可以改吗?

  1. 说一下整体原则:
    Standard
    规范的目的就是让大家都不再花时间浪费在无谓的代码风格之争上面。
    因此,原生
    Standard
    版不支持任何自定义规则。
  2. ESLint
    版由于是在ESLint中引入了
    eslint-config-standard
    插件来实现规范约束,因为可以在
    .eslintrc.js
    文件中覆盖
    rules

    但为了框架规范的一致性,除框架层面统一调整规范,各项目请勿私自调整。
  3. 具体
    ESLint
    版忽略的
    rules
    ,请以各端公共框架的
    .eslintrc.js
    为准

※ 其他FAQ

篇幅有限,其他未尽事宜,请直接查阅
Standard
官网:
JavaScript Standard Style

代码规范细则

  • 使用两个空格
    进行缩进。

    eslint:
    indent

    function hello (name) {
      console.log('hi', name)
    }
    
  • 除需要转义的情况外,
    字符串统一使用单引号

    eslint:
    quotes

    console.log('hello there')
    $("<div class='box'>")
    
  • 不要定义未使用的变量

    eslint:
    no-unused-vars

    function myFunction () {
      var result = something()   // ✗ avoid
    }
    
  • 关键字后面加空格

    eslint:
    keyword-spacing

    if (condition) { ... }   // ✓ ok
    if(condition) { ... }    // ✗ avoid
    
  • 函数声明时括号与函数名间加空格

    eslint:
    space-before-function-paren

    function name (arg) { ... }   // ✓ ok
    function name(arg) { ... }    // ✗ avoid
    
    run(function () { ... })      // ✓ ok
    run(function() { ... })       // ✗ avoid
    
  • 始终使用
    ===
    替代
    ==

    例外:
    obj == null
    可以用来检查
    null || undefined

    eslint:
    eqeqeq

    if (name === 'John')   // ✓ ok
    if (name == 'John')    // ✗ avoid
    

    if (name !== 'John')   // ✓ ok
    if (name != 'John')    // ✗ avoid
    
  • 字符串拼接操作符 (Infix operators)
    之间要留空格。

    eslint:
    space-infix-ops

    // ✓ ok
    var x = 2
    var message = 'hello, ' + name + '!'
    

    // ✗ avoid
    var x=2
    var message = 'hello, '+name+'!'
    
  • 逗号后面加空格

    eslint:
    comma-spacing

    // ✓ ok
    var list = [1, 2, 3, 4]
    function greet (name, options) { ... }
    

    // ✗ avoid
    var list = [1,2,3,4]
    function greet (name,options) { ... }
    
  • else 关键字要与花括号
    保持在同一行。

    eslint:
    brace-style

    // ✓ ok
    if (condition) {
      // ...
    } else {
      // ...
    }
    

    // ✗ avoid
    if (condition)
    {
      // ...
    }
    else
    {
      // ...
    }
    
  • 多行 if 语句的
    的括号不能省。

    eslint:
    curly

    // ✓ ok
    if (options.quiet !== true) console.log('done')
    

    // ✓ ok
    if (options.quiet !== true) {
      console.log('done')
    }
    

    // ✗ avoid
    if (options.quiet !== true)
    console.log('done')
    
  • 不要丢掉
    异常处理中
    err
    参数。

    eslint:
    handle-callback-err

    // ✓ ok
    run(function (err) {
      if (err) throw err
      window.alert('done')
    })
    

    // ✗ avoid
    run(function (err) {
      window.alert('done')
    })
    
  • 使用浏览器全局变量时加上
    window.
    前缀。
    document

    console

    navigator
    除外。

    eslint:
    no-undef

    window.alert('hi')   // ✓ ok
    
  • 不允许有连续多行空行

    eslint:
    no-multiple-empty-lines

    // ✓ ok
    var value = 'hello world'
    console.log(value)
    

    // ✗ avoid
    var value = 'hello world'
    
    console.log(value)
    
  • 对于三元运算符
    ?

    :
    与他们所负责的代码处于同一行

    eslint:
    operator-linebreak

    // ✓ ok
    var location = env.development ? 'localhost' : 'www.api.com'
    
    // ✓ ok
    var location = env.development
      ? 'localhost'
      : 'www.api.com'
    
    // ✗ avoid
    var location = env.development ?
      'localhost' :
      'www.api.com'
    
  • 每个 var 关键字
    单独声明一个变量。

    eslint:
    one-var

    // ✓ ok
    var silent = true
    var verbose = true
    
    // ✗ avoid
    var silent = true, verbose = true
    
    // ✗ avoid
    var silent = true,
        verbose = true
    
  • 条件语句中赋值语句
    使用括号包起来。这样使得代码更加清晰可读,而不会认为是将条件判断语句的全等号(
    ===
    )错写成了等号(
    =
    )。

    eslint:
    no-cond-assign

    // ✓ ok
    while ((m = text.match(expr))) {
      // ...
    }
    
    // ✗ avoid
    while (m = text.match(expr)) {
      // ...
    }
    
  • 单行代码块两边加空格

    eslint:
    block-spacing

    function foo () {return true}    // ✗ avoid
    function foo () { return true }  // ✓ ok
    
  • 对于变量和函数名统一使用驼峰命名法

    eslint:
    camelcase

    function my_function () { }    // ✗ avoid
    function myFunction () { }     // ✓ ok
    
    var my_var = 'hello'           // ✗ avoid
    var myVar = 'hello'            // ✓ ok
    
  • 不允许有多余的行末逗号

    eslint:
    comma-dangle

    var obj = {
      message: 'hello',   // ✗ avoid
    }
    
  • 始终将逗号置于行末

    eslint:
    comma-style

    var obj = {
      foo: 'foo'
      ,bar: 'bar'   // ✗ avoid
    }
    
    var obj = {
      foo: 'foo',
      bar: 'bar'   // ✓ ok
    }
    
  • 点号操作符须与属性需在同一行

    eslint:
    dot-location

    console.
      log('hello')  // ✗ avoid
    
    console
      .log('hello') // ✓ ok
    
  • 文件末尾留一空行

    eslint:
    eol-last

  • 函数调用时标识符与括号间不留间隔

    eslint:
    func-call-spacing

    console.log ('hello') // ✗ avoid
    console.log('hello')  // ✓ ok
    
  • 键值对当中冒号与值之间要留空白

    eslint:
    key-spacing

    var obj = { 'key' : 'value' }    // ✗ avoid
    var obj = { 'key' :'value' }     // ✗ avoid
    var obj = { 'key':'value' }      // ✗ avoid
    var obj = { 'key': 'value' }     // ✓ ok
    
  • 构造函数要以大写字母开头

    eslint:
    new-cap

    function animal () {}
    var dog = new animal()    // ✗ avoid
    
    function Animal () {}
    var dog = new Animal()    // ✓ ok
    
  • 无参的构造函数调用时要带上括号

    eslint:
    new-parens

    function Animal () {}
    var dog = new Animal    // ✗ avoid
    var dog = new Animal()  // ✓ ok
    
  • 对象中定义了存值器,一定要对应的定义取值器

    eslint:
    accessor-pairs

    var person = {
      set name (value) {    // ✗ avoid
        this._name = value
      }
    }
    
    var person = {
      set name (value) {
        this._name = value
      },
      get name () {         // ✓ ok
        return this._name
      }
    }
    
  • 子类的构造器中一定要调用
    super

    eslint:
    constructor-super

    class Dog {
      constructor () {
        super()   // ✗ avoid
      }
    }
    
    class Dog extends Mammal {
      constructor () {
        super()   // ✓ ok
      }
    }
    
  • 使用数组字面量而不是构造器

    eslint:
    no-array-constructor

    var nums = new Array(1, 2, 3)   // ✗ avoid
    var nums = [1, 2, 3]            // ✓ ok
    
  • 避免使用
    arguments.callee

    arguments.caller

    eslint:
    no-caller

    function foo (n) {
      if (n <= 0) return
    
      arguments.callee(n - 1)   // ✗ avoid
    }
    
    function foo (n) {
      if (n <= 0) return
    
      foo(n - 1)
    }
    
  • 避免对类名重新赋值

    eslint:
    no-class-assign

    class Dog {}
    Dog = 'Fido'    // ✗ avoid
    
  • 避免修改使用
    const
    声明的变量

    eslint:
    no-const-assign

    const score = 100
    score = 125       // ✗ avoid
    
  • 避免使用常量作为条件表达式的条件(循环语句除外)

    eslint:
    no-constant-condition

    if (false) {    // ✗ avoid
      // ...
    }
    
    if (x === 0) {  // ✓ ok
      // ...
    }
    
    while (true) {  // ✓ ok
      // ...
    }
    
  • 正则中不要使用控制符

    eslint:
    no-control-regex

    var pattern = /\x1f/    // ✗ avoid
    var pattern = /\x20/    // ✓ ok
    
  • 不要使用
    debugger

    eslint:
    no-debugger

    function sum (a, b) {
      debugger      // ✗ avoid
      return a + b
    }
    
  • 不要对变量使用
    delete
    操作

    eslint:
    no-delete-var

    var name
    delete name     // ✗ avoid
    
  • 不要定义冗余的函数参数

    eslint:
    no-dupe-args

    function sum (a, b, a) {  // ✗ avoid
      // ...
    }
    
    function sum (a, b, c) {  // ✓ ok
      // ...
    }
    
  • 类中不要定义冗余的属性

    eslint:
    no-dupe-class-members

    class Dog {
      bark () {}
      bark () {}    // ✗ avoid
    }
    
  • 对象字面量中不要定义重复的属性

    eslint:
    no-dupe-keys

    var user = {
      name: 'Jane Doe',
      name: 'John Doe'    // ✗ avoid
    }
    
  • switch
    语句中不要定义重复的
    case
    分支

    eslint:
    no-duplicate-case

    switch (id) {
      case 1:
        // ...
      case 1:     // ✗ avoid
    }
    
  • 同一模块有多个导入时一次性写完

    eslint:
    no-duplicate-imports

    import { myFunc1 } from 'module'
    import { myFunc2 } from 'module'          // ✗ avoid
    
    import { myFunc1, myFunc2 } from 'module' // ✓ ok
    
  • 正则中不要使用空字符

    eslint:
    no-empty-character-class

    const myRegex = /^abc[]/      // ✗ avoid
    const myRegex = /^abc[a-z]/   // ✓ ok
    
  • 不要解构空值

    eslint:
    no-empty-pattern

    const { a: {} } = foo         // ✗ avoid
    const { a: { b } } = foo      // ✓ ok
    
  • 不要使用
    eval()

    eslint:
    no-eval

    eval( "var result = user." + propName ) // ✗ avoid
    var result = user[propName]             // ✓ ok
    
  • catch
    中不要对错误重新赋值

    eslint:
    no-ex-assign

    try {
      // ...
    } catch (e) {
      e = 'new value'             // ✗ avoid
    }
    
    try {
      // ...
    } catch (e) {
      const newVal = 'new value'  // ✓ ok
    }
    
  • 不要扩展原生对象

    eslint:
    no-extend-native

    Object.prototype.age = 21     // ✗ avoid
    
  • 避免多余的函数上下文绑定

    eslint:
    no-extra-bind

    const name = function () {
      getName()
    }.bind(user)    // ✗ avoid
    
    const name = function () {
      this.getName()
    }.bind(user)    // ✓ ok
    
  • 避免不必要的布尔转换

    eslint:
    no-extra-boolean-cast

    const result = true
    if (!!result) {   // ✗ avoid
      // ...
    }
    
    const result = true
    if (result) {     // ✓ ok
      // ...
    }
    
  • 不要使用多余的括号包裹函数

    eslint:
    no-extra-parens

    const myFunc = (function () { })   // ✗ avoid
    const myFunc = function () { }     // ✓ ok
    
  • switch
    一定要使用
    break
    来将条件分支正常中断

    eslint:
    no-fallthrough

    switch (filter) {
      case 1:
        doSomething()    // ✗ avoid
      case 2:
        doSomethingElse()
    }
    
    switch (filter) {
      case 1:
        doSomething()
        break           // ✓ ok
      case 2:
        doSomethingElse()
    }
    
    switch (filter) {
      case 1:
        doSomething()
        // fallthrough  // ✓ ok
      case 2:
        doSomethingElse()
    }
    
  • 不要省去小数点前面的0

    eslint:
    no-floating-decimal

    const discount = .5      // ✗ avoid
    const discount = 0.5     // ✓ ok
    
  • 避免对声明过的函数重新赋值

    eslint:
    no-func-assign

    function myFunc () { }
    myFunc = myOtherFunc    // ✗ avoid
    
  • 不要对全局只读对象重新赋值

    eslint:
    no-global-assign

    window = {}     // ✗ avoid
    
  • 注意隐式的
    eval()

    eslint:
    no-implied-eval

    setTimeout("alert('Hello world')")                   // ✗ avoid
    setTimeout(function () { alert('Hello world') })     // ✓ ok
    
  • 嵌套的代码块中禁止再定义函数

    eslint:
    no-inner-declarations

    if (authenticated) {
      function setAuthUser () {}    // ✗ avoid
    }
    
  • 不要向
    RegExp
    构造器传入非法的正则表达式

    eslint:
    no-invalid-regexp

    RegExp('[a-z')    // ✗ avoid
    RegExp('[a-z]')   // ✓ ok
    
  • 不要使用非法的空白符

    eslint:
    no-irregular-whitespace

    function myFunc () /*<NBSP>*/{}   // ✗ avoid
    
  • 禁止使用
    __iterator__

    eslint:
    no-iterator

    Foo.prototype.__iterator__ = function () {}   // ✗ avoid
    
  • 外部变量不要与对象属性重名

    eslint:
    no-label-var

    var score = 100
    function game () {
      score: while (true) {      // ✗ avoid
        score -= 10
        if (score > 0) continue score
        break
      }
    }
    
  • 不要使用标签语句

    eslint:
    no-labels

    label:
    while (true) {
      break label     // ✗ avoid
    }
    
  • 不要书写不必要的嵌套代码块

    eslint:
    no-lone-blocks

    function myFunc () {
      {                   // ✗ avoid
        myOtherFunc()
      }
    }
    
    function myFunc () {
      myOtherFunc()       // ✓ ok
    }
    
  • 不要混合使用空格与制表符作为缩进

    eslint:
    no-mixed-spaces-and-tabs

  • 除了缩进,不要使用多个空格

    eslint:
    no-multi-spaces

    const id =    1234    // ✗ avoid
    const id = 1234       // ✓ ok
    
  • 不要使用多行字符串

    eslint:
    no-multi-str

    const message = 'Hello \
                       world'     // ✗ avoid
    
  • new
    创建对象实例后需要赋值给变量

    eslint:
    no-new

    new Character()                     // ✗ avoid
    const character = new Character()   // ✓ ok
    
  • 禁止使用
    Function
    构造器

    eslint:
    no-new-func

    var sum = new Function('a', 'b', 'return a + b')    // ✗ avoid
    
  • 禁止使用
    Object
    构造器

    eslint:
    no-new-object

    let config = new Object()   // ✗ avoid
    
  • 禁止使用
    new require

    eslint:
    no-new-require

    const myModule = new require('my-module')    // ✗ avoid
    
  • 禁止使用
    Symbol
    构造器

    eslint:
    no-new-symbol

    const foo = new Symbol('foo')   // ✗ avoid
    
  • 禁止使用原始包装器

    eslint:
    no-new-wrappers

    const message = new String('hello')   // ✗ avoid
    
  • 不要将全局对象的属性作为函数调用

    eslint:
    no-obj-calls

    const math = Math()   // ✗ avoid
    
  • 不要使用八进制字面量

    eslint:
    no-octal

    const octal = 042         // ✗ avoid
    const decimal = 34        // ✓ ok
    const octalString = '042' // ✓ ok
    
  • 字符串字面量中也不要使用八进制转义字符

    eslint:
    no-octal-escape

    const copyright = 'Copyright \251'  // ✗ avoid
    
  • 使用
    __dirname

    __filename
    时尽量避免使用字符串拼接

    eslint:
    no-path-concat

    const pathToFile = __dirname + '/app.js'            // ✗ avoid
    const pathToFile = path.join(__dirname, 'app.js')   // ✓ ok
    
  • 使用
    getPrototypeOf
    来替代
    __proto__

    eslint:
    no-proto

    const foo = obj.__proto__               // ✗ avoid
    const foo = Object.getPrototypeOf(obj)  // ✓ ok
    
  • 不要重复声明变量

    eslint:
    no-redeclare

    let name = 'John'
    let name = 'Jane'     // ✗ avoid
    
    let name = 'John'
    name = 'Jane'         // ✓ ok
    
  • 正则中避免使用多个空格

    eslint:
    no-regex-spaces

    const regexp = /test   value/   // ✗ avoid
    
    const regexp = /test {3}value/  // ✓ ok
    const regexp = /test value/     // ✓ ok
    
  • return 语句中的赋值必需有括号包裹

    eslint:
    no-return-assign

    function sum (a, b) {
      return result = a + b     // ✗ avoid
    }
    
    function sum (a, b) {
      return (result = a + b)   // ✓ ok
    }
    
  • 避免将变量赋值给自己

    eslint:
    no-self-assign

    name = name   // ✗ avoid
    
  • 避免将变量与自己进行比较操作

    esint:
    no-self-compare

    if (score === score) {}   // ✗ avoid
    
  • 避免使用逗号操作符

    eslint:
    no-sequences

    if (doSomething(), !!test) {}   // ✗ avoid
    
  • 不要随意更改关键字的值

    eslint:
    no-shadow-restricted-names

    let undefined = 'value'     // ✗ avoid
    
  • 禁止使用稀疏数组(Sparse arrays)

    eslint:
    no-sparse-arrays

    let fruits = ['apple',, 'orange']       // ✗ avoid
    
  • 不要使用制表符

    eslint:
    no-tabs

  • 正确使用 ES6 中的字符串模板

    eslint:
    no-template-curly-in-string

    const message = 'Hello ${name}'   // ✗ avoid
    const message = `Hello ${name}`   // ✓ ok
    
  • 使用
    this
    前请确保
    super()
    已调用

    eslint:
    no-this-before-super

    class Dog extends Animal {
      constructor () {
        this.legs = 4     // ✗ avoid
        super()
      }
    }
    

  • throw
    抛错时,抛出
    Error
    对象而不是字符串

    eslint:
    no-throw-literal

    throw 'error'               // ✗ avoid
    throw new Error('error')    // ✓ ok
    
  • 行末不留空格

    eslint:
    no-trailing-spaces

  • 不要使用
    undefined
    来初始化变量

    eslint:
    no-undef-init

    let name = undefined    // ✗ avoid
    
    let name
    name = 'value'          // ✓ ok
    
  • 循环语句中注意更新循环变量

    eslint:
    no-unmodified-loop-condition

    for (let i = 0; i < items.length; j++) {...}    // ✗ avoid
    for (let i = 0; i < items.length; i++) {...}    // ✓ ok
    
  • 如果有更好的实现,尽量不要使用三元表达式

    eslint:
    no-unneeded-ternary

    let score = val ? val : 0     // ✗ avoid
    let score = val || 0          // ✓ ok
    
  • return

    throw

    continue

    break
    后不要再跟代码

    eslint:
    no-unreachable

    function doSomething () {
      return true
      console.log('never called')     // ✗ avoid
    }
    
  • finally
    代码块中不要再改变程序执行流程

    eslint:
    no-unsafe-finally

    try {
      // ...
    } catch (e) {
      // ...
    } finally {
      return 42     // ✗ avoid
    }
    
  • 关系运算符的左值不要做取反操作

    eslint:
    no-unsafe-negation

    if (!key in obj) {}       // ✗ avoid
    
  • 避免不必要的
    .call()

    .apply()

    eslint:
    no-useless-call

    sum.call(null, 1, 2, 3)   // ✗ avoid
    
  • 避免使用不必要的计算值作对象属性

    eslint:
    no-useless-computed-key

    const user = { ['name']: 'John Doe' }   // ✗ avoid
    const user = { name: 'John Doe' }       // ✓ ok
    
  • 禁止多余的构造器

    eslint:
    no-useless-constructor

    class Car {
      constructor () {      // ✗ avoid
      }
    }
    
  • 禁止不必要的转义

    eslint:
    no-useless-escape

    let message = 'Hell\o'  // ✗ avoid
    
  • import, export 和解构操作中,禁止赋值到同名变量

    eslint:
    no-useless-rename

    import { config as config } from './config'     // ✗ avoid
    import { config } from './config'               // ✓ ok
    
  • 属性前面不要加空格

    eslint:
    no-whitespace-before-property

    user .name      // ✗ avoid
    user.name       // ✓ ok
    
  • 禁止使用
    with

    eslint:
    no-with

    with (val) {...}    // ✗ avoid
    
  • 对象属性换行时注意统一代码风格

    eslint:
    object-property-newline

    const user = {
      name: 'Jane Doe', age: 30,
      username: 'jdoe86'            // ✗ avoid
    }
    
    const user = { name: 'Jane Doe', age: 30, username: 'jdoe86' }    // ✓ ok
    
    const user = {
      name: 'Jane Doe',
      age: 30,
      username: 'jdoe86'
    }                                                                 // ✓ ok
    
  • 代码块中避免多余留白

    eslint:
    padded-blocks

    if (user) {
                                // ✗ avoid
      const name = getName()
    
    }
    
    if (user) {
      const name = getName()    // ✓ ok
    }
    
  • 展开运算符与它的表达式间不要留空白

    eslint:
    rest-spread-spacing

    fn(... args)    // ✗ avoid
    fn(...args)     // ✓ ok
    
  • 遇到分号时空格要后留前不留

    eslint:
    semi-spacing

    for (let i = 0 ;i < items.length ;i++) {...}    // ✗ avoid
    for (let i = 0; i < items.length; i++) {...}    // ✓ ok
    
  • 代码块首尾留空格

    eslint:
    space-before-blocks

    if (admin){...}     // ✗ avoid
    if (admin) {...}    // ✓ ok
    
  • 圆括号间不留空格

    eslint:
    space-in-parens

    getName( name )     // ✗ avoid
    getName(name)       // ✓ ok
    
  • 一元运算符后面跟一个空格

    eslint:
    space-unary-ops

    typeof!admin        // ✗ avoid
    typeof !admin        // ✓ ok
    
  • 注释首尾留空格

    eslint:
    spaced-comment

    //comment           // ✗ avoid
    // comment          // ✓ ok
    
    /*comment*/         // ✗ avoid
    /* comment */       // ✓ ok
    
  • 模板字符串中变量前后不加空格

    eslint:
    template-curly-spacing

    const message = `Hello, ${ name }`    // ✗ avoid
    const message = `Hello, ${name}`      // ✓ ok
    
  • 检查
    NaN
    的正确姿势是使用
    isNaN()

    eslint:
    use-isnan

    if (price === NaN) { }      // ✗ avoid
    if (isNaN(price)) { }       // ✓ ok
    
  • 用合法的字符串跟
    typeof
    进行比较操作

    eslint:
    valid-typeof

    typeof name === 'undefimed'     // ✗ avoid
    typeof name === 'undefined'     // ✓ ok
    
  • 自调用匿名函数 (IIFEs) 使用括号包裹

    eslint:
    wrap-iife

    const getName = function () { }()     // ✗ avoid
    
    const getName = (function () { }())   // ✓ ok
    const getName = (function () { })()   // ✓ ok
    
  • yield *
    中的
    *
    前后都要有空格

    eslint:
    yield-star-spacing

    yield* increment()    // ✗ avoid
    yield * increment()   // ✓ ok
    
  • 请书写优雅的条件语句(avoid Yoda conditions)

    eslint:
    yoda

    if (42 === age) { }    // ✗ avoid
    if (age === 42) { }    // ✓ ok
    

关于分号

  • 不要使用分号。(参见:
    1

    2

    3
    )

    eslint:
    semi

    window.alert('hi')   // ✓ ok
    window.alert('hi');  // ✗ avoid
    
  • 不要使用
    (
    ,
    [
    ,or
    `
    等作为一行的开始。

    在没有分号的情况下代码压缩后会导致报错,而坚持这一规范则可避免出错。

    eslint:
    no-unexpected-multiline

    // ✓ ok
    ;(function () {
      window.alert('ok')
    }())
    
    // ✗ avoid
    (function () {
      window.alert('ok')
    }())
    

    // ✓ ok
    ;[1, 2, 3].forEach(bar)
    
    // ✗ avoid
    [1, 2, 3].forEach(bar)
    

    // ✓ ok
    ;`hello`.indexOf('o')
    
    // ✗ avoid
    `hello`.indexOf('o')
    

    **备注:**上面的写法只能说聪明过头了。相比更加可读易懂的代码,那些看似投巧的写法是不可取的。

    譬如:

    ;[1, 2, 3].forEach(bar)
    

    建议的写法是:

    var nums = [1, 2, 3]
    nums.forEach(bar)
    

拓展阅读

一个值得观看的视频:
  • JavaScript 中的分号多余吗?- YouTube

    当前主流的代码压缩方案都是基于词法(AST-based)进行的,所以在处理无分号的代码时完全没有压力(何况JavaScript 中分号本来就不是强制的)。

一段摘抄自
"An Open Letter to JavaScript Leaders Regarding Semicolons"
这篇文章的内容:

[自动化插入分号的做法]是安全可依赖的,而且其产出的代码能够在所有浏览器里很好地运行。Closure compiler, yuicompressor, packer 还有 jsmin 都能正确地对这样的代码进行压缩处理。并没有任何性能相关的问题。

不得不说,Javascript 社区里的大牛们一直是错误的,并不能教给你最佳实践。真是让人忧伤啊。

我建议先弄清楚 JS是怎样断句的(还有就是哪些地方看起来断了其实并没有),明白了这个后就可以随心写出漂亮的代码了。

一般来说,
\n
表示语句结束,除非:

  1. 该语句有未闭合的括号,数组字面量, 对象字面量 或者其他不能正常结束一条语句的情况(譬如,以
    .

    ,
    结尾)
  2. 该语句是
    --
    或者
    ++
    ,它会将后面的内容进行自增或减)
  3. 该语句是
    for()

    while()

    do

    if()
    或者
    else
    并且没有
    {
  4. 下一行以
    [

    (

    +

    *

    /

    -

    ,

    .
    或者其他只会单独出现在两块内容间的二元操作符。

第一条很容易理解。即使在 JSLint 中,也允许JSON,构造器的括号中,以及使用
var
配合
,
,结尾来声明多个变量等这些情中包含
\n

第二条有点奇葩。 我还想不出谁会(除了这里用作讨论外)写出
i\n++\nj
这样的代码来,不过,顺便说一下,这种写法最后解析的结果是
i; ++j
,而不是
i++; j

第三条也容易理解。
if (x)\ny()
等价于
if (x) { y() }
。解释器会向下寻找到代码块或一条语句为止。

;
是条合法的 JavaScript 语句。所以
if(x);
等价于
if(x){}
,表示"如果 x 为真,什么也不做。",这种写法在循环里面可以看到,就是当条件判断与条件更新是同一个方法的时候。
不常见,但也不至于没听说过吧。

第四条就是常见的 "看,说过要加分号!" 的情形。但这些情况可以通过在语句前面加上分号来解决,如果你确定该语句跟前面的没关系的话。举个例子,假如你想这样:

foo();
[1,2,3].forEach(bar);

那么完全可以这样来写:

foo()
;[1,2,3].forEach(bar)

后者的好处是分号比较瞩目,一旦习惯后便再也不会看到以
(

[
开头又不带分号的语句了。

THE END

SqlSugar的开发框架本身主要是基于常规关系型数据库设计的框架,支持多种数据库类型的接入,如SqlServer、MySQL、Oracle、PostgreSQL、SQLite等数据库,非关系型数据库的MongoDB数据库也可以作为扩展整合到开发框架里面,通过基类的继承关系很好的封装了相关的基础操作功能,极大的减少相关处理MongoDB的代码,并提供很好的开发效率。本篇随笔介绍如何在SqlSugar的开发框架整合MongoDB数据库的开发。

1、MongDB的简单介绍

MongoDB是一款由C++编写的高性能、开源、无模式的常用非关系型数据库产品,是非关系数据库当中功能最丰富、最像关系数据库的数据库。它扩展了关系型数据库的众多功能,例如:辅助索引、范围查询、排序等。

MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似Json的Bson格式,因此可以存储比较复杂的数据类型。
MongoDB 最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。并且MongoDB-4.2版本开始已经支持分布式事务功能。

MongoDB数据库有几个简单的概念需要了解一下。

1)MongoDB中的
database
有着和我们熟知的"数据库"一样的概念 (对 Oracle 来说就是 schema)。一个 MongoDB 实例中,可以有零个或多个数据库,每个都作为一个高等容器,用于存储数据。

2)数据库中可以有零个或多个
collections
(集合)。集合和传统意义上的 table 基本一致,可以简单的把两者看成是一样的东西。

3)集合是由零个或多个
documents
(文档)组成。同样,一个文档可以看成是一
row

4)文档是由零个或多个
fields
(字段)组成。,对应的就是关系数据库的
columns

5)
Indexes
(索引)在 MongoDB 中扮演着和它们在 RDBMS 中一样的角色,都是为了提高查询的效率。

6)
Cursors
(游标)和上面的五个概念都不一样,但是它非常重要,并且经常被忽视,其中最重要的你要理解的一点是,游标是当你问 MongoDB 拿数据的时候,它会给你返回一个结果集的指针而不是真正的数据,这个指针我们叫它游标,我们可以拿游标做我们想做的任何事情,比如说计数或者跨行之类的,而无需把真正的数据拖下来,在真正的数据上操作。

它们的对比关系图如下所示。

数据在Mongodb里面都是以Json格式方式进行存储的,如下所示是其中的一个记录内容。

BSON格式

Bson是一种类Json的一种
二进制
形式的存储格式,简称Binary Json,它和Json一样,支持内嵌的文档对象和数组对象,但是Bson有Json没有的一些数据类型,如Date和BinData类型。

Bson可以做为网络数据交换的一种存储形式,这个有点类似于Google的Protocol
Buffer
,但是Bson是一种schema-less的存储形式,它的优点是灵活性高,但它的缺点是空间利用率不是很理想,Bson有三个特点:轻量性、可遍历性、高效性,

{“hello":"world"} 这是一个Bson的例子,其中"hello"是key name,它一般是cstring类型,字节表示是cstring::= (byte*) "/x00" ,其中*表示零个或多个byte字节,/x00表示结束符;后面的"world"是value值,它的类型一般是string,double,array,binarydata等类型。

MongDB数据库本身支持多种开发语言的驱动,MongoDB有官方的驱动如下:

我们框架基于C#开发,使用的时候,安装MongoDB的C#的驱动 MongoDB.Driver 即可。

在MongoDB数据库的集合里面,都要求文档有一个_id字段,这个是强制性的,而且这个字段的存储类型为ObjectId类型,这个值考虑了分布式的因素,综合了机器码,进程,时间戳等等方面的内容,它的构造如下所示。

ObjectId是一个12字节的
BSON
类型字符串。按照字节顺序,依次代表:

  • 4字节:UNIX时间戳
  • 3字节:表示运行MongoDB的机器
  • 2字节:表示生成此_id的进程
  • 3字节:由一个随机数开始的计数器生成的值

实体基类一般包含了一个属性Id,这个是一个字符串型的对象(也可以使用ObjectId类型,但是为了方便,我们使用字符型,并声明为ObjectId类型即可),由于我们声明了该属性对象为ObjectId类型,那么我们就可以在C#代码里面使用字符串的ID类型了。

2、基于MongoDB数据库的封装处理

以前介绍过,针对常规关系型数据库的开发,在SqlSugar开发框架上,我们设计一些基类,以便重用相关的逻辑代码,通过泛型的约束,可以提供强类型的数据接口,非常方便。

其中MyCrudService里面封装了很多CRUD以及常用的处理方法。类似的处理方式,我们专门为MongoDB数据库的访问操作,设计了一个功能强大的基类即可。

在数据库表的实体对应关系上,我们依旧遵循则相应的设计规则,基类实体采用IEntity<string>的接口类型,因此他们具有一个字符串的Id类型。其他业务对象继承该基类对象即可。

    /// <summary>
    ///基于MongoDB的实体类基类/// </summary>
    public class BaseMongoEntity : Entity<string>{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public override string Id { get; set; }
}

相应的,我们根据常规数据库的基类接口名称,在处理MongoDB数据库的操作接口的时候,名称保持一致性。

其中TEntity为强类型实体类型,而TGetListInput 是定义的一个分页接口。定义的基类接口代码如下所示。

其中接口对象 CurrentApiUser是我们用户上下文的信息,包含一些驻留在ClainPrincipal中的信息,用于记录访问接口的用户信息的。

其他接口定义类似的处理即可。

基类接口的实现类,就是我们需要设计的MongoDB数据库操作类了,初始化类的代码如下所示。

    /// <summary>
    ///MongoDB基础仓储实现/// </summary>
    /// <typeparam name="TEntity"></typeparam>
    public abstract class BaseMongoService<TEntity, TGetListInput> : IBaseMongoService<TEntity, TGetListInput> 
        where TEntity : class, IEntity<string>, new()whereTGetListInput : IPagedAndSortedResultRequest
{
protected readonly IMongoDBContext mongoContext = NullMongoDBContext.Instance;//空实现 protected IMongoCollection<TEntity> collection; //强类型对象集合 protected IMongoCollection<BsonDocument> bsonCollection; //弱类型集合BsonDocument集合 /// <summary> ///当前Api用户信息/// </summary> public IApiUserSession CurrentApiUser { get; set; } = NullApiUserSession.Instance;//空实现 /// <summary> ///构造函数/// </summary> protectedBaseMongoService()
{
//如果SerivcePovider已经设置值,则获得注入的接口对象 if (ServiceLocator.SerivcePovider != null)
{
CurrentApiUser
= ServiceLocator.GetService<IApiUserSession>();
mongoContext
= ServiceLocator.GetService<IMongoDBContext>();
collection
= mongoContext.GetCollection<TEntity>(typeof(TEntity).Name);//强类型对象集合 bsonCollection = mongoContext.GetCollection<BsonDocument>(typeof(TEntity).Name);//弱类型集合BsonDocument集合 }
}
/// <summary> ///获取所有记录/// </summary> /// <returns></returns> public virtual async Task<ListResultDto<TEntity>>GetAllAsync()
{
var all = await collection.FindAsync(Builders<TEntity>.Filter.Empty);var list = awaitall.ToListAsync();return new ListResultDto<TEntity>()
{
Items
=list
};
}

我们通过构建对应的强类型Collection和弱类型Collection,来操作实体类和BsonDocument的相关操作的。其中的上下文对象,参考随笔《
NoSQL – MongoDB Repository Implementation in .NET Core with Unit Testing example
》进行的处理。

   /// <summary>
    ///MongoDB 上下文对象/// </summary>
    public classMongoDBContext : IMongoDBContext
{
private IMongoDatabase _db { get; set; }private MongoClient _mongoClient { get; set; }public IClientSessionHandle Session { get; set; }public MongoDBContext(IOptions<Mongosettings>configuration)
{
_mongoClient
= newMongoClient(configuration.Value.Connection);
_db
=_mongoClient.GetDatabase(configuration.Value.DatabaseName);
}
/// <summary> ///获取强类型集合对象/// </summary> /// <typeparam name="T">对象类型</typeparam> /// <param name="name"></param> /// <returns></returns> public IMongoCollection<T> GetCollection<T>(string name) where T : class, new()
{
return _db.GetCollection<T>(name);
}
}
public interfaceIMongoDBContext
{
IMongoCollection
<T> GetCollection<T>(string name) where T : class, new();
}

通过IOptions 方式我们注入对应的MongoDB数据库配置信息,在appsettings.json中添加根节点内容。

  "MongoSettings": {"Connection": "mongodb://localhost:27017/", //MongoDB连接字符串
    "DatabaseName": "iqidi" //MongoDB数据库名称
  },

我们在启动Web API的时候,在Program.cs 代码中配置好就可以了。

//MongoDB配置
builder.Services.Configure<Mongosettings>(builder.Configuration.GetSection("MongoSettings"));

默认初始化的IMongoDBContext是一个空接口,我们可以在Web API启动的时候,指定一个具体的实现就可以了

//添加IMongoContext实现类
builder.Services.AddSingleton<IMongoDBContext, MongoDBContext>();

对于基类接口,分页查询获取对应列表数据,是常规的处理方式,默认需要排序、分页,返回对应的数据结构,如下代码所示。

        /// <summary>
        /// 根据条件获取列表
        /// </summary>
        /// <param name="input">分页查询条件</param>
        /// <returns></returns>
        public virtual async Task<PagedResultDto<TEntity>>GetListAsync(TGetListInput input)
{
var query =CreateFilteredQueryAsync(input);var totalCount =await query.CountAsync();//排序处理 query =ApplySorting(query, input);//分页处理 query =ApplyPaging(query, input);//获取列表 var list =await query.ToListAsync();return new PagedResultDto<TEntity>(
totalCount,
list
);
}

其中
PagedResultDto
是我们SqlSugar开发框架参照ABP框架定义一个数据结构,包含一个TotalCount数量和一个Items的对象集合。而其中
CreateFilteredQueryAsync 是定义的一个可供业务子类重写的函数,用来处理具体的查询条件。在基类BaseMongoService中只是提供一个默认的可查询对象。

        /// <summary>
        /// 留给子类实现过滤条件的处理
        /// </summary>
        /// <returns></returns>
        protected virtual IMongoQueryable<TEntity>CreateFilteredQueryAsync(TGetListInput input)
{
returncollection.AsQueryable();
}

例如,对于一个具体的业务对象操作类,CustomerService的定义如下所示,并且具体化查询的条件处理,如下代码所示。

namespace SugarProject.Core.MongoDB
{
/// <summary> /// 基于MongoDB数据库的应用层服务接口实现 /// </summary> public class CustomerService : BaseMongoService<CustomerInfo, CustomerPagedDto>, ICustomerService
{
/// <summary> /// 构造函数 /// </summary> public CustomerService()
{
}
/// <summary> /// 自定义条件处理 /// </summary> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override IMongoQueryable<CustomerInfo>CreateFilteredQueryAsync(CustomerPagedDto input)
{
var query =base.CreateFilteredQueryAsync(input);

query
=query
.Where(t
=> !input.ExcludeId.IsNullOrWhiteSpace() && t.Id != input.ExcludeId) //不包含排除ID .Where(t=> !input.Name.IsNullOrWhiteSpace() && t.Name.Contains(input.Name)) //如需要精确匹配则用Equals //年龄区间查询 .Where(t=> input.AgeStart.HasValue && t.Age >=input.AgeStart.Value)
.Where(t
=> input.AgeEnd.HasValue && t.Age <=input.AgeEnd.Value)//创建日期区间查询 .Where(t => input.CreateTimeStart.HasValue && t.CreateTime >=input.CreateTimeStart.Value)
.Where(t
=> input.CreateTimeEnd.HasValue && t.CreateTime <=input.CreateTimeEnd.Value)
;
returnquery;
}

这个处理方式类似于常规关系型数据库的处理方式,就是对条件的判断处理。而具体的业务对象模型,和常规框架的实体类很类似。

    /// <summary>
    ///客户信息///继承自BaseMongoEntity,拥有Id主键属性/// </summary>
    public classCustomerInfo : BaseMongoEntity
{
/// <summary> ///默认构造函数(需要初始化属性的在此处理)/// </summary> publicCustomerInfo()
{
this.CreateTime =System.DateTime.Now;
}
#region Property Members /// <summary> ///姓名/// </summary> public virtual string Name { get; set; }/// <summary> ///年龄/// </summary> public virtual int Age { get; set; }/// <summary> ///创建人/// </summary> public virtual string Creator { get; set; }/// <summary> ///创建时间/// </summary> public virtual DateTime CreateTime { get; set; }#endregion}

对于插入和更新操作等常规操作,我们调用普通的Collection操作处理就可以了

        /// <summary>
        ///创建对象/// </summary>
        /// <param name="input">实体对象</param>
        /// <returns></returns>
        public virtual asyncTask InsertAsync(TEntity input)
{
SetObjectIdIfEmpty(input);
//如果Id为空,设置为ObjectId的值 awaitcollection.InsertOneAsync(input);
}
/// <summary> ///更新记录/// </summary> public virtual async Task<bool>UpdateAsync(TEntity input)
{
SetObjectIdIfEmpty(input);
//如果Id为空,设置为ObjectId的值//await _dbSet.ReplaceOneAsync(Builders<TEntity>.Filter.Eq("_id", input.Id), input);//要修改的字段 var list = new List<UpdateDefinition<TEntity>>();foreach (var item ininput.GetType().GetProperties())
{
if (item.Name.ToLower() == "id") continue;
list.Add(Builders
<TEntity>.Update.Set(item.Name, item.GetValue(input)));
}
var updatefilter = Builders<TEntity>.Update.Combine(list);var update = await collection.UpdateOneAsync(Builders<TEntity>.Filter.Eq("_id", input.Id), updatefilter);var result = update != null && update.ModifiedCount > 0;returnresult;
}

更新操作,有一种整个替换更新,还有一个是部分更新,它们两者是有区别的。如果对于部分字段的更新,那么操作如下所示 ,主要是利用UpdateDefinition对象来指定需要更新那些字段属性及值等信息。

        /// <summary>
        ///封装处理更新的操作(部分字段更新)/// </summary>
        /// <example>
        ///var update = Builders<UserInfo>.Update.Set(s => s.Name, newName);/// </example>
        public virtual async Task<bool> UpdateAsync(string id, UpdateDefinition<TEntity>update)
{
var result = await collection.UpdateOneAsync(s => s.Id == id, update, new UpdateOptions() { IsUpsert = true});return result != null && result.ModifiedCount > 0;
}

根据MongoDB数据库的特性,我们尽量细化对数据库操作的基类接口,定义所需的接口函数即可。

对于Web API的控制器设计,我们在之前的随笔也有介绍,为常规授权处理的BaseApiController,为常规业务CRUD等接口处理的BusinessController,如下所示。

其中ControllerBase是.net core Web API中的标准控制器基类,我们由此派生一个LoginController用于登录授权,而BaseApiController则处理常规接口用户身份信息,而BusinessController则是对标准的增删改查等基础接口进行的封装,我们实际开发的时候,只需要开发编写类似CustomerController基类即可。

而对于 MongoDB的Web API控制器,我们为了方便开发,也设计了同类型的Web API 控制器基类。

其中MongoBaseController基类具有常规的CRUD的接口定义处理,只要继承它就可以了,而如果只是继承BaseApiController这需要自定义控制器接口的方法。

最后我们启动Swagger进行测试对应的接口即可,实际还可以整合在UI中进行测试处理。我们安装MongoDB数据库的管理工具后,可以在
MongoDBCompass
中进行查询对应数据库的数据。

    /// <summary>
    ///客户信息的控制器对象(基于MongoDB),基于BaseApiController,需要自定义接口处理/// </summary>
[ApiController]
[Route(
"api/MongoCustomer")]public classMongoCustomerController : BaseApiController
{
privateICustomerService _service;/// <summary> ///构造函数,并注入基础接口对象/// </summary> /// <param name="service"></param> publicMongoCustomerController(ICustomerService service)
{
this._service =service;
}
/// <summary> ///获取所有记录/// </summary> [HttpGet]
[Route(
"all")]public virtual async Task<ListResultDto<CustomerInfo>>GetAllAsync()
{
//检查用户是否有权限,否则抛出MyDenyAccessException异常 base.CheckAuthorized(AuthorizeKey.ListKey);return await_service.GetAllAsync();
}

而如果继承自MongoBaseController ,那么就会具有基类MongoBaseController 公开的所有控制器方法。

    /// <summary>
    ///客户信息的控制器对象(基于MongoDB),基于MongoBaseController,具有常规CRUD操作接口/// </summary>
[ApiController]
[Route(
"api/MongoCustomer2")]public class MongoCustomer2Controller : MongoBaseController<CustomerInfo, CustomerPagedDto>{/// <summary> ///构造函数,并注入基础接口对象/// </summary> /// <param name="service"></param> public MongoCustomer2Controller(ICustomerService service) : base(service)
{
}
}

早几年前曾经也介绍过该数据库的相关使用,随笔如下所示,有需要也可以了解下。