前端干货之JS最好实践

延续更新地点
https://wdd.js.org/js-best-pr…

1. 作风

一千个读者有一千个哈姆雷特,每一个人都有本身的code style。我也曾为了要不要加分号给同事闹个脸红脖子粗,现实上有必要吗? 实在JavaScript已经有了比较盛行的几个作风

我本身运用的是JavaScript Standard Style, 我之所以运用这个,是由于它有一些东西。可以让你写完代码后,一旦保留,就自动帮你把你的作风的代码修正成规范支解,而不是死记硬背应当怎样写。看完这个页面,你就应当立马爱上JavaScript Standard Style , 如果你用vscode, 恰好你有写vue, 你想在.vue文件中运用standard作风,那末你须要看看这篇文章

2. 可保护性

许多时刻,我们不是从零最先,开辟新代码。而是去保护别人的代码,以别人的事情效果为基础。确保本身的代码可保护,是赠人玫瑰,手留余香的功德。一方面让别人看的惬意,另一方面也防备本身长时候没看过本身的代码,本身都难以明白。

2.1. 什么是可保护代码

可保护的代码的一些特性

  • 可明白易于明白代码的用处
  • 可顺应数据的变化,不须要完整重写代码
  • 可扩大要斟酌将来对中心功用的扩大
  • 可调试给出充足的信息,让调试的时刻,肯定问题所在
  • 不可支解函数的功用要单一,功用粒度不可支解,可复用性加强

2.2. 代码商定

2.2.1. 可读性

  • 一致的缩进体式格局
  • 诠释
  • 空缺行

2.2.1.1. 缩进:

  • 平常运用4个空格
  • 不必制表符的缘由是它在差别编辑器里显现效果差别

2.2.1.2. 诠释:哪些处所须要诠释?

  • 函数和要领
  • 大段代码
  • 庞杂的算法
  • hack

2.2.1.3. 空缺行:哪些处所须要空缺行?

  • 要领之间
  • 要领里的局部变量和第一个语句之间
  • 单行或许多行诠释
  • 要领内衣个逻辑单位之间
// Good
if (wl && wl.length) {

    for (i = 0, l = wl.length; i < l; ++i) {
        p = wl[i];
        type = Y.Lang.type(r[p]);
        
        if (s.hasOwnProperty(p)) {
        
            if (merge && type == 'object') {
                Y.mix(r[p], s[p]);
            } else if (ov || !(p in r)) {
                r[p] = s[p];
            }
        }
    }
}

2.2.2. 变量名和函数名

There are only two hard problem in Computer Science cache invalidation and naming things.—Phil Karlton

  • 驼峰式定名
  • 变量名以名词开首
  • 要领名以动词开首
  • 常量悉数大写
  • 组织函数以大写字母开首
  • jQuery对象以”$”标记开首
  • 自定义事宜处置惩罚函数以“on”开首
// Good
var count = 10;
var myName = "wdd";
var found = true;

// Bad: Easily confused with functions
var getCount = 10;
var isFound = true;

// Good
function getName() {
    return myName;
}

// Bad: Easily confused with variable
function theName() {
    return myName;
}

// Bad:
var btnOfSubmit = $('#submit');

// Good:
var $btnOfSubmit = $('#submit');

// Bad:给App增添一个处置惩罚谈天事宜的函数,平常都是和websocket服务端推送音讯相干
App.addMethod('createChat',function(res){
    App.log(res);
});
// Bad: 此处挪用,这里很轻易误以为这个函数是处置惩罚建立谈天的逻辑函数
App.createChat();

// Good: 
App.addMethod('onCreateChat',function(res){
    App.log(res);
});
// Good:此处挪用
App.onCreateChat();

变量定名不仅仅是一种科学,更是一种艺术。总之,要短小精悍,见名知意。有些名词可以反应出变量的范例。

2.2.2.1. 变量名

名词数据范例寄义
count, length,size数值
name, title,message字符串
i, j, k用来轮回
car,person,student,user对象
success,fail布尔值
payloadpost数据的要求体
method要求体式格局

2.2.2.2. 函数名

动词寄义
canFunction returns a boolean
hasFunction returns a boolean
isFunction returns a boolean
getFunction returns a nonboolean
set Function is used to save a value

2.2.2.3. 一些与函数名搭配的经常使用动词

动词用法
send发送
resend重发
validate考证
query查询
create建立
add增添
delete删除
remove移除
insert插进去
update更新,编辑
copy复制
render衬着
close封闭
open开启
clear消灭
edit编辑
query查询
on当事宜发作
list衬着一个列表,如用户列表renderUsersList()
content衬着内容,如用户概况的页面 renderUserContent()

2.2.2.4. 接口经常使用的动词

关于http要求的最经常使用的四种要领,get,post,put,delete,有一些经常使用的名词与其对应

寄义要求要领词语栗子
增添postcreatecreateUser,createCall
删除deletedeletedeleteUser
修正putupdateupdateUser,updateProfile
查询getget,querygetUser,queryUser(无条件查询运用get,有条件查询运用query)

2.2.2.5. 学会运用单复数定名函数

函数名寄义
getUser()猎取一个用户,平常是经由历程唯一的id来猎取
getUsers()猎取一组用户,平常是经由历程一些条件来猎取
createUser()建立一个用户
createUsers()建立一组用户

2.2.2.6. 常量

var MAX_COUNT = 10;
var URL = "http://www.nczonline.net/";

2.2.2.7. 组织函数

// Good
function Person(name) {
    this.name = name;
}
Person.prototype.sayName = function() {
    alert(this.name);
};
var me = new Person("wdd");

2.2.2.8. 底层http要求接口函数

  • 发起运用“_”开首,比方App._getUsers();而关于接口函数的封装,比方App.getUsers(),内部逻辑挪用App._getUsers();

2.2.3. 文件名

  • 悉数运用小写字母
  • 单词之间的距离运用“-”

eg:

app-main.js
app-event.js
app-user-manger.js

2.2.4. 文件归类

本身写的js文件最好和援用的一些第三方js离别安排在差别的文件夹下。

2.2.5. 万万别用alert

alert的瑕玷

  • 如果你用alert来显现提醒音讯,那末用户除了点击alert上的的肯定按钮外,就只能点击上面的封闭,或许挑选制止再挑选对话框,除此以外什么都不能操纵。
  • 有些浏览器如果制止了alert的选项,那末你的alert是不会显现的
  • 如果你在try catch语句里运用alert,那末console里将不会输出毛病信息,你都没方法检察毛病的细致缘由,以及储失足的位置。

更文雅的提醒体式格局

  • console.log() 一般提醒音讯
  • console.error() 毛病提醒音讯
  • console.info() 信息提醒音讯
  • console.warn() 正告提醒音讯

2.3. 松懈耦合

  • html文件中只管防止写js语句
  • 只管防止在js变动某个css类的属性,而运用变动类的要领
  • 不要在css中写js的表达式
  • 解耦运用逻辑和事宜处置惩罚顺序

2.3.1. 将运用逻辑和事宜处置惩罚顺序的解耦

//平常事宜定阅的写法,以jQuery的写法为栗子
$(document).on('click','#btn-get-users',function(event){
    event.stopPropagation();
    
    //下面的省略号示意实行猎取统统用于并显现在页面上的逻辑
    // Bad
    ...
    ...
    ...
    //
});

如果增添了需求,当点击别的一个按钮的时刻,也要实行猎取统统用户并显现在页面上,那末上面省略的代码又要复制一份。如果接口有修改,那末须要在两个差别的处所都要修正。
所以,应当如许。

$(document).on('click','#btn-get-users',function(event){
    event.stopPropagation();
    
    //将运用逻辑星散在其他个函数中
    // Good
    App.getUsers();
    App.renderUsers();
});

2.3.2. 松懈解耦划定规矩

  • 不要将event对象传给其他要领,只通报来自event对象中的某些数据
  • 任何事宜处置惩罚顺序都应当只处置惩罚事宜,然后把处置惩罚转交给运用逻辑。

2.3.3. 将异步要求和数据处明白耦

// Bad
ReqApi.tenant.queryUsers({},function(res){
    if(!res.success){
        console.error(res);
        return;
    }
    
    //对数据的处置惩罚
    ...
    ...
    ...
});    

上面代码对数据的处置惩罚直接写死在异步要求内里,如果换了一个要求,然则数据处置惩罚体式格局是一样的,那末又要复制一遍数据处置惩罚的代码。最好的体式格局是将数据处置惩罚模块化成为一个函数。

// Good
ReqApi.tenant.queryUsers({},function(res){
    if(!res.success){
        console.error(res);
        return;
    }
    
    //对数据的处置惩罚
    App.renderUsers(res.data);
}); 

异步要求只处置惩罚要求,不处置惩罚数据。函数的功用要专注,功用粒度不可支解。

2.3.4. 不要将某个变量写死在函数中,只管运用参数通报进来

如果你须要一个函数去考证输入框是不是是空,以下。这类体式格局就会绑定死了这个只能考证id为test的输入框,换成其他的就不可

// bad
function checkInputIsEmpty(){
    var value = $('#test').val();
    if(value){
        return true;
    }
    else{
        return false;
    }
}

// good 
function isEmptyInput(id){
    var value = $('#'+id).val();
    if(value){
        return true;
    }
    else{
        return false;
    }
}

2.4. 编程实践

2.4.1. 尊总对象统统权

javascript动态性子是的险些任何东西在任何时候都能变动,如许就很轻易覆写了一些默许的要领。致使一些灾难性的效果。如果你不担任或许保护某个对象,那末你就不能对它举行修正。

  • 不要为实例或原型增添属性
  • 不要为实例或许原型增添要领
  • 不要重定义存已存在的要领

2.4.2. 防止全局变量

// Bad 两个全局变量
var name = "wdd";
funtion getName(){
    console.log(name);
}

// Good 一个全局变量
var App = {
    name:"wdd",
    sayName:funtion(){
        console.log(this.name);//如果这个函数当做回调数运用,这个this可以指向window,
    }
};

单一的全局变量就是定名空间的观点,比方雅虎的YUI,jQuery的$等。

2.4.3. 防止与null举行比较

funtion sortArray(values){
    // 防止
    if(values != null){
        values.sort(comparator);
    }
}
function sortArray(values){
    // 引荐
    if(values instanceof Array){
        values.sort(compartor);
    }
}

2.4.3.1. 与null举行比较的代码,可以用以下手艺举行替代

  • 如果值是一个运用范例,运用instanceof操纵符,搜检其组织函数
  • 如果值是基础范例,运用typeof搜检其范例
  • 如果是愿望对象包含某个特定的要领名,则只用typeof操纵符确保指定名字的要领存在于对象上。

代码中与null比较越少,就越轻易肯定代码的目标,消弭不必要的毛病。

2.4.4. 从代码中星散设置文件

设置数据是一些硬代码(hardcoded),看下面的栗子

function validate(value){
    if(!value){
        alert('Invalid value');
        location.href = '/errors/invalid.php';
    }
}

上面代码里有两个设置数据,一个是UI字符串(‘Invalid value’),另一个是一个Url(‘/error/invalid.php’)。如果你把他们写死在代码里,那末如果当你须要修正这些处所的时刻,那末你必需一处一处的搜检并修正,而且还可以会脱漏。

2.4.4.1. 所以第一步是要辨别,哪些代码应当写成设置文件的情势?

  • 显现在UI元素中的字符串
  • URL
  • 一些反复的唯一值
  • 一些设置变量
  • 任何可以转变的值

2.4.4.2. 一些例子

var Config = {
    "MSG_INVALID_VALUE":"Invalid value",
    "URL_INVALID":"/errors/invalid.php"
}

2.4.5. 调试信息开关

在开辟历程当中,可以到处留下几个console.log,或许alert语句,这些语句在开辟历程当中是很有代价的。然则项目一旦进入临盆环境,过量的console.log可以影响到浏览器的运转效力,过量的alert会下降顺序的用户体验。而我们最好不要在进入临盆环境前,一处一处像扫雷一样删除或许诠释掉这些调试语句。

最好的体式格局是设置一个开关。

//全局敕令空间
var App = {
    debug:true,
    log:function(msg){
        if(debug){
            console.log(msg);
        }
    },
    alert:function(msg){
        if(debug){
            alert(msg);
        }
    }
};

//运用
App.log('猎取用户信息胜利');
App.alert('暗码不婚配');

//封闭日记输出与alert
App.debug = false;

2.4.6. 运用jQuery Promise

没运用promise之前的回调函数写法

// bad:没运用promise之前的回调函数写法
function sendRequest(req,successCallback,errorCallback){
    var inputData = req.data || {};
    inputData = JSON.stringify(inputData);
    $.ajax({
        url:req.base+req.destination,
        type:req.type || "get",
        headers:{
            sessionId:session.id
        },
        data:inputData,
        dataType:"json",
        contentType : 'application/json; charset=UTF-8',
        success:function(data){
            successCallback(data);
        },
        error:function(data){
            console.error(data);
            errorCallback(data);
        }
    });
}

//挪用
sendRequest(req,function(res){
    ...
},function(res){
    ...
});

运用promise以后

function sendRequest(req){
    var dfd = $.Deferred();
    var inputData = req.data || {};
    inputData = JSON.stringify(inputData);
    $.ajax({
        url:req.base+req.destination,
        type:req.type || "get",
        headers:{
            sessionId:session.id
        },
        data:inputData,
        dataType:"json",
        contentType : 'application/json; charset=UTF-8',
        success:function(data){
            dfd.resolve(data);
        },
        error:function(data){
            dfd.reject(data);
        }
    });
    
    return dfd.promise();
}

//挪用
sendRequest(req)
.done(function(){
    //要求胜利
    ...
})
.fail(function(){
    //要求失利
    ...
});

2.4.7. 显现毛病提醒,不要给后端接口背锅

如果前端要去接口猎取用户信息并显现出来,如果你的要求花样是准确的,然则接口返回400以上的毛病,你必需经由历程提醒来示知测试,这个毛病是接口的返回毛病,而不是前端的逻辑毛病。

2.4.8. REST化接口要求

对资本的操纵包含猎取、建立、修正和删除资本,这些操纵恰好对应HTTP协定供应的GET、POST、PUT和DELETE要领。

对应体式格局

要求范例接口前缀
GET.get,
POST.create 或许 .get
PUT.update
DELETE.delete

申明

  • 有些接口虽然是猎取某一个资本,然则它运用的倒是POST要求,所以发起运用.get比较好

示例:

// 与用户相干的接口
App.api.user = {};

// 猎取一个用户: 平常来说是一个指定的Id,比方userId
App.api.user.getUser = function(){
    ...
};

// 猎取一组用户: 平常来说是一些条件,猎取条件下的用户,挑选相符条件的用户
App.api.user.getUsers = function(){
    ...
};

// 建立一个用户
App.api.user.createUser = function(){
    
};

// 建立一组用户
App.api.user.createUsers = function(){
    
};

// 更新一个用户
App.api.user.updateUser = function(){
    
};

// 更新一组用户
App.api.user.updateUsers = function(){
    
};

// 更新一个用户
App.api.user.updateUser = function(){
    
};

// 更新一组用户
App.api.user.updateUsers = function(){
    
};

// 删除一个用户
App.api.user.deleteUser = function(){
    
};

// 删除一组用户
App.api.user.deleteUsers = function(){
    
};

3. 机能

3.1. 注重作用域

  • 防止全局查找
  • 防止with语句

3.2. 挑选准确的要领

  • 优化轮回

    • 减值迭代:从最大值最先,在轮回中不停减值的迭代器越发高效
    • 简化停止条件:由于每次轮回历程都邑盘算停止条件,所以必需保证它只管快。也就是防止其他属性查找
    • 简化轮回体:由于轮回体是实行最多的,所以要确保其最大限制地优化。
  • 睁开轮回
  • 防止两重诠释:
// **Bad** 某些代码求值
eval("alert('hello')");

// **Bad** 建立新函数
var sayHi = new Function("alert('hello')");

// **Bad** 设置超时
setTimeout("alert('hello')");
  • 机能的其他注重事项

    • 原生要领较快
    • switch语句较快:可以恰当的替代ifelse语句case 的分支不要凌驾128条
    • 位运算符较快

3.3. 最小化语句数

3.3.1. 多个变量声明(烧毁)

// 体式格局1:Bad
var count = 5;
var name = 'wdd';
var sex = 'male';
var age = 10;

// 体式格局2:Good
var count = 5,
    name = 'wdd',
    sex = 'male',
    age = 10;

2017-03-07 理论上体式格局2可以要比体式格局1机能高一点。然则我在现实运用中,这个快一点险些是没什么感觉的。就像你没法感觉到小草的发展一样。反而可读性更为重要。所以,每行最好只定义一个变量,而且每行都有一个var,并用分号末端。

3.3.2. 插进去迭代值

// Good
var name = values[i++];

3.3.3. 运用数组和对象字面量

// Good
var values = ['a','b','c'];

var person = {
    name:'wdd',
    age:10
};

只需有可以,只管运用数组和对象字面量的表达式来消弭不必要的语句

3.4. 优化DOM交互

在JavaScript各个方面中,DOM无疑是最慢的一部份。DOM操纵与交互要斲丧大批的时候。由于他们每每须要从新衬着全部页面或许某一部份。进一步说,看似纤细的操纵也可以花良久来实行。由于DOM要处置惩罚异常多的信息。明白怎样优化与DOM的交互可以极大的进步剧本完成的速率。

  • 运用dom缓存手艺
  • 最小化现场更新
  • 运用innerHTML插进去大段html
  • 运用事宜代办

3.4.1. Dom缓存手艺

挪用频次异常高的dom查找,可以将DOM缓存在于一个变量中

// 最简朴的dom缓存

var domCache = {};

function myGetElement(tag){
    return domCache[tag] = domCache[tag] || $(tag);
}

3.5. 防止太长的属性查找,设置一个快捷体式格局

// 先看下面的极度状况
app.user.mother.parent.home.name = 'wdd'
app.user.mother.parent.home.adderess = '上海'
app.user.mother.parent.home.weather = '好天'

// 更文雅的体式格局
var home = app.user.mother.parent.home;
home.name = 'wdd';
home.address = '上海',
home.weather = '好天'

注重
运用上面的体式格局是有条件的,必需保证app.user.mather.parent.home是一个对象,由于对象是通报的援用。如果他的范例是一个基础范例,比方:number,string,boolean,那末复制操纵仅仅是值通报,新定义的home的转变,并不会影响到app.user.mather.parent.home的转变。

4. 快捷体式格局

4.1. 字符串转数字

+'4.1' === 4.1

4.2. 数字转字符

4.1+'' === '4.1'

4.3. 字符串取整

'4.99' | 0 === 4

5. 通用编码准绳

发起读者自行扩大

  • DRY(dont't repeat yoursele: 不要反复你本身)
  • 高内聚低耦合
  • 开放闭合
  • 最小不测
  • 单一职责(single responsibility)

6. 高等技能

6.1. 平安范例检测

  • javascript内置范例检测并不牢靠
  • safari某些版本(<4)typeof正则表达式返回为function

发起运用Object.prototype.toString.call()要领检测数据范例

function isArray(value){
    return Object.prototype.toString.call(value) === "[object Array]";
}

function isFunction(value){
    return Object.prototype.toString.call(value) === "[object Function]";
}

function isRegExp(value){
    return Object.prototype.toString.call(value) === "[object RegExp]";
}

function isNativeJSON(){
    return window.JSON && Object.prototype.toString.call(JSON) === "[object JSON]";
}

关于ie中一COM对象情势完成的任何函数,isFunction都返回false,由于他们并不是原生的javascript函数。

在web开辟中,可以辨别原生与非原生的对象异常重要。只要如许才确实晓得某个对象是不是有哪些功用

以上统统的准确性的条件是:Object.prototype.toString没有被修正过

6.2. 作用域平安的组织函数

function Person(name){
    this.name = name;
}

//运用new来建立一个对象
var one = new Person('wdd');

//直接挪用组织函数
Person();

由于this是运转时分派的,如果你运用new来操纵,this指向的就是one。如果直接挪用组织函数,那末this会指向全局对象window,然后你的代码就会掩盖window的原生name。如果有其他处所运用过window.name, 那末你的函数将会埋下一个深藏的bug。

那末,怎样才建立一个作用域平安的组织函数?

function Person(name){
    if(this instanceof Person){
        this.name = name;
    }
    else{
        return new Person(name);
    }
}

6.3. 惰性载入函数

假设有一个要领X,在A类浏览器里叫A,在b类浏览器里叫B,有些浏览器并没有这个要领,你想完成一个跨浏览器的要领。

惰性载入函数的头脑是:在函数内部转变函数本身的实行逻辑

function X(){
    if(A){
        return new A();
    }
    else{
        if(B){
            return new B();
        }
        else{
            throw new Error('no A or B');
        }
    }
}

换一种写法

function X(){
    if(A){
        X = function(){
            return new A();
        };
    }
    else{
        if(B){
            X = function(){
                return new B();
            };
        }
        else{
            throw new Error('no A or B');
        }
    }
    
    return new X();
}

6.4. 防改动对象

6.4.1. 不可扩大对象 Object.preventExtensions

// 下面代码在谷歌浏览器中实行
> var person = {name: 'wdd'};
undefined
> Object.preventExtensions(person);
Object {name: "wdd"}
> person.age = 10
10
> person
Object {name: "wdd"}
> Object.isExtensible(person)
false

6.4.2. 密封对象Object.seal

密封对象不可扩大,而且不能删除对象的属性或许要领。然则属性值可以修正。

> var one = {name: 'hihi'}
undefined
> Object.seal(one)
Object {name: "hihi"}
> one.age = 12
12
> one
Object {name: "hihi"}
> delete one.name
false
> one
Object {name: "hihi"}

6.4.3. 凝结对象 Object.freeze

最严厉的防改动就是凝结对象。对象不可扩大,而且密封,不能修正。只能接见。

6.5. 高等定时器

6.5.1. 函数撙节

函数撙节的头脑是:某些代码不可以没有中断的一连反复实行

var processor = {
    timeoutId: null,

    // 现实举行处置惩罚的要领
    performProcessing: function(){
        ...
    },

    // 初始化挪用要领
    process: function(){
        clearTimeout(this.timeoutId);

        var that = this;

        this.timeoutId = setTimeout(function(){
            that.performProcessing();
        }, 100);
    }
}

// 尝试最先实行
processor.process();

6.5.2. 中心定时器

页面如果有十个地区要动态显现当前时候,平常来说,可以用10个定时来完成。实在一个中心定时器就可以搞定。

中心定时器动画 demo地点:http://wangduanduan.coding.me…

var timers = {
        timerId: 0,
        timers: [],
        add: function(fn){
            this.timers.push(fn);
        },
        start: function(){
            if(this.timerId){
                return;
            }

            (function runNext(){
                if(timers.timers.length > 0){
                    for(var i=0; i < timers.timers.length ; i++){
                        if(timers.timers[i]() === false){
                            timers.timers.splice(i, 1);
                            i--;
                        }
                    }

                    timers.timerId = setTimeout(runNext, 16);
                }
            })();
        },
        stop: function(){
            clearTimeout(timers.timerId);
            this.timerId = 0;
        }
    };

7. 函数式编程

引荐浏览:JS函数式编程中文版

8. HTML的申饬

  • 运用input的时刻,一定要加上maxlength属性。(你以为只须要输入一个名字的处所,用户可以复制一篇文章放进去。)
  • 从input取值的时刻,最好去除一下首尾空格

9. ajax的申饬

ajax在运用的时刻,比方点击按钮,猎取某个列表。须要注重以下方面

  1. ajax要求还没有完毕时,按钮一定要disabled,防备屡次点击。要求完毕时,才去掉按钮的disabled属性。
  2. 要求没完毕的时刻,一定要显现一个gif的动画,通知用户要求还在loading。不要让用户以为这渣滓顺序又卡死了。
  3. 要求的效果如果是空的,一定要通知用户: 很抱歉,临时没有查询到相干纪录之类的话语。不要给一个空缺页面给用户。
  4. 最好斟酌到要求报错的状况,给出友爱的毛病提醒。

10. 代码整齐之道

10.1. 函数整齐

  • 只管将统统代码封装在函数中,不要暴露全局变量
  • 每一个函数的函数体中,代码行越少越好,最好一个函数中就一句代码

11. 工程化与模块化

11.1. 前端构建东西必不可少

11.1.1. webpack

11.1.2. rollup

11.1.3. parcel

12. 协定 TCP IP HTTP

如果你以为前端不须要关于协定的学问,那末你就是大错特错了。实在不仅仅是前端,统统的开辟者都应当进修底层的协定。由于他们是互联网通讯的基石。

引荐三本必读的书本

或许你一也可以看看关于协定方面的一些问题,以及如果你遇到过,你是不是晓得怎样处理:

13. 引荐深度浏览

13.1. 引荐浏览手艺书本

13.2. 引荐浏览在线文章

13.3. 手艺以外

14. 参考文献

    原文作者:Carl
    原文地址: https://segmentfault.com/a/1190000007101196
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞