ES6 函数

函数,是 JS 语法中较为重要的那一个,随着 ES6 的整体更新,函数也得到了进一步的加强。

默认值

想必在写函数时,都会有一个困惑:要是在这个函数调用的时候,参数没有传入时,该怎么办?设置默认值。对,所以大部分的函数都会有以下的代码。

function func(param1, param2) {
    param1 = param1 || {/*...*/}
    /* ... */
}

以上代码相信绝大多数的 javascript 开发者都写过,乍一看是没啥大问题的,但是如果传入的值是 false 呢?那么 param1 就会被替换成默认值。为了避免这个问题就应该将代码改一改:

if( typeof a === 'undefined' ){
    a = {/* ... */}
}

这就大大增加了代码量,而且一个痛点莫名其妙的增加了:增加了代码的不可读性。

ES6 中允许为函数设置默认值,而且定义明确,代码的可读性大大增加。

function log(x, y = 'World') {
    console.log(x, y);
}

log('Hello');               // Hello World
log('Hello', 'China');      // Hello China
log('Hello', '');           // Hello

这让函数体内(也就是 {} 内)的代码专注于函数实现,而不用去处理默认值的逻辑。

当然在构造函数中也可以使用。

function Point(x = 0, y = 0) {
    this.x = x;
    this.y = y;
}

var p = new Point();
p                           // { x: 0, y: 0 }

解构赋值与默认值结合使用

如果不理解解构赋值的含义,可以查看。不多说,先上个例子:

function foo({ x, y = 5 }) {
    console.log(x, y);
}

foo({});                    // undefined, 5
foo({x: 1});                // 1, 5
foo({x: 1, y: 2});          // 1, 2
foo();                      // TypeError: Cannot read property 'x' of undefined

上述代码使用的是结构赋值时的默认值,并没有使用函数传参的默认值。

在明白了上面这个例子后,在来看这个例子:

function foo({x, y = 5} = {x: 4, y: 7}) {
    console.log(x, y);
}

foo({});                    // undefined, 5
foo({x: 1});                // 1, 5
foo({x: 1, y: 2});          // 1, 2
foo();                      // 4, 7

唯一的不同就是,在函数参数的后面多了 = {x: 1, y: 2},造成的结果就是在 foo 函数调用的第 4 种方式上的结果。很明显,第四种方式,并没有传递参数,就使用了我们规定好的默认值。

那么现在我们就可以用两种方式来规定一个值的默认值了,假设一个场景,我们的函数需要一个含有 xy 的对象,当有参数传入时, y 的默认值是 5 ,而没有参数传入时,默认值为 {x: 4, y: 7} ,就可以用上面的函数实现,避免了很多不必要的逻辑判断代码去为函数参数设置默认值。

应用

利用默认值来指定一个参数不可省略:

function throwIfMissing() {
    throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()) {
    return mustBeProvided;
}

foo();
// Error: Missing parameter

上述的代码如果没有 foo 传递实际的参数,就会去调用默认的函数,就会抛出一个错误。对上面的 throwIfMissing 函数进行进一步的改造,就能抛出更具体的错误信息了。

function throwIfMissing(argName = '') {
    throw new Error('Missing '+ argName +' parameter');
}

function foo(arg1 = throwIfMissing('arg1')) {
    return arg1;
}

foo();
// Error: Missing arg1 parameter

函数参数的位置

通常情况下,有默认值的参数应该位于函数的参数列表的尾部。因为如果非尾部的参数有默认值的话,该值后面的参数又是必须传递参数的,导致的结果就是这个参数设不设置默认值,都是需要传递参数的。

// 例一
function f(x = 1, y) {
    return [x, y];
}

f();                        // [1, undefined]
f(2);                       // [2, undefined])
f(, 1);                     // 报错
f(undefined, 1);            // [1, 1]

// 例二
function f(x, y = 5, z) {
    return [x, y, z];
}

f();                        // [undefined, 5, undefined]
f(1);                       // [1, 5, undefined]
f(1, ,2);                   // 报错
f(1, undefined, 2);         // [1, 5, 2]

函数的 length 属性

指定了默认值以后,函数的 length 属性,将返回没有指定默认值的参数个数。

(function (a) {}).length;               // 1
(function (a = 5) {}).length;           // 0
(function (a, b, c = 5) {}).length;     // 2

如果设置了默认值的参数不是尾参数,那之后的参数也不会记录到 length

(function (a = 0, b, c) {}).length;     // 0
(function (a, b = 1, c) {}).length;     // 1

函数参数的作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。

let x = 1;

function func(y = x) {
  let x = 2;
  console.log(y);
}

f();                // 1

上面的代码中,函数调用时,参数 y = x 形成一个单独的作用域(这个处在参数初始化阶段,而函数的作用域还没出现)。这个作用域里面,变量 x 本身没有定义,所以指向外层的全局变量 x

想象一下,如果这个单独的作用域没出现的话,而是函数的作用域,那么其实上面的代码大概可以等价于:

let x = 1;

function func() {
  let y = x;
  let x = 2;
  console.log(y);
}

f();                // Uncaught ReferenceError: x is not defined

那么这个函数是执行不下去的。由于 let 变量不存在变量提升,因此会报错。

当然如果参数的默认值是一个函数,该函数的作用域同样遵守这个规则。

let foo = 'outer';

function bar(func = () => foo) {
  let foo = 'inner';
  console.log(func());
}

bar();              // outer

REST 参数

ES6 引入了 REST 参数(即 "...参数名" ),用于获取函数的多余参数,在一定情况下减少了对 arguments 对象的使用了。

function add(...values) {
    let sum = 0;
    
    for (var val of values) {
        sum += val;
    }
    
    return sum;
}

add(2, 5, 3);       // 10

由于 arguments 是函数体内的一个变量,所以必须写在函数体内(也就是 {} 内),而 REST 参数写在函数参数里,使用起来便捷。以下就是使用 argumentsREST 参数实现函数参数排序两种不同的方式。

// arguments变量的写法,必须在函数体内才能获取。
function sortNumbers() {
    return Array.prototype.slice.call(arguments).sort();
}

// rest 参数的写法。
const sortNumbers = (...numbers) => numbers.sort();

很明显,结合使用 ES6 的写法,使用 REST 参数方式更加的语义化和便捷。

REST 参数表示剩下的参数,所以在 REST 参数前是可以有别的参数存在的,但是在 REST 参数之后是不可以在有函数参数的。并且 REST 参数代表的是一个数组,故所有的数组的方法都使用于这个变量。

function push(array, ...items) {
    items.forEach(function(item) {
        array.push(item);
        console.log(item);
    });
}

var a = [];
push(a, 1, 2, 3);

// 报错
function f(a, ...b, c) {
    // ...
}

扩展运算符

扩展运算符是 REST 参数的逆,将一个数组转化为用逗号分隔的参数序列。

console.log(...[1, 2, 3]);
// 1 2 3

console.log(1, ...[2, 3, 4], 5);
// 1 2 3 4 5

[...document.querySelectorAll('div')];
// [<div>, <div>, <div>]

一些使用

function push(array, ...items) {
    array.push(...items);
}

function add(x, y) {
    return x + y;
}

var numbers = [4, 38];
add(...numbers);        // 42

扩展运算符的应用

替代apply方法

// ES5的写法
Math.max.apply(null, [14, 3, 77]);

// ES6的写法
Math.max(...[14, 3, 77]);

// 等同于
Math.max(14, 3, 77);

由于 Math.max 需要一系列单独的数字,而不是一个数字,在 ES5 中只能使用 apply 方法来实现,而在 ES6 中使用扩展运算符就方便的多了。

像数组的 push 方法也使用:

// ES5 的写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);

// ES6 的写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
arr1.push(...arr2);

合并数组

// ES5
[1, 2].concat(more);
// ES6
[1, 2, ...more];

var arr1 = ['a', 'b'];
var arr2 = ['c'];
var arr3 = ['d', 'e'];

// ES5 的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6 的合并数组
[...arr1, ...arr2, ...arr3];
// [ 'a', 'b', 'c', 'd', 'e' ]

与解构赋值结合

// ES5
a = list[0], rest = list.slice(1);
// ES6
[a, ...rest] = list;

alist 数组中的第一个,而 rest 取剩下的所有。 ES5ES6 实现的区别。同样的 REST 之后不可以用别的解构的值,并且只能在数组中使用。

函数的返回值

JavaScript 中若需要返回多值时,只能使用对象或是数组,而扩展运算符本质上是数组,所以也可以用着函数的返回值中。

function test(...args){
    return [...args];
}

test(["a", "b", "c"]);
// ["a", "b", "c"]

字符串

将字符串转为真正的数组。

[...'hello'];
// [ "h", "e", "l", "l", "o" ]

Iterator 对象

任何 Iterator 接口的对象,都可以用扩展运算符转为真正的数组。

let map = new Map([
    [1, 'one'],
    [2, 'two'],
    [3, 'three'],
]);

let arr = [...map.keys()];      // [1, 2, 3]

箭头函数

感觉这点时 EMCAScript 6 中极其重要的一点,所以单独会有一节。

总结

主要是函数参数的一些注意点,以及一些新特性,由于箭头函数在 ES6 中应该会成为随手就写的编程方式,所以单独开一篇。

Read more

Gitlab 搭建

Gitlab 搭建

为什么? 想要自己搭建一个代码仓库无非是以下几点原因: 1. 公司内部项目 2. 自己的项目,但不适合在公网 3. 大部分的 git 仓库虽然有私有服务,但价格都不便宜,甚至不如一台云服务器来的便宜 配置及安装文档 Gitlab * 由于 gitlab 会用到 22 端口端口转发的化就走不了 git clone 的默认配置,且占用内存较高,不推荐使用 docker 进行部署; * 由于 gitlab 自带 nginx 默认情况下会与属主机的 nginx 冲突,因此推荐只使用 gitlab 自带的 nginx 进行端口转发; 最小化配置 # path /etc/gitlab/gitlab.rb external_url 'http://git.

By breeze
NPM 私服

NPM 私服

verdaccio 私服搭建,搭建过程中的一些问题以及解决: * docker compose 启动后,可能会报错:配置找不到,添加 config.yaml 文件到映射的 ./conf 目录即可,config.yaml 具体内容可在官网找到,下方也有最小化配置。 services: verdaccio: image: verdaccio/verdaccio container_name: 'verdaccio' networks: - node-network environment: - VERDACCIO_PORT=4873 - VERDACCIO_PUBLIC_URL=http://npm.demo.com ports: - '10001:4873'

By breeze