ES6 学习笔记(三)

Symbol

Symbol 是 JavaScript 的第七种类型(前六种是:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object))。它用于生成独一无二的变量名,防止相同变量名的相互覆盖,例如在对象中,用字符串声明的属性如果相同,那么会互相覆盖

注意点:

  • Symbol 不能与其他值一起计算,例如与字符串连接,但是可以使用 toString() 进行转换
  • Symbol 不能使用点运算符作为属性名,只能使用方括号,避免与字符串方式冲突
  • Symbol 作为属性名,该属性不会出现在 for…in、for…of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames() 返回
  • Symbol 可以被 Object.getOwnPropertySymbols() 返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var s1 = Symbol('foo');
var s2 = Symbol('bar');
var s3 = Symbol();
var s4 = Symbol();
var s5 = Symbol('foo');
s1 // Symbol(foo)
s2 // Symbol(bar)
s3 === s4; // false
s1 === s5; // false
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
var mySymbol = Symbol();
var a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"

Symbol.for() & Symbol.keyFor()

Symbol.for(name)Symbol(name) 不同的地方在于,前者在没有存在 name 注册的 Symbol 变量时,会注册一个新的 Symbol 变量,如果存在,那么就会返回该 name 注册的 Symbol。后者不管 name 有没有注册过,都会产生一个新的 Symbol

1
2
3
4
5
6
7
8
9
10
var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
s1 === s2 // true
Symbol.for("bar") === Symbol.for("bar")
// true
Symbol("bar") === Symbol("bar")
// false

Symbol.keyFor(Symbol) 会返回已注册过的 Symbol 的值的 desc

1
2
3
4
5
var s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
var s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

Proxy

Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程

new Proxy(target, handler)

target 为要通过 handler 进行处理拦截的对象

get()

用于拦截对对象属性访问时的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var person = {
name: "张三"
};
var proxy = new Proxy(person, {
get: function(target, property) {
if (property in target) {
return target[property];
} else {
throw new ReferenceError("Property \"" + property + "\" does not exist.");
}
}
});
proxy.name // "张三"
proxy.age // 抛出一个错误
// 对数组负数的索引值进行处理
function createArray(...elements) {
let handler = {
get(target, propKey, receiver) {
let index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
return Reflect.get(target, propKey, receiver);
}
};
let target = [];
target.push(...elements);
return new Proxy(target, handler);
}
let arr = createArray('a', 'b', 'c');
arr[-1] // c

set()

用于拦截设置属性时的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// 对于age以外的属性,直接保存
obj[prop] = value;
}
};
let person = new Proxy({}, validator);
person.age = 100;
person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错

apply(target, context, args)

用于拦截函数的调用、call和apply操作。target 为目标函数,context 为函数执行上下文,args 为函数参数

1
2
3
4
5
6
7
8
9
10
11
var target = function () { return 'I am the target'; };
var handler = {
apply: function () {
return 'I am the proxy';
}
};
var p = new Proxy(target, handler);
p()
// "I am the proxy"

has()

用于处理一些在执行 “xxx in obj” 的拦截,可以对某些属性进行隐藏

1
2
3
4
5
6
7
8
9
10
11
var handler = {
has (target, key) {
if (key[0] === '_') {
return false;
}
return key in target;
}
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
'_prop' in proxy // false

construct()

用于拦截 new 操作时的处理

1
2
3
4
5
6
7
8
9
10
var p = new Proxy(function() {}, {
construct: function(target, args) {
console.log('called: ' + args.join(', '));
return { value: args[0] * 10 };
}
});
new p(1).value
// "called: 1"
// 10

deleteProperty()

用于拦截删除对象属性时的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
return true;
}
}
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
var target = { _prop: 'foo' }
var proxy = new Proxy(target, handler)
delete proxy._prop; // 报错

defineProperty()

用于拦截定义对象属性时的操作

1
2
3
4
5
6
7
8
9
var handler = {
defineProperty (target, key, descriptor) {
return false
}
}
var target = {}
var proxy = new Proxy(target, handler)
proxy.foo = 'bar';
console.log(proxy.foo); // undefined

enumerate()

用于拦截 “for xxx in obj” 时的处理操作,跟 has() 不是同种拦截,互不影响

1
2
3
4
5
6
7
8
9
10
11
12
// 这个例子在 FF 上成功,但是 Chrome 上依旧打印了所有属性
var handler = {
enumerate (target) {
return Object.keys(target).filter(key => key[0] !== '_')[Symbol.iterator]();
}
}
var target = { prop: 'foo', _bar: 'baz', _prop: 'foo' }
var proxy = new Proxy(target, handler)
for (let key in proxy) {
console.log(key);
// "prop"
}

Reflect

Reflect 对象提供了若干个能对任意对象进行某种特定的可拦截操作(interceptable operation)的方法,它所提供的静态方法在 Proxy 中都有对应的相同方法,可以这么说,Reflect 保留了原先的默认处理方式,以供 Proxy 在修改默认处理的同时也能应用默认处理后再进行一些其他处理。某些方法也是 Object 上的方法,不过修改相应的返回值,更加完善
参考资料:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
// 老写法
'assign' in Object // true
// 新写法
// 行为被函数化
Reflect.has(Object, 'assign') // true
// 添加了自己的行为,同时使用 Reflect 的相应静态方法执行默认行为
var loggedObj = new Proxy(obj, {
get(target, name) {
console.log('get', target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log('delete' + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log('has' + name);
return Reflect.has(target, name);
}
});

可能有人会对 Reflect 的存在感到疑惑,我个人认为 Reflect 存在的意义大概有两点,一是对 ES5 中 Object 一些方法的返回值进行修改完善,但是为了兼容性,开拓了一个新的对象来存放这些静态方法。二是为了让 Proxy 可以方便地在拦截器中添加处理方法的同时调用默认处理方法,所以 Reflect 中存在的静态方法都跟 Proxy 中的方法一一对应,同时修改了部分 Object 中的方法的返回值,例如 Object 中一些调用失败的方法会抛出错误,而在 Reflect 中修改为返回布尔值,以上是一点点见解,可以参考一下:https://tc39.github.io/ecma262/#sec-reflect-object

以上实例代码大部分来自阮一峰老师的 《ECMAScript 6 入门》 一书
书籍在线阅读地址: http://es6.ruanyifeng.com/#README

ES6 学习笔记(二)

函数的扩展

扩展符号…

ES6 新增了符号 ...,该符号类似于剩余函数的逆运算,能将一个数组变为一组参数传入函数中,只要实现了 Iterator 接口的对象,都可以使用该符号转换数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function push(array, ...items) {
array.push(...items);
}
function add(x, y) {
return x + y;
}
var numbers = [4, 38];
add(...numbers) // 42
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
arr1.push(...arr2);
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' ]

箭头函数

箭头函数能很方便地创建匿名函数,特别是在 JavaScript 这种回调大量应用的语言中,箭头函数能让语法看起来更自然,使用起来也更便捷

箭头函数需要注意一下几个点:

  • this 指向创建函数时的对象,而非执行时的对象
  • 无法将其作为构造函数,即无法 new,否则会报错
  • 无法使用 arguments,不过可以使用剩余函数 rest 来实现相同效果
  • 无法使用 yield 命令,即无法作为 Generator 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var result = values.sort((a, b) => a - b);
var f = () => 5;
// 等同于
var f = function (){ return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
var sum = (num1, num2) => { return num1 + num2; };
var getTempItem = id => ({ id: id, name: "Temp" });
// 嵌套函数
function insert(value) {
return {into: function (array) {
return {after: function (afterValue) {
array.splice(array.indexOf(afterValue) + 1, 0, value);
return array;
}};
}};
}
insert(2).into([1, 3]).after(1); //[1, 2, 3]
// 使用箭头函数改写
let insert = (value) => ({into: (array) => ({after: (afterValue) => {
array.splice(array.indexOf(afterValue) + 1, 0, value);
return array;
}})});
insert(2).into([1, 3]).after(1); //[1, 2, 3]

ES6 对函数还有一个 Tail call optimization (即“尾调用优化”),等后面再重新写一篇文章分析,尾调用优化将会大大避免递归的栈溢出

对象的扩展

简写

ES6 中对象中属性的声明有了简便的做法,请看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var birth = '2000/01/01';
var Person = {
name: '张三',
//等同于birth: birth
birth,
// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }
};
// 在 CommonJS 中使用 exports 暴露接口时很方便
module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
getItem: getItem,
setItem: setItem,
clear: clear
};

判断对象相等

ES6 使用 Object.is() 来判断两个对象是否相等,相对于使用 =====,有以下两个区别

1
2
3
4
5
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

对象复制

ES6 使用 Object.assign() 对一个对象进行复制,但是执行的是浅复制,如果被复制的对象中有属性是一个对象,那么该对象的属性将会被共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var target = { a: 1, b: 1 };
var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
Object.assign(target) === target // true
Object.assign(undefined) // 报错
Object.assign(null) // 报错
var v1 = 'abc';
var v2 = true;
var v3 = 10;
// 字符串是可枚举的,所以可以被复制,不可枚举属性将被忽略
var obj = Object.assign({}, v1, v2, v3);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
var obj = Object.assign(true); // true会被转换为对象返回
Object.assign([1, 2, 3], [4, 5]); // [4, 5, 3]

以上实例代码大部分来自阮一峰老师的 《ECMAScript 6 入门》 一书
书籍在线阅读地址: http://es6.ruanyifeng.com/#README

ele.me 二面代码题实现

饿了么二面是在线写代码,有三道题,一道题十分钟内写完提交,在一个网站写,会自动录制代码编写过程

  1. 首先写了一个判断数组的函数

    1
    2
    3
    function isArray(array) {
    return !!array && Object.prototype.toString.call(array) === "[object Array]";
    }
  2. 有序数组的打乱

    1
    2
    3
    4
    5
    6
    7
    function random(array) {
    if(!isArray(array)) return array;
    return array.sort(function() {
    return 0.8 - Math.random();
    });
    }
  3. 多维数组变为一维数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    function flatten(array) {
    if(!isArray(array)) return [array];
    let len = array.length;
    let flattenArr = [];
    for(let idx=0;idx<len;idx++) {
    Array.prototype.push.apply(flattenArr, isArray(array[idx]) ? flatten(array[idx]) : [array[idx]]);
    }
    return flattenArr;
    }
    // 使用 ES6 的 Generator 函数实现
    function flattenES6(array) {
    function* iterator(array) {
    if(isArray(array)) {
    for(let i=0;i<array.length;i++) {
    yield* iterator(array[i]);
    }
    }else {
    yield array;
    }
    }
    let result = [];
    for(let value of iterator(array)) {
    result.push(value);
    }
    return result;
    }
  4. 将一个乱序数组,里面有*数字,将*提到最前,数字往后放,并且数字顺序不变

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    function arrange(array) {
    if(!isArray(array)) return array;
    let prePoint = 0,
    nextPoint = 1,
    len = array.length;
    while(true) {
    while(array[prePoint] === "*" && prePoint < len) {
    prePoint++;
    }
    while(array[nextPoint] !== "*" && nextPoint < len) {
    nextPoint++;
    }
    while(prePoint > nextPoint) {
    nextPoint++;
    }
    if(prePoint >= len || nextPoint >= len) {
    break;
    }
    // exchange
    [array[prePoint], array[nextPoint]] = [array[nextPoint], array[prePoint]];
    }
    return array;
    }

ES6 学习笔记

let 和 const

let

使用 let 定义的变量只在块级作用域里可以访问,而 var 定义的变量没有块级作用域的概念,只要在作用域内即可访问

我们可以在循环内使用 let 代替 var

1
2
3
4
for(let i=0;i<5;i++) {
console.log(i);// 0,1,2,3,4
}
console.log(i);// Uncaught ReferenceError: i is not defined

let 声明的变量不存在变量提升

1
2
3
4
5
console.log(i);// undefined
console.log(j);// Uncaught ReferenceError
var i = 5;
let j = 6;

暂时性死区(temporal dead zone,简称TDZ)

在使用 let 和 const 定义变量的块级作用域里,会形成封闭的块级作用域,在使用 let 或 const 定义变量的语句之前,该变量无法被赋值,都会抛出 ReferenceError

1
2
3
4
5
6
7
8
9
10
11
if(true) {
tmp = "a";// ReferenceError
console.log(tmp);// ReferenceError
// 死区结束
let tmp;
console.log(tmp);// undefined
tmp = "b";
console.log(tmp);// b
}

其他一些限制

  • 使用 let 声明的变量不允许在同一块级作用域内重复声明
  • 块级作用域内声明的函数只在该块级作用域内可用
  • ES6 块级作用域内的声明的函数不存在函数提升

const

使用 const 声明的变量无法更改,严格模式下重复赋值会报错,而常规模式下不报错也不赋值成功

限制

  • const 同样存在暂时性死区
  • const 不允许重复定义,不存在变量提升
  • 使用 const 声明一个对象,只保证该对象的指针不被修改,不保证该对象内部属性不被修改
  • 使用 const 声明的变量只在当前块级作用域内有效
  • 使用 const 声明的变量可以跨模块使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // constants.js 模块
    export const A = 1;
    export const B = 3;
    export const C = 4;
    // test1.js 模块
    import * as constants from './constants';
    console.log(constants.A); // 1
    console.log(constants.B); // 3
    // test2.js 模块
    import {A, B} from './constants';
    console.log(A); // 1
    console.log(B); // 3
  • 在 window 作用域下使用 const 声明的变量不会变为 window.? 变量

解构赋值

ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// ES5
var a = 1;
var b = 2;
var c = 3;
// ES6
// 解构成功
var [a, b, c] = [1, 2, 3];
// 解构失败——部分解构
var [x, y] = [1];
console.log(x);// 1
console.log(y);// undefined
// 解构成功——不完全解构
var [i, [j], k] = [1, [2, 3], 4];
console.log(i);// 1
console.log(j);// 2
console.log(k);// 4
// 模式匹配的解构
var [a1, [a2, a3]] = [1, [2, 3]];
console.log(a1);// 1
console.log(a2);// 2
console.log(a3);// 3
var [, , tmp] = [1, 2, 3];
console.log(tmp);// 3
var [x1, ...x2] = [1, 2, 3, 4, 5];
console.log(x1);// 1
console.log(x2);// [2, 3, 4, 5]
var [y1, y2, ...y3] = [1];
console.log(y1);// 1
console.log(y2);// undefined
console.log(y3);// []
// 有默认值的解构赋值
// 只有对应赋值是 undefined,默认值才会生效
var [z1 ,z2 = 2] = [1, undefined];
console.log(z1);// 1
console.log(z2);// 2
// 对象的解构赋值
// 位置可以不同
var {q2, q1} = {q1: 1, q2: 2};
console.log(q1);// 1
console.log(q2);// 2
// 下面这个有点反人类
// 真正被赋值的是后者 t1, t2,而不是前者 r1, r2
var {r1: t1, r2: t2} = {r1: 1, r2: 2};
console.log(t1);// 1
console.log(t2);// 2
// 字符串的解构赋值
var [s1, s2, s3] = "abc";
console.log(s1);// a
console.log(s2);// b
console.log(s3);// c
// 函数参数的解构赋值
function func([x, y]) {
return x + y;
}
func([1, 2]);// 3
function func2([x = 1, y = 2] = []) {
return [x, y];
}
func2([2, 3]);// [2, 3]
func2([2]);// [2, 2]
func2([]);// [1, 2]
// 变量赋值交换
function exchange([x, y]) {
var [x, y] = [y, x];
return [x, y];
}
exchange([1, 2]);// [2, 1]

字符串

Unicode 表示法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 单字节 Unicode
console.log("\u0061");// a
// 双字节 Unicode
console.log("\u{20BB7}");// 𠮷
let hello = 123;
hell\u{6F} // 123
// 对于双字节字符,用 codePointAt() 正确获取对应十进制字符码
var s = "𠮷a";
// 这里是“𠮷”的十进制字符码
console.log(s.codePointAt(0));// 134071
// 这里是“𠮷”的第二个十进制字符码
console.log(s.codePointAt(1));// 57271
// 这里是“a”的十进制字符码
console.log(s.codePointAt(2));// 97
// 合成字符的规范化,用于正确判断相等性
console.log("\u01D1".normalize() === "\u004F\u030C".normalize()) // true
console.log("\u01D1" === "\u004F\u030C");

其他函数

  • includes(code, startIndex):返回布尔值,表示是否找到了参数字符串
  • startsWith(code, startIndex):返回布尔值,表示参数字符串是否在源字符串的头部
  • endsWith(code, startIndex):返回布尔值,表示参数字符串是否在源字符串的尾部
  • repeat():返回一个新字符串,表示将原字符串重复n次

模板字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// ES5
$("#result").append(
"There are <b>" + basket.count + "</b> " +
"items in your basket, " +
"<em>" + basket.onSale +
"</em> are on sale!"
);
// ES6
// 注意模板字符串使用 `` 代替 ""
// ${} 代表变量
$("#result").append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);
// 可以在模板字符串内运算
var x = 1;
var y = 2;
`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"
`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"
var obj = {x: 1, y: 2};
`${obj.x + obj.y}`
// 3
// 可以调用函数
function fn() {
return "Hello World";
}
`foo ${fn()} bar`
// foo Hello World bar
// raw 会自动转义斜杠
String.raw`Hi\n${2+3}!`;// Hi\n5!
// 此外还有标签模板等扩展,更详细的看教程

数组的扩展

新的数组函数

  • Array.from(),用于将类数组对象转换为真正的数组,如果有第二个参数可是实现类似 map() 的效果,使用[…array]会有相同结果
  • Array.of(),用于将一组数值转换为数组对象
  • (new Array()).copyWithin(target, startIndex, endIndex),用于将 startIndex 到 endIndex 的元素复制到 target 位为开头的位置上
  • (new Array()).find(arr, callback),用于寻找 arr 数组中符合 callback 中条件的第一个元素
  • (new Array()).findIndex(arr, callback),用于寻找 arr 数组中符合 callback 中条件的第一个元素的索引
  • (new Array()).fill(char, starIndex, endIndex),用于用 char 填充数组
  • (new Array()).entries(),返回所有键-值(key-value)对
  • (new Array()).keys(),返回所有键(key)的值
  • (new Array()).values(),返回所有值(value)

以上例子参考自阮一峰老师的《ECMAScript 6入门》一书的开源版本,地址:http://es6.ruanyifeng.com/#README

gulp 简单使用

前言

今天必须安利一下 gulp 这个前端自动化构建工具,天呐噜,今天学了一下怎么用以后,我已经忘记什么是 Grunt 了!gulp 比 Grunt 好的地方就是,基于 Node.js 的流,使用 JS 语法,配置项相对于 Grunt 的简直就简单得不要不要的(虽然我 Grunt 也用得很少,因为发现用 Grunt 的配置时间比我自己去压缩什么用的时间还长……),另外,两个配置文件的代码清晰度和简洁度的对比上,gulp 就是完胜啊么么哒~

引入和使用

  1. 安装好 Node.jsnpm 两个神器以后,往下看

  2. 使用 npm 全局安装 gulp,命令行如下所示

    npm install gulp -g

  3. 或者你也可以不全局安装,仅仅作为项目开发依赖,那么使用如下命令行安装 gulp

    npm install gulp –save-dev

  4. 在项目根目录下创建一个 gulpfile.js 文件,用于 gulp 任务的配置

  5. gulpfile.js 里进行任务配置,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    var gulp = require("gulp");
    // JavaScript 压缩
    var uglify = require("gulp-uglify");
    // JavaScript 语法检测
    var jshint = require("gulp-jshint");
    // 文件输出重命名
    var rename = require("gulp-rename");
    gulp.task("jshint", function() {
    gulp.src("Nope.js")
    .pipe(jshint())
    .pipe(jshint.reporter("default"));
    });
    gulp.task("compress", function() {
    gulp.src("Nope.js")
    .pipe(rename("Nope.min.js"))
    .pipe(uglify())
    .pipe(gulp.dest("gulpJS/"));
    });
    gulp.task("default", ["jshint", "compress"]);
  6. 在当先目录下使用命令行工具,输入命令执行 gulp 进行自动构建

    gulp
    或者
    gulp jshint

如果单独输入 gulp,那么会执行 default 任务,如果在 gulp 后面加一个任务名称,那么就是执行特定名称任务

API 说明

  • gulp.task(name[, deps], fn)

    task 用于配置一个单独的任务,name 是该任务的名称;deps 是该任务的依赖任务,会在该任务执行前执行完毕;fn 是该任务的执行体。

  • gulp.src(globs[, options])

    src 用于配置该任务的目标文件,globs 可以配置文件路径,当然不止于此,更多的写法可以查看官网的 API 文档;options 用于配置一些选项,如 {buffer: false},将文件输入改为 stream 而不是 buffer。

  • gulp.pipe()

    使用过 Node.js 就很容易明白中间件的概念,pipe 的作用类似于中间件的作用,对文件进行处理后输出。

  • gulp.dest(path[, options])

    dest 用于将 pipe 进来的文件流输出到 path 下,options 用于配置写文件的一些权限等。

结束语

上面提到了 gulp 的几个常用 API,此外还有几个 API 我没用到,例如 watch,可能后续在使用 less 的时候会用到。gulp 的 API 就是这么少,更多的依赖中间件处理,所以代码也更简洁,gulp 还有很多有趣的地方我没发现,希望后续学习可以发掘更多 gulp 的优点。

官方中文文档:http://www.gulpjs.com.cn/docs/api/

简单使用 require.js

前言

CommonJSAMD 是目前前端模块化的两个常用规范,此外,还有从 sea.js 引申出来的 CMD 规范,与 AMD 规范有异曲同工之妙,由于没有实际使用过,所以还是不赘述了。

CommonJS

CommonJS 规范在 JavaScript 上的应用比较广泛的当属 Node.js。Node.js 是运行在 V8 引擎上的以 JavaScript 作为编程语言的服务端语言,模块化对服务端是非常重要的。美国程序员 Ryan Dahl(Node.js 创始人)在使用了 CommonJS 规范来对 Node.js 的模块进行规范管理,并且模块的加载时同步的。你可以像下面一样引入模块:

1
2
3
4
5
6
7
8
var express = require("express");
var app = express();
app.get("/something", function(req, res, next) {
// doing something
})

以上代码引入了 express 框架模块,并且执行了 express,这样 app 这个变量就得到了执行后的一个实例,并可以使用其中的各种函数方法。

为什么使用 AMD?

既然有了 CommonJS 规范来进行 JavaScript 的模块化管理,为什么还会出现 AMD 规范?原因很简单,环境不同,在服务端环境中,由于模块文件都存在于服务器中,所以请求模块的时间几乎可以忽略,而且请求模块的次数大多数只请求一次就可以从服务器开启一直使用到服务器重启。而在客户端环境中,每次刷新页面都会重新请求 JavaScript 文件,并且,CommonJS 是同步加载模块文件,所谓同步,就是必须等到 JavaScript 模块文件请求并加载完毕才会继续往下执行程序,在客户端页面中,如果一个 JavaScript 模块文件请求和加载的时间很长,那么页面的渲染和后续的程序执行将会一直被阻塞。所以,客户端的模块化管理需要一种 非阻塞式异步 的加载方式,而 AMD 规范就是这么一种规范。

require.js

require.js 是对 AMD 规范的一种实现和执行,此外,require.js 也可以使用 CommonJS 风格的方式。

引入

1
<script data-main="javascripts/main.js" src="javascripts/require.js"></script>

以上,引入了 require.js 的同时,使用 data-main 指定了最早加载的一个模块,require.js 里的 baseUrl 同时默认被设置为 javascripts,如果此后没有显式地使用 require.config() 对 baseUrl 和 paths 进行设置,那么所有依赖模块的基础路径都将会以该 baseUrl 路径作为基础路径进行搜索。

require.config(options)

1
2
3
4
5
6
7
8
9
10
require.config({
baseUrl: "./libs",
paths: {
jquery: "jquery-2.2.2",
underscore: "/underscore",
backbone: "http://backbonejs.org/backbone"
}
})

以上代码中,我们显式地设置了模块搜索路径的 baseUrl 为 ./libs,同时在 paths 中,我们指定了 jqueryunderscorebackbone 的路径,这里使用了三种不同的路径定义方式,会有不同的搜索结果。假设我们当前页面的路径为 http://localhost,那么三个模块文件的路径如下:

  • jquery 的路径为 baseUrl + paths路径 = http://localhost/libs/jquery-2.2.2.js

  • underscore 的路径为 当前路径 + paths路径 = http://localhost/underscore.js

  • backbone 的路径为 paths 路径 = http://backbonejs.org/backbone.js

这里可以发现,所有 JavaScript 的后缀 .js 都不能加上去,不然会请求类似 underscore.js.js 这样的错误文件。以上就是模块文件路径指定的多种不同方式所产生的不同结果。

define(name, deps, callback)

require.js 使用 define(name, deps, callback) 来定义模块,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// first
define(function() {
// do somthing
return {
// return a key-value object
api: "myApi"
};
});
// second
define(["jquery", "underscore"], function($, _) {
// do something
return ……;
});
// third
define("foo/user", ["jquery", "underscore"], function($, _) {
// do something
return ……;
});
  • 第一种方法是简单的定义模块,这里可以在 callback 里直接定义一些东西,使用 return 返回一个对象字面量

  • 第二种方法首先定义了该模块的依赖模块 jquery 和 underscore,然后定义一个 callback函数,当 jquery 和 underscore加载完成后,其暴露的接口将会依次传给 callback 的 arguments

  • 第三种方法首先定义了该模块的名称,后面的如第二种方法一样,但是不推荐自定义模块名称,如果你移动了该模块的路径,那么就得重命名

require(deps, callback)

require.js 使用 require(deps, callback) 来请求加载模块,如下代码所示:

1
2
3
4
5
require(["jquery"], function($) {
// do something
// never need return anything
});

require() 和 define() 的使用上很相似,但是 require() 用来加载,define() 用来定义,区别是前者不需要返回一个对象,后者需要返回一个对象来代表该模块的接口。

CommonJS 风格的 require.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 可以解决循环依赖
define(function(require, exports, module) {
var base = require("./base");
exports.a = function() {
return base;
};
});
// 就近依赖
define(["require", "jquery"], function(require) {
var $ = require("jquery");
return function() {
return $;
}
});
// 定义模块并暴露接口
define(["exports", "a"], function(exports, a) {
exports.bar = function() {
return a.bar();
};
});

以上三种 CommonJS 风格的定义模块方法,更详细的请参考 http://www.requirejs.cn/

参考来源: http://www.ruanyifeng.com/blog/2012/11/require_js.html

DAY5 完成 & 总结

最终效果图

演示地址

http://www.libinhong.com:90/

踩到的坑

  • Backbone 的 View 监听本身 Model 的 change 事件,仅在 Model 的属性实际发生变化才会被触发,如果 set 一个相同的值进 Model,实际上并不会触发事件

  • Backbone 的 View 中的 el 代表了该 View 对应的 dom 节点,同时,在该 View 中定义的事件所对应的 dom 只能处于该 el 下,否则无法正确找到 dom 并添加事件

  • Backbone 的 events 无法绑定 audio 的事件,估计是因为 JQuery 无相对应的事件,所以只能自己手动绑定事件

总结

  • 最终没有使用 require.js

  • 在使用 Backbone 的过程中,V 层聚集了大量的渲染页面的逻辑,C 层则更多的提供了集合的功能,并且向服务器 fetch 数据,M 层则是模型层,没有涉及逻辑。

  • 对 MVC 结构有了更深的了解,对分层的好处也有了体会

  • 编码期间不断对功能性函数的重构是很重要的,即使是两行代码也有可能需要独立为一个功能函数,到了后期发现每一个独立的功能函数都能应用到对应的地方,避免了后期代码行的重复

吐槽

Oxygen 从 20 号开始写,从 需求 —— 原型 —— 实现 一步步走下来一共经历 6 天,实际工作时间不会超过 5 天,中间夹杂着各种上课、做实验、做作业,这次算是对前面一个阶段学习的总结。第一次尝试了 Backbone.js 和 JQuery.js,使用手段颇为生疏,希望以后有机会再次熟悉一下各种 API 的使用。最后吐槽一下接下来十几周都要面对 JavaEE 和 JSP 和 SSH 的课程的痛苦……

DAY4 Backbone.js API阅读(二)

Collection

Backbone.js 的 C 层不同于通常 MVC 架构的 Controller 层,而是 Collection 层,Collection 层不仅负责与数据库的交互和 Model 的数据交互,同时也是 Model 的一个有序集合。当集合中的 Model 发生变化时,将会触发该集合的 change 事件,当使用 fetch() 函数时,可能会触发 add 或者 remove 事件。而一些在 Model 上触发的事件可能也会在 Collection 中触发。

  • extend——用于创建一个新的 Collection 集合并对其进行相应扩充和定义,如下定义:

    1
    2
    3
    4
    var books = Backbone.Collection.extend({
    model: Book,
    ……
    })
  • model——用于定义该集合中的 Model 类型

  • add——向 Collection 添加一个或多个 Model,同时会触发 add 事件
  • remove——从 Collection 移除一个或多个 Model,同时会触发 remove 事件
  • reset——当有大量 Model 需要更改的时候,这时候使用 reset 插入一个模型组,同时触发 reset 事件,但是不会触发 addremove 事件
  • set——通过传入一组 Model,如果传入的 Model 在 Collection 已经存在,那么将会合并,如果不存在,那么将会添加,如果 Collection 中存在传入 Model 组中不存在的 Model,那么该 Model 将会被删除。以上将会触发 addremovechange 事件
  • fetch——从服务端拉取更新数据,并使用 reset 到 Collection 中。
  • comparator——设置 Collection 排序的依据,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var Chapter = Backbone.Model;
    var chapters = new Backbone.Collection;
    chapters.comparator = 'page';
    chapters.add(new Chapter({page: 9, title: "The End"}));
    chapters.add(new Chapter({page: 5, title: "The Middle"}));
    chapters.add(new Chapter({page: 1, title: "The Beginning"}));
    alert(chapters.pluck('title'));
  • Collection 同时提供了多个 Underscore.js 的函数,例如:

    1
    2
    3
    books.each(function(book) {
    // doing something
    })

reset 用于重置数据,即将原有数据删除,添加传入的新数据,而 set 则更新数据,只有当原先集合中的某个 Model 不存在于新传入的数据中时,该 Model 才会被删除,否则只是合并数据。总结就是 reset 是重置,set 是更新。

View

View 用来处理视图的渲染以及视图的更新,我们可以自定义一个 render 函数来定义当模型数据发生变化时(render 可以绑定在自身模型的 change 事件上)如何渲染视图。

  • 一般地,我们如下所示去扩充一个自定义 View:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    var DocumentRow = Backbone.View.extend({
    tagName: "li",
    className: "document-row",
    events: {
    "click .icon": "open",
    "click .button.edit": "openEditDialog",
    "click .button.delete": "destroy"
    },
    initialize: function() {
    this.listenTo(this.model, "change", this.render);
    },
    render: function() {
    ...
    }
    });
  • tagName——代表 View 的标签类型,设置以后同时也会设置 View 的 el

  • className——设置 View 的样式类
  • events——在该视图中绑定相应的事件
  • initialize——在 View 被实例化时执行
  • render——默认无任何操作,我们可以重载该函数,定义如何渲染视图,将其绑定到 Model 的 change 事件,这样当 Model 发生变化时,可以立即调用 render 函数更新视图。
  • el——引用了该 View 的 dom 元素
  • $el——通过引用 el,对其进行封装,其可以使用 JQuery 的函数
  • setElement——将 View 从旧的引用 dom 对象切换到新的 dom 对象引用,视图的事件委托和 $el 也响应迁移
  • template——利用 Underscore 的 template 方法可以为 View 设定一个模板,有两种方法,如下所示:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // first
    var LibraryView = Backbone.View.extend({
    template: _.template(...)
    });
    // second
    <script id="teamTemplate" type="text/template">
    <%= name %>
    </script>
    App.Views.Team = Backbone.View.extend({
    className : '.team-element',
    tagName : 'div',
    model : new App.Models.Team
    render : function() {
    // Compile the template
    var compiledTemplate = _.template($('#teamTemplate').html());
    // Model attributes loaded into the template. Template is
    // appended to the DOM element referred by the el attribute
    $(this.el).html(compiledTemplate(this.model.toJSON()));
    }
    });

明天继续看 Router 的使用,今晚参加了网易的笔试,还是有点累。

DAY3 Backbone.js API阅读

原来机房看 API 的学习效率奇低,今天在机房上一天课,下午5点半下个脚都是麻的,今天主要还是熟悉一下 Backbone.js 的API,下面看几个比较重要的 API,也算是对今天学习的一个备忘录。官方文档写得有点简单了,后来看官方提供的 Todos 的例子的源码加上一边看官方文档才渐渐明白运行机制。

Backbone.js

Backbone.js 强制依赖 Underscore.js,其很多实现方式都利用了 Underscore 的 API,由于之前通读过 Underscore 的源代码,所以这个并没有太大问题。此外,Backbone.js 还半依赖 JQuery.js,主要是 Backbone 里的一个 save 函数当执行时会执行 Backbone.sync 从而需要使用 ajax 访问服务器进行 Model 的保存,当然,可以通过重载 Backbone.sync 来使用其他方式保存 Model,例如Backbone.localStorage.js就是一个用于重载 sync 的库。

Model

Model 用于处理 数据交互逻辑,一般地,使用 Backbone.Model.extend(properties, [classProperties]) 创建一个新的模型,properties 为一个对象实例,里面包含多个属性:

  • defaults——用于设置一些初始的模型属性,如下面代码所示:

    1
    2
    3
    4
    5
    6
    7
    var Meal = Backbone.Model.extend({
    defaults: {
    "appetizer": "caesar salad",
    "entree": "ravioli",
    "dessert": "cheesecake"
    }
    });
  • initialize——在模型创建后执行的函数

  • save——将模型的 attributes 模型状态保存到持久层
  • validate——用于验证调用 save 以后传入的 attributes 的值的合法性,如果有返回数据,代表验证失败,save 就不会成功保存模型,如果在 set 中传入 {validate:true},那么会执行 validate 验证。
  • fetch——从服务器更新模型数据,如果数据有更新,则会触发 change 事件,同时接受 successerror 回调
  • previous——在该模型的 change 事件中,可以通过此函数获取上一次更改的值
  • previousAttributes——同样用过此函数获取上一次更改的属性散列副本
  • 可以重载 Model 中的函数,如 getset

DAY2 播放器页面的实现

早上8点多起床转角遇到爱——一只好大的蜘蛛,面对面吓蒙逼了。下午将播放器的图给切出来,主要还是微调一下样式比较麻烦,测试了一下 IE 9+ 都是正常显示的,IE 8 不支持 border-radiusbox-shadow 就没办法啦,rem 也不能用,但是这个能用正常的 px 解决。

最终页面效果图

实现过程

  • 进度条的布局,典型的双飞翼布局方式,左右固定了 60px 作为左右两个时间的空间
  • 进度条的实现,用了三个 div 分别代表 当前进度加载进度进度总长
  • 歌曲的封面,为了实现宽度固定,焦点居中,使用 div 的 background 加载封面,background-position 可以实现焦点居中
  • 歌词的显示,使用 <ul> 可以实现,后面也方便进行滚动
  • 控制器的样式,引入了 font-awesome 作为图标
  • 音量调节的弹出,使用 CSS 的 :hover 进行一个样式优先级转换,还是尽量避免使用 JS 实现效果
  • 一些字体的凹凸感,使用了 text-shadow 实现,阴影往下偏移1个像素,发散一个像素,颜色使用比字体颜色偏浅的黑色

demo查看

Stay folish<br><br>Stay hungry