类型

JavaScript类型主要包括了primitiveobject类型,其中primitive类型包括了:nullundefinedbooleannumberstringsymbol(es6)

参考链接:1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures

  1. http://www.w3.org/html/ig/zh/wiki/ES5/types

类型检测

说到类型检测主要包括了:typeofinstanceofObject.prototype.toString.call(xxx)或{}.toString.call(xxx)。这里在玉伯的博文Sea.js 源码解析(三)中有讨论过Object.prototype.toString.call(xxx)或{}.toString.call(xxx)哪一个更好,有兴趣的可以看一看。

typeof

typeof一般适用于判断primitive类型的数据,在判断object类型的数据时有时会有意想不到的结果,例如:typeof null结果为object。下面的表是typeof元素符的一个结果:

val 类型 结果
Undefined “undefined”
Null “object”
Boolean “boolean”
Number “number”
String “string”
Object(原生,且没有实现 [[Call]]) “object”
Object(原生或者宿主且实现了 [[Call]]) “function”
Object(宿主且没实现 [[Call]]) 由实现定义,但不能是 “undefined”、”boolean”、”number” 或 “string”。

这里还有一篇相关介绍typeof的文章:JavaScript’s typeof operator
也可以看ES5中关于typeof的介绍:typeof 运算符

instanceof

instanceof运算符是用于判断一个实例是否属于某一类型,例如:a instanceof Person,其内部原理实际上是判断Person.prototype是否在a实例的原型链中,其原理可以用下面的函数来表达:

1
2
3
4
5
6
7
8
9
10
11
function instance_of(V, F) {
var O = F.prototype;
V = V.__proto__;
while (true) {
if (V === null)
return false;
if (O === V)
return true;
V = V.__proto__;
}
}

V8源码的runtime.js中也有关于instanceof运算符的描述:

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
// ECMA-262, section 11.8.6, page 54. To make the implementation more
// efficient, the return value should be zero if the 'this' is an
// instance of F, and non-zero if not. This makes it possible to avoid
// an expensive ToBoolean conversion in the generated code.
function INSTANCE_OF(F) {
var V = this;
if (!IS_SPEC_FUNCTION(F)) {
throw %MakeTypeError('instanceof_function_expected', [F]);
}

// If V is not an object, return false.
if (!IS_SPEC_OBJECT(V)) {
return 1;
}

// Check if function is bound, if so, get [[BoundFunction]] from it
// and use that instead of F.
var bindings = %BoundFunctionGetBindings(F);
if (bindings) {
F = bindings[kBoundFunctionIndex]; // Always a non-bound function.
}
// Get the prototype of F; if it is not an object, throw an error.
var O = F.prototype;
if (!IS_SPEC_OBJECT(O)) {
throw %MakeTypeError('instanceof_nonobject_proto', [O]);
}

// Return whether or not O is in the prototype chain of V.
return %IsInPrototypeChain(O, V) ? 0 : 1;
}

Object.prototype.toString.call(xxx)或{}.toString.call(xxx)

这两个的区别在JavaScript’s typeof operator中有说到:You can always swap {} with Object.prototype, to save creating an object just to exploit its toString() method.。也就是使用Object.prototype.toString会节省创建一个对象。

实际在使用时Object.prototype.toString.call(xxx)会返回类似[object String]的字符串给我们,用他我们就可以很好的判断变量的类型了。

1
2
3
4
5
6
7
var a = 'sss';
var b = [];
var c = function () {}

console.log(Object.prototype.toString.call(a))
console.log(Object.prototype.toString.call(b))
console.log(Object.prototype.toString.call(c))

类型转换

类型转换主要分为两大类:ToPrimitiveToObject。其中ToPrimitive又分为了:ToNumberToStringToBoolean

ToPrimitive

看名字就能知道,ToPrimitive是用于变量需要转换为原始类型时调用。在JavaScript内部实现了该函数,在需要将变量转换为原始类型时就会调用该函数,下面看一下它的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
// ECMA-262, section 9.1, page 30. Use null/undefined for no hint,
// (1) for number hint, and (2) for string hint.
function ToPrimitive(x, hint) {
// Fast case check.
// 如果为字符串,则直接返回
if (IS_STRING(x)) return x;
// Normal behavior.
if (!IS_SPEC_OBJECT(x)) return x;
if (IS_SYMBOL_WRAPPER(x)) throw MakeTypeError('symbol_to_primitive', []);
if (hint == NO_HINT) hint = (IS_DATE(x)) ? STRING_HINT : NUMBER_HINT;
return (hint == NUMBER_HINT) ? %DefaultNumber(x) : %DefaultString(x);
}

上面有一个hint的参数,当没有传入hint参数时,且x不是Date对象时会通过%DefaultNumber(x)来转换,否则通过%DefaultString(x)。这里也可以看到日期类型的对象转换为原始类型时的不同。

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
// ECMA-262, section 8.6.2.6, page 28.
function DefaultNumber(x) {
if (!IS_SYMBOL_WRAPPER(x)) {
// 转换为数字原始类型时,首先通过valueOf来转换
var valueOf = x.valueOf;
if (IS_SPEC_FUNCTION(valueOf)) {
var v = %_CallFunction(x, valueOf);
if (%IsPrimitive(v)) return v;
}

// 否则通过toString
var toString = x.toString;
if (IS_SPEC_FUNCTION(toString)) {
var s = %_CallFunction(x, toString);
if (%IsPrimitive(s)) return s;
}
}
// 否则抛出异常
throw %MakeTypeError('cannot_convert_to_primitive', []);
}

// ECMA-262, section 8.6.2.6, page 28.
function DefaultString(x) {
if (!IS_SYMBOL_WRAPPER(x)) {
// 转换为字符串原始类型时首先通过toString
var toString = x.toString;
if (IS_SPEC_FUNCTION(toString)) {
var s = %_CallFunction(x, toString);
if (%IsPrimitive(s)) return s;
}

// 否则通过valueOf
var valueOf = x.valueOf;
if (IS_SPEC_FUNCTION(valueOf)) {
var v = %_CallFunction(x, valueOf);
if (%IsPrimitive(v)) return v;
}
}
// 否则抛出异常
throw %MakeTypeError('cannot_convert_to_primitive', []);
}

ToNumber

ToNumber是用于将变量转换为number类型,它在V8中的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ECMA-262, section 9.3, page 31.
function ToNumber(x) {
// 如果为number直接返回
if (IS_NUMBER(x)) return x;
// 如果为字符串,则调用StringToNumber转换
if (IS_STRING(x)) {
return %_HasCachedArrayIndex(x) ? %_GetCachedArrayIndex(x)
: %StringToNumber(x);
}
if (IS_BOOLEAN(x)) return x ? 1 : 0;
// 如果为undefined,则返回NAN
if (IS_UNDEFINED(x)) return NAN;
// 如果为symbol,则抛出异常
if (IS_SYMBOL(x)) throw MakeTypeError('symbol_to_number', []);
// 如果为null或
return (IS_NULL(x)) ? 0 : ToNumber(%DefaultNumber(x));
}

ToString

ToString是用于将变量转换为string类型,它在V8中的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ECMA-262, section 9.8, page 35.
function ToString(x) {
// 如果为string,则直接返回
if (IS_STRING(x)) return x;
// 如果为number,则调用_NumberToString
if (IS_NUMBER(x)) return %_NumberToString(x);
// 如果为boolean
if (IS_BOOLEAN(x)) return x ? 'true' : 'false';
// 如果为undefined,则返回undefined字符串
if (IS_UNDEFINED(x)) return 'undefined';
// 如果为symbol,则抛出异常
if (IS_SYMBOL(x)) throw %MakeTypeError('symbol_to_string', []);
// 如果为null,或者对象
return (IS_NULL(x)) ? 'null' : %ToString(%DefaultString(x));
}

ToBoolean

ToBoolean是用于将变量转换为boolean类型,它在V8中的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// ECMA-262, section 9.2, page 30
function ToBoolean(x) {
// 如果为boolean,则直接返回
if (IS_BOOLEAN(x)) return x;
// 如果为string,则当字符串长度不为0时返回true
if (IS_STRING(x)) return x.length != 0;
// 如果为null,返回false
if (x == null) return false;
// 如果为number,当number不为0,且不为NAN时返回true
if (IS_NUMBER(x)) return !((x == 0) || NUMBER_IS_NAN(x));
// 否则返回true
return true;
}

ToObject

ToObject是用于变量需要转换为对象时调用。在JavaScript内部实现了该函数,在需要将变量转换为对象时就会调用该函数,下面看一下它的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
// ECMA-262, section 9.9, page 36.
function ToObject(x) {
if (IS_STRING(x)) return new $String(x);
if (IS_NUMBER(x)) return new $Number(x);
if (IS_BOOLEAN(x)) return new $Boolean(x);
if (IS_SYMBOL(x)) return %NewSymbolWrapper(x);
// 如果为null或undefined,抛出异常
if (IS_NULL_OR_UNDEFINED(x) && !IS_UNDETECTABLE(x)) {
throw %MakeTypeError('undefined_or_null_to_object', []);
}
return x;
}

ADD

+运算时也会涉及到类型转换,例如有面试题:{} + 1new Date() + 1返回什么呢?这就要看看我们V8引擎内部是怎么对加法进行运算的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ECMA-262, section 11.6.1, page 50.
function ADD(x) {
// Fast case: Check for number operands and do the addition.
// 如果都为number或string,则直接调用NumberAdd或_StringAdd
if (IS_NUMBER(this) && IS_NUMBER(x)) return %NumberAdd(this, x);
if (IS_STRING(this) && IS_STRING(x)) return %_StringAdd(this, x);

// Default implementation.
// 否则将两边操作数分别转换为原始类型
var a = %ToPrimitive(this, NO_HINT);
var b = %ToPrimitive(x, NO_HINT);

if (IS_STRING(a)) {
return %_StringAdd(a, %ToString(b));
} else if (IS_STRING(b)) {
return %_StringAdd(%NonStringToString(a), b);
} else {
return %NumberAdd(%ToNumber(a), %ToNumber(b));
}
}

可以看到在操作数有一个不为number或string时,ADD操作就会将相应的操作数转换为原始类型,然后再进行相应的加法操作,可以看到上面的{} + 1{}不为原始类型,所以就会调用ToPrimitive({})ToPrimitive(1)ToPrimitive({})调用的结果为[object Object],所以最后会进行_StringAdd操作,最后的结果是:[object Object]1

Equals

相等操作也会在某些情况下进行相应的类型转换,所以可以看看它的内部实现是怎样的:

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
// ECMA-262 Section 11.9.3.
function EQUALS(y) {
if (IS_STRING(this) && IS_STRING(y)) return %StringEquals(this, y);
var x = this;

while (true) {
// 如果x为number
if (IS_NUMBER(x)) {
while (true) {
if (IS_NUMBER(y)) return %NumberEquals(x, y);
if (IS_NULL_OR_UNDEFINED(y)) return 1; // not equal
if (IS_SYMBOL(y)) return 1; // not equal
if (!IS_SPEC_OBJECT(y)) {
// String or boolean.
return %NumberEquals(x, %ToNumber(y));
}
y = %ToPrimitive(y, NO_HINT);
}
} else if (IS_STRING(x)) {
while (true) {
if (IS_STRING(y)) return %StringEquals(x, y);
if (IS_SYMBOL(y)) return 1; // not equal
if (IS_NUMBER(y)) return %NumberEquals(%ToNumber(x), y);
if (IS_BOOLEAN(y)) return %NumberEquals(%ToNumber(x), %ToNumber(y));
if (IS_NULL_OR_UNDEFINED(y)) return 1; // not equal
y = %ToPrimitive(y, NO_HINT);
}
} else if (IS_SYMBOL(x)) {
if (IS_SYMBOL(y)) return %_ObjectEquals(x, y) ? 0 : 1;
return 1; // not equal
} else if (IS_BOOLEAN(x)) {
if (IS_BOOLEAN(y)) return %_ObjectEquals(x, y) ? 0 : 1;
if (IS_NULL_OR_UNDEFINED(y)) return 1;
if (IS_NUMBER(y)) return %NumberEquals(%ToNumber(x), y);
if (IS_STRING(y)) return %NumberEquals(%ToNumber(x), %ToNumber(y));
if (IS_SYMBOL(y)) return 1; // not equal
// y is object.
x = %ToNumber(x);
y = %ToPrimitive(y, NO_HINT);
} else if (IS_NULL_OR_UNDEFINED(x)) {
return IS_NULL_OR_UNDEFINED(y) ? 0 : 1;
} else {
// x is an object.
if (IS_SPEC_OBJECT(y)) {
return %_ObjectEquals(x, y) ? 0 : 1;
}
if (IS_NULL_OR_UNDEFINED(y)) return 1; // not equal
if (IS_SYMBOL(y)) return 1; // not equal
if (IS_BOOLEAN(y)) y = %ToNumber(y);
x = %ToPrimitive(x, NO_HINT);
}
}
}

上面的代码逻辑大概是这样的:

  1. 如果有一个操作数为string,则执行StringEquals操作
  2. 否则,如果第一个操作数为number,如果另外一个操作数为原始类型(非null和undefined),则将其执行ToNumber,并进行NumberEquals,如果为nullundefined则返回false,如果为对象类型,则将该操作数执行ToPrimitive后再重复以上步骤
  3. 否则,如果第一个操作数为string,如果另外一个操作数也为string,则进行StringEquals,如果为number或boolean,则将第一个操作数执行ToNumber,再进行NumberEquals,如果为nullundefined则返回false,如果为对象类型,则将该操作数执行ToPrimitive后再重复以上步骤
  4. 否则如果第一个操作数为boolean,如果另外一个操作数也为boolean,则进行_ObjectEquals,否则如果为string或number,则进行NumberEquals,如果为nullundefined则返回false,如果为对象类型,则将该操作数执行ToPrimitive后再重复以上步骤
  5. 如果第一个操作数为对象类型,则将其ToPrimitive,在重复以上步骤

例如有个面试题是这样的[] == ![],是true还是false,我们首先看右边的![],空数组转换为boolean是true的,再进行!,可以知道右边的![]false,当一个对象和boolean进行equal时,[]会进行ToPrimitive,这里就会首先调用Array.prototype.valueOf,调用后返回的是[],不是原始类型,再进行Array.prototype.toString,这里返回了""空字符,空字符和false进行相等比较这里就是true了。

Compare

什么是Compare呢,就是> < <=这些操作都是comparecompare也会涉及到类型转换的操作,我们这里看内部compare是怎么实现的:

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
// ECMA-262, section 11.8.5, page 53. The 'ncr' parameter is used as
// the result when either (or both) the operands are NaN.
function COMPARE(x, ncr) {
var left;
var right;
// Fast cases for string, numbers and undefined compares.
if (IS_STRING(this)) {
if (IS_STRING(x)) return %_StringCompare(this, x);
if (IS_UNDEFINED(x)) return ncr;
left = this;
} else if (IS_NUMBER(this)) {
if (IS_NUMBER(x)) return %NumberCompare(this, x, ncr);
if (IS_UNDEFINED(x)) return ncr;
left = this;
} else if (IS_UNDEFINED(this)) {
if (!IS_UNDEFINED(x)) {
%ToPrimitive(x, NUMBER_HINT);
}
return ncr;
} else if (IS_UNDEFINED(x)) {
%ToPrimitive(this, NUMBER_HINT);
return ncr;
} else {
left = %ToPrimitive(this, NUMBER_HINT);
}

right = %ToPrimitive(x, NUMBER_HINT);
if (IS_STRING(left) && IS_STRING(right)) {
return %_StringCompare(left, right);
} else {
var left_number = %ToNumber(left);
var right_number = %ToNumber(right);
if (NUMBER_IS_NAN(left_number) || NUMBER_IS_NAN(right_number)) return ncr;
return %NumberCompare(left_number, right_number, ncr);
}
}

compare操作相对于equal还是相对来说简单一点的,它首先会判断第一个操作数,如果为对象则将其ToPrimitive,否则如果两个操作数都为string时,进行_StringCompare,否则,将两个操作数都ToNumber再进行NumberCompare

最近在学习数据结构,就试着用JavaScript来实现一些基本数据结构。本文介绍的主要是拓扑排序JavaScript实现。

拓扑排序是对有向无圈图(DAG)的一种排序,它使得如果存在一条从vi到vj的路径,那么在排序中vj必须出现在vi后面。

如果图是有圈的,那么拓扑排序是不可能的。因为对于两个圈上的两个顶点v和w,v先于w,w又同时先于v。

拓扑排序的实现原理:

  1. 从 DAG 图中选择一个 没有前驱(即入度为0)的顶点并输出
  2. 从图中删除该顶点和所有以它为起点的有向边
  3. 重复 1 和 2 直到当前的 DAG 图为空或当前图中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环。

enter image description here

以上得到拓扑排序后的结果是 { 1, 2, 4, 3, 5 }

下面是JavaScript的实现:

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
function EdgeNode (id) {
this.id = id;
this.afters = [];
this.indegree = 0;
}

function topsort (edges) {
var nodes = {};
var result = [];
var queue = [];

// build data structres
edges.forEach(function (edge) {
var fromEdge = edge[0];
var fromStr = fromEdge.toString();
var fromNode;

if (!(fromNode = nodes[fromStr])) {
fromNode = nodes[fromStr] = new EdgeNode(fromEdge);
}

edge.forEach(function (toEdge) {
// since from and to are in same array, we'll always see from again, so make sure we skip it..
if (toEdge == fromEdge) {
return;
}

var toEdgeStr = toEdge.toString();

if (!nodes[toEdgeStr]) {
nodes[toEdgeStr] = new EdgeNode(toEdge);
}
nodes[toEdgeStr].indegree++;
fromNode.afters.push(toEdge);
});
});

// topsort
var keys = Object.keys(nodes);
keys.forEach(function (key) {
if (nodes[key].indegree === 0) {
queue.push(key);
}
});
while (queue.length !== 0) {
let vertex = queue.shift();
result.push(nodes[vertex].id);

nodes[vertex].afters.forEach(function (after) {
var afterStr = after.toString();

nodes[afterStr].indegree--;
if (nodes[afterStr].indegree === 0) {
queue.push(afterStr);
}
});
}

return result;
}

var edges = [
['two', 'three'],
['four', 'six'],
['one', 'three'],
['two', 'four'],
['six', 'nine'],
['five', 'seven'],
['five', 'eight'],
['five', 'nine'],
['seven', 'eight'],
['eight', 'nine'],
['one', 'two'],
['four', 'five'],
['four', 'six'],
['three', 'six'],
['six', 'seven'],
['three', 'four']
];
var sorted = topsort(edges);
console.log(sorted);

以上参考链接:

本文主要是用JavaScript实现数据结构中的各种排序算法,例如:插入排序
、希尔排序、合并排序等。

插入排序

插入排序应该算是最简单和容易理解的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。具有n个元素时它需要经过n-1趟排序。对于p = 1p = n-1趟,插入排序保证从位置0到位置p上的元素为已排序状态。它就是基于这个事实来排序的。

enter image description here

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function insertSort (arr) {
var len = arr.length;

if (len <= 1) {
return arr;
}

// 1~n-1趟排序
for (var i = 1; i < len; i++) {
let tmp = arr[i];
for (var j = i; j > 0 && arr[j - 1] > tmp; j--) {
arr[j] = arr[j - 1];
}
arr[j] = tmp;
}

return arr;
}

var arr = [12, 290, 219, 278, 21, 43, 89, 78, 4432];
console.log(insertSort(arr));

如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数减去(n-1)次。平均来说插入排序算法复杂度为O(n2)。因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。

希尔排序

希尔排序的名称源自于它的发明者Donald Shell。它又称之为缩小增量排序。它其实是插入排序的一种更高效的版本。

希尔排序的原理是使用一个增量序列:h1、h2、…、hn。希尔排序的原理是在使用增量hk的一趟排序之后,对于每一个i我们有A[i] ≤ A[i + hk],所有相隔hk的元素都被排序。此时称文件为hk-排序。但是对于不同的增量序列,排序的性能也会有所影响。我们一般使hn = Math.floor(N / 2)和hk=Math.floor(hk-1 / 2)。算法实现如下:

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 shellSort (arr) {
var len = arr.length;

if (len <= 1) {
return arr;
}

// 增量
for (var increment = Math.floor(len / 2); increment > 0; increment = Math.floor(increment / 2)) {
for (var i = increment; i < len; i++) {
var tmp = arr[i];
for (var j = i; j >= increment; j -= increment) {
if (arr[j] < arr[j - increment]) {
arr[j] = arr[j - increment];
} else {
break;
}
}
arr[j] = tmp;
}
}
return arr;
}

var arr = [12, 290, 219, 278, 21, 43, 89, 78, 4432];
console.log(shellSort(arr));

堆排序

二叉堆我们可以利用数组来实现,要进行排序,我们第一步时执行建堆操作,然后我们再依次执行deleteMin操作就可以得到我们的排好序的元素。我们可以使用最小堆,每次deleteMin时向一个临时数组中插入元素。这种情况的问题是多了一倍的空间需求。为了避免使用第二个数组,在每次deleteMin之后,堆缩小1,位于堆中最后的单元就可以用于存放刚刚删除的元素。但这样元素会按递减的顺序排列。为了达到递增的效果,我们这里要使用最大堆。

下面是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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function leftChild (i) {
return 2 * i + 1;
}

function swap (arr, i, j) {
var tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}

function precDown (arr, i, len) {
var index = i;
var child;
var tmp;

for (tmp = arr[index]; leftChild(index) < len; index = child) {
child = leftChild(index);

if (child !== len - 1 && arr[child + 1] > arr[child]) {
child++;
}
if (tmp < arr[child]) {
arr[index] = arr[child];
} else {
break;
}
}
arr[index] = tmp;
}

function heapSort (arr) {
var len = arr.length;

// build max heap
for (let i = Math.floor(len / 2); i >= 0; i--) {
precDown(arr, i, len)
}
// delete max element
for (let i = len - 1; i > 0; i--) {
swap(arr, 0, i);
precDown(arr, 0, i);
}
return arr;
}

var arr = [1, 42, 53, 2432, 422, 5443, 89];
console.log(heapSort(arr));

堆排序的算法复杂度是O(NlogN)

归并排序

归并排序其实使用到了算法设计思想里面的分治法,分而治之。分治法是按照以下方案来工作的:

  1. 将问题的实例划分为同一问题的几个较小的实例,最好拥有同样的规模
  2. 对这些较小的实例求解
  3. 如果必要的话,合并这些较小问题的解,以得到原始问题的解

归并排序的思想就是将需要排序的数组A[0...n-1]一分为二,A[0...Math.floor(n/2)-1]A[Math.floor(n - 1)...n-1],并对每个子数组递归排序,然后将这两个排好序的子数组合并为一个有序数组。归并排序最主要的部分就是merge过程。下面看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
28
29
30
31
32
function merge (left, right) {
var result = [];
var leftIndex = 0;
var rightIndex = 0;

while (leftIndex < left.length && rightIndex < right.length) {
if (left[leftIndex] < right[rightIndex]) {
result.push(left[leftIndex++]);
} else {
result.push(right[rightIndex++]);
}
}

return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}

function mergeSort (arr) {
var len = arr.length;

if (len <= 1) {
return arr;
}

var middle = Math.floor(len / 2);
var left = arr.slice(0, middle);
var right = arr.slice(middle);

return merge(mergeSort(left), mergeSort(right));
}

var arr = [21, 53, 643, 654, 24, 892, 5338];
console.log(mergeSort(arr));

归并算法的运行时间为O(NlogN),但是它很难用于主存排序,主要问题在于合并两个排序的表需要线性附加内存,在整个算法中还要将数据拷贝到临时数组再拷贝回来这样一些附加的工作,其结果严重放慢了排序的速度。

快速排序

正如它的名字,快速排序是在时间中最快的已知排序算法,它的平均运行时间是O(NlogN)。快速排序也是一种分治的递归算法。将数组S排序的基本算法由下列简单的四步组成:

  1. 如果S中元素个数是0或1,则返回
  2. 取S中任一元素v,称之为枢纽元
  3. S - {v}分成两个不相交的集合:S1 = {x∈S - {v} | x ≤ v}和S2 = {x∈S - {v} | x ≥ v}
  4. 返回{quicksort(S1)},继续v,继而quicksort(S2)

由于对枢纽元的处理会导致第三步中的分割不唯一,因此,我们希望把等于枢纽元的大约一半的关键字分到S1中,而另外一半分到S2中,那怎么去选择一个好的枢纽元呢?

选取枢纽元

一种错误的方法

通常的,没有经过充分考虑的选择是将第一个元素用作枢纽元。如果输入是随机的,那么这是可以接受的,但是如果输入是预排序或是反序的,那么这样的枢纽元就会产生一个劣质的分割,因为所有的元素不是都被划入S1就是被划入S2

一种安全的作法

一种安全的方针是随机选取枢纽元。但是另一方面,随机数的生成一般是昂贵的,根本减少不了算法奇遇部分的平均运行时间。

三数中值分割法

一组N个数的中值是第Math.ceil(N/2)个最大的数。枢纽元的最好的选择是数组的中值。不幸的是,这很难算出,且会减慢快速排序的速度。因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。例如,输入为8, 1, 4, 9, 6, 3, 5, 2, 7, 0,它的左边元素是8,右边元素是0,中心位置为Math.floor((left + right) / 2)上的元素是6,于是枢纽元v=6

下面来看看具体实现了:

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
function swap (arr, i, j) {
var tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}

function median3 (arr, left, right) {
var center = Math.floor((left + right) / 2);

if (arr[left] > arr[center]) {
swap(arr, left, center);
}
if (arr[left] > arr[right]) {
swap(arr, left, right);
}
if (arr[center] > arr[right]) {
swap(arr, center, right);
}

swap(arr, center, right - 1);
return arr[right - 1];
}

function qSort (arr, left, right) {
// 枢纽元
var pivot = median3(arr, left, right);
var i = left;
var j = right - 1;

while (i < j) {
while (arr[++i] < pivot) {}
while (arr[--j] > pivot) {}
if (i < j) {
swap(arr, i, j);
} else {
break;
}
}
swap(arr, i, right - 1);
if (left < i - 1) {
qSort(arr, left, i - 1);
}
if (i + 1 < right) {
qSort(arr, i + 1, right);
}

return arr;
}

function quickSort (arr) {
return qSort(arr, 0, arr.length - 1);
}


var arr = [21, 53, 643, 654, 24, 892, 5338];
console.log(quickSort(arr));

本文内容来自于:https://leanpub.com/understandinges6/read#leanpub-auto-objectis。内容有删减,有不懂的地方可以看原文哟。

ES6对对象也进行了很多扩展,比如属性和方法的简写方式、Object.assign()等。下面来一一介绍ES6中对象的扩展。

属性和方法的简写

ES5或者更早时,对象直接量其实就是简单的名-值对集合,这意味着当属性值被初始化时可能会有一些重复。例如:

1
2
3
4
5
6
function createPerson(name, age) {
return {
name: name,
age: age
};
}

createPerson()创建了一个对象,这个对象的属性名称和函数的参数名称是一样的。其结果是导致了nameage的复制,尽管每个表示一个过程的不同方面。

ES6中,当属性名和局部变量名是一样时,我们可以省略它后面的冒号和值。例如,上面的例子可以重写为:

1
2
3
4
5
6
function createPerson(name, age) {
return {
name,
age
};
}

当一个对象直接量的一个属性只有名称没有值时,JavaScript引擎会在周围作用域中寻找和属性名有相同名称的变量。如果找到,值就被赋给对象直接量中相同名的属性。所以在这个例子中,对象直接量的属性name被赋值为局部变量name的值。

除了属性可以简写外,方法也是可以简写的。在ES5或之前,我们定义方法必须像下面这样:

1
2
3
4
5
6
var person = {
name: "Nicholas",
sayName: function() {
console.log(this.name);
}
};

但在ES6中,通过省略冒号和function关键字,使语法变得更加简洁。你可以重写之前的例子:

1
2
3
4
5
6
var person = {
name: "Nicholas",
sayName() {
console.log(this.name);
}
};

计算属性名

JavaScript中,我们定义属性时,有两种方式:中括号[].的方式:

1
2
3
4
5
// 方法一
obj.foo = true;

// 方法二
obj['a'+'bc'] = 123;

.运算符具有很大的局限性,比如first name这种属性只能通过中括号的方式来定义。中括号的方式允许我们使用变量或者在使用标识符时会导致语法错误的字符串直接量来定义属性。例如:

1
2
3
4
5
6
7
8
var person = {},
lastName = "last name";

person["first name"] = "Nicholas";
person[lastName] = "Zakas";

console.log(person["first name"]); // "Nicholas"
console.log(person[lastName]); // "Zakas"

这两种方式只能通过中括号的方式来定义的。在ES5中,你可以在对象直接量中使用字符串直接量作为属性,例如:

1
2
3
4
5
var person = {
"first name": "Nicholas"
};

console.log(person["first name"]); // "Nicholas"

但是当我们的属性名存在一个变量中或者需要计算时,使用对象直接量是无法定义属性的。但是在ES6中计算属性名语法,同样是通过中括号的方式。例如:

1
2
3
4
5
6
7
8
9
var lastName = "last name";

var person = {
"first name": "Nicholas",
[lastName]: "Zakas"
};

console.log(person["first name"]); // "Nicholas"
console.log(person[lastName]); // "Zakas"

在对象直接量中的中括号表明属性名是需要被计算的,它的内容被计算为字符串。

Object.assign()

对于对象构成最流行的模式之一可能是mixin,一个对象从另一个对象中接收属性和方法。许多JavaScript库都有一个类似下面的mixin方法:

1
2
3
4
5
6
7
function mixin(receiver, supplier) {
Object.keys(supplier).forEach(function(key) {
receiver[key] = supplier[key];
});

return receiver;
}

mixin()方法遍历supplier对象的自有属性,并将其拷贝到receiver。这就使得receiver没有通过继承就获得了新的行为。例如:

1
2
3
4
5
6
7
8
9
10
11
function EventTarget() { /*...*/ }
EventTarget.prototype = {
constructor: EventTarget,
emit: function() { /*...*/ },
on: function() { /*...*/ }
};

var myObject = {};
mixin(myObject, EventTarget.prototype);

myObject.emit("somethingChanged");

在这个例子中,myObjectEventTarget.prototype接收了新的行为。

为此在ES6中添加了Object.assign(),它和mixin()的行为一样。但不同之处在于,mixin()使用赋值运算符=来拷贝,它不能拷贝访问属性accessor properties到接受者作为访问属性。Object.assign()是可以做到这点的。

我们可以使用Object.assign()重写上面的代码:

1
2
3
4
5
6
7
8
9
10
11
function EventTarget() { /*...*/ }
EventTarget.prototype = {
constructor: EventTarget,
emit: function() { /*...*/ },
on: function() { /*...*/ }
}

var myObject = {}
Object.assign(myObject, EventTarget.prototype);

myObject.emit("somethingChanged");

Object.assign()可以接受任意多个提供属性的对象,接收者则按顺序从提供者接收属性,这可能会导致第二个提供者会覆盖第一个提供者提供给接收者的属性。例如:

1
2
3
4
5
6
7
8
9
10
11
12
var receiver = {};

Object.assign(receiver, {
type: "js",
name: "file.js"
}, {
type: "css"
}
);

console.log(receiver.type); // "css"
console.log(receiver.name); // "file.js"

下面再看看Object.assign()用于访问属性的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
var receiver = {},
supplier = {
get name() {
return "file.js"
}
};

Object.assign(receiver, supplier);

var descriptor = Object.getOwnPropertyDescriptor(receiver, "name");

console.log(descriptor.value); // "file.js"
console.log(descriptor.get); // undefined

重复对象字面量属性

ES5的严格模式中,引入了一个对重复对象字面量属性的检查,它会抛出一个错误如果发现了重复属性。例如:

1
2
3
4
var person = {
name: "Nicholas",
name: "Greg" // syntax error in ES5 strict mode
};

但是在ES6中,重复属性检查已经被移除了。不管是strictnostrict模式都不会取检查重复属性,它会取给定名称的最后一个属性作为实际值:

1
2
3
4
5
6
var person = {
name: "Nicholas",
name: "Greg" // not an error in ES6
};

console.log(person.name); // "Greg"

在这个例子中,person.name的值为Greg,因为它是赋给该属性的最后一个值。

改变原型

原型是JavaScript继承时的基础,因此,ES6使得原型更强大。ES5中添加了Object.getPrototypeOf()方法来检索任何给定对象的原型。在ES6中添加了相反操作的方法,Object.setPrototypeOf(),它允许我们改变任何给定对象的原型。

ES6之前,我们无法在对象创建后来改变其原型,ES6Object.setPrototypeOf()打破了这一情况。Object.setPrototypeOf()接收两个参数,第一个参数为要改变原型的对象,第二个参数为被设置为第一个对象的原型的对象。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let person = {
getGreeting() {
return "Hello";
}
};

let dog = {
getGreeting() {
return "Woof";
}
};

// prototype is person
let friend = Object.create(person);
console.log(friend.getGreeting()); // "Hello"
console.log(Object.getPrototypeOf(friend) === person); // true

// set prototype to dog
Object.setPrototypeOf(friend, dog);
console.log(friend.getGreeting()); // "Woof"
console.log(Object.getPrototypeOf(friend) === dog); // true

这段代码中,我们有两个基本对象:persondog,两个对象都有一个getGreeting()的方法,对象friend首先从person中继承,意味着调用getGreeting()会输出Hello。当我们改变friend的原型为dog时,此时getGreeting()输出Woof

一个对象的原型的实际值是存储在一个内部属性[[Prototype]]中。Object.getPrototypeOf()方法返回存储在[[Prototype]]的值,而Object.setPrototypeOf()改变存储在[[Prototype]]上的值。

super引用

ES6中我们可以通过super引用来调用原型上的方法。例如,如果你想覆盖一个对象实例上的方法,但它同样需要去调用相同名字的原型方法,在ES5中我们可以通过以下方式来实现:

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
let person = {
getGreeting() {
return "Hello";
}
};

let dog = {
getGreeting() {
return "Woof";
}
};

// prototype is person
let friend = {
__proto__: person,
getGreeting() {
// same as this.__proto__.getGreeting.call(this)
return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
}
};

console.log(friend.getGreeting()); // "Hello, hi!"
console.log(Object.getPrototypeOf(friend) === person); // true
console.log(friend.__proto__ === person); // true

// set prototype to dog
friend.__proto__ = dog;
console.log(friend.getGreeting()); // "Woof, hi!"
console.log(friend.__proto__ === dog); // true
console.log(Object.getPrototypeOf(friend) === dog); // true

我们可以看到是通过Object.getPrototypeOf(this).getGreeting.call(this)的方式来实现的,但是在ES6中,我们不用这么复杂,只需要super就行了,下面是重写后的代码:

1
2
3
4
5
6
7
8
let friend = {
__proto__: person,
getGreeting() {
// same as Object.getPrototypeOf(this).getGreeting.call(this)
// or this.__proto__.getGreeting.call(this)
return super.getGreeting() + ", hi!";
}
};

以上就是本文的全部内容啦。

本文翻译自:https://leanpub.com/understandinges6/read#leanpub-auto-objectis,内容有删减,仅作为阅读笔记。

ES6中的函数增加了很多新特性,使得在JavaScript中编程不容易出错,且比以往任何时候还要强大。ES6的函数主要带来了以下扩展:参数的默认值、rest参数、解构参数、扩展(Spread)运算符、name属性、箭头函数等。下面就一一介绍。

参数默认值

JavaScript中,我们可以给函数传入任意多个参数,而不需要管它实际定义的形参个数。这允许我们定义的函数可以处理参数不同的情况,我们可以为没有指定的参数默认值。在ES5中我们可以经常看到下面这样的代码:

1
2
3
4
5
6
7
8
function makeRequest(url, timeout, callback) {

timeout = timeout || 2000;
callback = callback || function() {};

// the rest of the function

}

在这个例子中,timeoutcallback都是可选的,因为在没有传相应的参数时,它们有默认值2000function() {}。我们经常用这种方式来实现默认参数。

ES6中对函数进行了扩展,添加了参数默认值,在没有传入相应参数时,会使用你在定义函数时给定的默认值,而不是像ES5中那样对每个参数还要做一次判断。在ES6中我们可以这样:

1
2
3
4
5
function makeRequest(url, timeout = 2000, callback = function() {}) {

// the rest of the function

}

这样只有我们的第一个参数是期望传入的,而其他两个参数都有默认值,这使得函数更加简洁,我们不需要去判断是否某个参数没有被传入,就像timeout = timeout || 2000;这样。当makeRequest()调用时传入了所有的三个参数时,默认参数就不会被使用了。例如:

1
2
3
4
5
6
7
8
9
10
// uses default timeout and callback
makeRequest("/foo");

// uses default callback
makeRequest("/foo", 500);

// doesn't use defaults
makeRequest("/foo", 500, function(body) {
doSomething(body);
});

任何参数有默认值时我们认为它是可选的,而那些没有默认值的参数我们认为是必须的参数。

Rest参数

上面也说到JavaScript中是可以传入任意多个参数,有时候我们没有必要指定所有的参数。在以前我们可以通过arguments函数的所有参数,虽然这在大多数情况下可以工作的很好,但是还是会有一点小累赘。例如:

1
2
3
4
5
6
7
8
9
10
11
12
function sum(first) {
let result = first,
i = 1,
len = arguments.length;

while (i < len) {
result += arguments[i];
i++;
}

return result;
}

上面这个函数将所有传入的参数相加,例如我们可以sum(1)或者sum(1, 2, 3, 4)都是可以的。但这个函数我们有几件事情要注意。第一、函数不能明显的看出可以处理一个以上的参数。第二、因为我们指定了一个first参数,那我们就必须从arguments的索引1开始,而不是索引0。当然记住正确的索引并不是很困难,但是还是需要我们关注的一件事。ES6为我们提供了rest参数来解决这个问题。

rest参数由三个.加上参数名字来表示,那个参数名字成为一个数组包含了参数的其余部分。例如,sum()函数可以使用rest参数重写:

1
2
3
4
5
6
7
8
9
10
11
12
function sum(first, ...numbers) {
let result = first,
i = 0,
len = numbers.length;

while (i < len) {
result += numbers[i];
i++;
}

return result;
}

在这个版本的函数中,我们可以看到numbers包含了除first参数之外的剩余参数。这意味着我们可以从0开始遍历numbers,而不需要有任何顾虑。我们也可以很容易的看出函数可以处理任意多个参数。

对于rest参数有一个限制就是,在rest参数后不能再跟其他函数,否则会语法错误。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Syntax error: Can't have a named parameter after rest parameters
function sum(first, ...numbers, last) {
let result = first,
i = 0,
len = numbers.length;

while (i < len) {
result += numbers[i];
i++;
}

return result;
}

参数解构

在前面我们学过变量解构,解构同样也可以使用在函数的参数中。

通常我们会使用一个options对象作为一个参数来代替传入多个参数,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function setCookie(name, value, options) {

options = options || {};

var secure = options.secure,
path = options.path,
domain = options.domain,
expires = options.expires;

// ...
}

setCookie("type", "js", {
secure: true,
expires: 60000
});

除了namevalue是必须的之外,其他数据没有顺序优先级,这里我们使用了一个options对象来代替,而不是额外的命名参数。这种方式是OK的,但是给我们的函数带来了不透明性。

使用参数解构,之前的函数可以被重写为:

1
2
3
4
5
6
7
8
9
function setCookie(name, value, { secure, path, domain, expires }) {

// ...
}

setCookie("type", "js", {
secure: true,
expires: 60000
});

这个例子和前面的不同之处在于使用了参数解构取出了必要的数据。这样使得更清楚的知道需要什么样的参数。参数解构和变量解构一样,如果没有传入相应数据时,其值为undefined

有一点需要注意的是,当我们在上面的函数中没有传入第三个参数时,就像这样:

1
2
// Error!
setCookie("type", "js");

这个代码是会报错的,在内部执行时其实是像下面这样:

1
2
3
4
5
6
function setCookie(name, value, options) {

var { secure, path, domain, expires } = options;

// ...
}

因为变量解构时,当右边的表达式为nullundefined时是会报错的。不过我们可以通过传入一个默认的空对象来解决这个问题:

1
2
3
4
function setCookie(name, value, { secure, path, domain, expires } = {}) {

// ...
}

扩展运算符

扩展运算符和rest参数正好相反,rest参数允许我们将多个独立的参数合并成一个数组,而扩展运算符允许我们将一个数组分割,每个元素作为独立参数传入到函数中。我们可以考虑Math.max()方法,我们可以传入任意多个参数,然后返回最大的那个参数值。例如:

1
2
3
4
let value1 = 25,
value2 = 50;

console.log(Math.max(value1, value2)); // 50

当只有几个参数时我们很好处理,但是当我们的值存在一个数组中时我们应该怎么办呢?在ES5中我们可以通过apply来操作:

1
2
3
let values = [25, 50, 75, 100]

console.log(Math.max.apply(Math, values)); // 100

ES6的扩展运算符使这种情况变得更加简单。你可以通过在数组名前面加上...传入函数中,就像rest参数那样。例如:

1
2
3
4
5
let values = [25, 50, 75, 100]

// equivalent to
// console.log(Math.max(25, 50, 75, 100));
console.log(Math.max(...values)); // 100

你也可以混合使用扩展运算符和其他参数。例如:

1
2
3
let values = [-25, -50, -75, -100]

console.log(Math.max(...values, 0)); // 0

name属性

ES6为所有函数添加了一个name属性,在ES6的程序中所有的函数的name属性都确保有一个合适的值。例如:

1
2
3
4
5
6
7
8
9
10
function doSomething() {
// ...
}

var doAnotherThing = function() {
// ...
};

console.log(doSomething.name); // "doSomething"
console.log(doAnotherThing.name); // "doAnotherThing"

在这段代码中,doSomething()函数的name属性值为doSomething,因为他是一个函数声明。在匿名函数表达式中,doAnotherThing()name属性值为doAnotherThing

下面看个更详细的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var doSomething = function doSomethingElse() {
// ...
};

var person = {
get firstName() {
return "Nicholas"
},
sayName: function() {
console.log(this.name);
}
}

console.log(doSomething.name); // "doSomethingElse"
console.log(person.sayName.name); // "sayName"
console.log(person.firstName.name); // "get firstName"

对于函数名还有几种特殊情况。对于使用bind()创建的函数会在它们的函数名前加上bound前缀。对于使用Function构造函数创建的函数它的名字为anonymous。例如:

1
2
3
4
5
6
7
var doSomething = function() {
// ...
};

console.log(doSomething.bind().name); // "bound doSomething"

console.log((new Function()).name); // "anonymous"

new.target[[Call]][[Construct]]

ES5甚至更早,函数服务于双重目的,通过new来调用或没有new。当使用new时,函数内的this值是返回的新对象。例如:

1
2
3
4
5
6
7
8
9
function Person(name) {
this.name = name;
}

var person = new Person("Nicholas");
var notAPerson = Person("Nicholas");

console.log(person); // "[Object object]"
console.log(notAPerson); // "undefined"

当没有使用new调用Person()时,返回undefined。这里很明显的是这段代码的意图是使用Personnew来创建一个对象。在ES6中,在函数双重角色的困惑上做了一些改变。

第一、规范定义了两个不同的仅在内部使用的方法,每个函数都有:[[Call]][[Construct]]。当一个函数没有通过new来调用时,[[Call]]方法会被执行。当一个函数通过new来调用时,[[Construct]]被调用。[[Construct]]方法有责任创建一个新的对象。被称之为new target,然后执行函数体,this的值被设置为new target。有一个[[Construct]]方法的函数被称之为构造函数

要注意的是不是所有的函数都有[[Construct]],也不是所有的函数能通过new来调用。箭头函数,将在后面介绍到,没有[[Construct]]方法。

ES5中,最流行的方式来决定一个函数能否通过new来调用是使用instanceof操作符。例如:

1
2
3
4
5
6
7
8
9
10
function Person(name) {
if (this instanceof Person) {
this.name = name; // using new
} else {
throw new Error("You must use new with Person.")
}
}

var person = new Person("Nicholas");
var notAPerson = Person("Nicholas"); // throws error

在这里,this的值会被检查是否为构造函数的实例,如果是的话,它继续正常执行。如果不是,就会抛出一个异常。这个能工作是因为[[Construct]]方法创建了Person的一个新实例,并将它赋给了this。不幸的是,这种方式不是完全可信的,this的值可以不通过new的方式也可以为Person的实例,例如:

1
2
3
4
5
6
7
8
9
10
function Person(name) {
if (this instanceof Person) {
this.name = name; // using new
} else {
throw new Error("You must use new with Person.")
}
}

var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // works!

我们这里通过Person.call(),并且传递person对象作为第一个参数,这就使得无法区分this是通过new创建的还是其他方式。

为了解决这个问题,ES6引入了new.target元属性。当一个函数的[[Construct]]被调用,new.target会成为新创建对象的实例,这个值也会在函数内成为this的值。如果[[Call]]被执行,new.targetundefined。这意味着我们现在可以通过检查new.target是否被定义安全的检查函数是否是通过new的方式被调用。例如:

1
2
3
4
5
6
7
8
9
10
function Person(name) {
if (typeof new.target !== "undefined") {
this.name = name; // using new
} else {
throw new Error("You must use new with Person.")
}
}

var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // error!

箭头函数

ES6中新添加了一个箭头函数,就如它的名字,函数通过一种新的语法,使用箭头=>来定义。然而,箭头函数在有些方面和传统的JavaScript函数是不同的:

  • 词法的this绑定:在该函数内的this是通过定义箭头函数的地方决定,而不是使用它的地方决定。
  • 不能当作构造函数:箭头函数没有[[Construct]]方法,因此它不能作为构造函数。当箭头函数与new一起使用时将会抛出异常。
  • 不可以改变this值:在函数内的this的值是不能改变的,它在函数的整个生命周期中保持相同的值。
  • 没有arguments对象:你不能通过arguments对象来获取参数,你必须使用命名参数或者rest参数等。

其中有一些原因来表明这些不同为什么存在。首先,this的绑定在JavaScript是一个常见的错误根源。在一个函数内对this值的跟踪是非常容易丢失的,这可能会导致意想不到的后果。第二,通过限制箭头函数在执行代码时只有单一的this值,JavaScript引擎能更好的优化操作。

语法

箭头函数的语法有多种风格,这取决于你试图完成什么。所有的变种(箭头函数的多种风格)都是以函数参数开始,跟着箭头,跟着函数体。参数和函数体取决于使用可以采取不同的形式。例如,下面的箭头函数接受一个单一参数和简单的返回它:

1
2
3
4
5
6
7
var reflect = value => value;

// effectively equivalent to:

var reflect = function(value) {
return value;
};

当箭头函数只有一个参数时,我们只需直接使用这个参数而不需要其他的语法。然后箭头函数的右边会被计算和返回。即使我们这没有一个明确的return声明,箭头函数会返回传入的第一个参数。

如果你传入的参数个数大于1时,你就需要圆括号将参数用括号扩起来啦。例如:

1
2
3
4
5
6
7
var sum = (num1, num2) => num1 + num2;

// effectively equivalent to:

var sum = function(num1, num2) {
return num1 + num2;
};

sum()函数将两个参数相加并返回结果。不同之处在于我们的参数放在圆括号内,且用,分隔。

需要注意的是,当我们的箭头函数没有参数时,我们必须包含一个空圆括号。例如:

1
2
3
4
5
6
7
var getName = () => "Nicholas";

// effectively equivalent to:

var getName = function() {
return "Nicholas";
};

当你需要提供一个更传统的函数体时,可能包含多个表达式,那么我们可以将这些语句放在花括号{}内,并且定义一个明确的返回值。例如:

1
2
3
4
5
6
7
8
9
var sum = (num1, num2) => {
return num1 + num2;
};

// effectively equivalent to:

var sum = function(num1, num2) {
return num1 + num2;
};

当你需要创建一个什么也不做的函数时,我们需要包括花括号,例如:

1
2
3
4
5
var doNothing = () => {};

// effectively equivalent to:

var doNothing = function() {};

还有一点需要注意的是,因为花括号被用于包含函数体,当我们需要从箭头函数返回一个对象直接量时,我们需要将直接量放在圆括号内。例如:

1
2
3
4
5
6
7
8
9
10
11
var getTempItem = id => ({ id: id, name: "Temp" });

// effectively equivalent to:

var getTempItem = function(id) {

return {
id: id,
name: "Temp"
};
};

词法的this绑定

JavaScript中最常见出错的地方就是this在函数内的绑定。因为this的值可以根据调用它的上下文在单个函数内改变,这就可能错误的使用影响某个对象,但你的意图是另外一个对象。考虑以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var PageHandler = {

id: "123456",

init: function() {
document.addEventListener("click", function(event) {
this.doSomething(event.type); // error
}, false);
},

doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};

在这段代码中,我们知道this.doSomething中的this对象其实是指向document的,但我们的本意其实是PageHandler对象。如果你试图运行代码,会得到一个错误。你可能会使用bindvar me = this的方式来解决这个问题。例如:

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
// bind way
var PageHandler = {

id: "123456",

init: function() {
document.addEventListener("click", (function(event) {
this.doSomething(event.type); // no error
}).bind(this), false);
},

doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};

// var me = this;
var PageHandler = {

id: "123456",

init: function() {
var me = this;
document.addEventListener("click", function(event) {
me.doSomething(event.type); // no error
}, false);
},

doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};

现在代码如你预期的那样执行了,但是我们总会觉得有些奇怪。通过调用bind(this),你实际上是创建了一个新的函数,新函数的this被绑定到了PageHandler

但是在箭头函数内,它具有隐式this绑定,这意味着箭头函数内的this值总是与定义箭头函数的作用域的this具有相同的值。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
var PageHandler = {

id: "123456",

init: function() {
document.addEventListener("click",
event => this.doSomething(event.type), false);
},

doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};

现在this的值和init()内的this值是一样的,它就能用bind的方式实现一样的功能了。

箭头函数被设计为“一次性“函数,所以它不能用于定义新的类型。它和普通的函数不同,它没有prototype属性。如果你尝试在箭头函数上使用new操作符,就会报错。例如:

1
2
var MyType = () => {},
object = new MyType(); // error - you can't use arrow functions with 'new'

因为this的值是静态绑定到箭头函数上,你不能通过apply()call()bind()的方式来改变this的值。

词法arguments绑定

尽管箭头函数自身没有arguments对象,但是它能获取到包含它的函数的arguments对象。例如:

1
2
3
4
5
6
7
function createArrowFunctionReturningFirstArg() {
return () => arguments[0];
}

var arrowFunction = createArrowFunctionReturningFirstArg(5);

console.log(arrowFunction());

识别箭头函数

尽管语法不同,箭头函数同样也是函数,它也可以被识别:

1
2
3
4
var comparator = (a, b) => a - b;

console.log(typeof comparator); // "function"
console.log(comparator instanceof Function); // true

至此,就是本文的全部内容啦。

ES6中对字符串进行了很多扩展,包括加强了Unicode的支持、

更好的Unicode支持

ES6之前,JavaScript的字符串是完全基于16位字符编码的想法。所有字符串的属性和方法,例如:lengthcharAt(),都是基于16位序列代表单个字符的想法。ES5还允许Javascript引擎决定使用哪种编码方式,UCS-2UTF-16

对于Unicode的既定目标,保持在16位是不可能的为全世界的每一个字符提供一个全局唯一的标识符。这些全局唯一标志服被称之为code point,且从0开始。一个字符串编码有责任编码一个code pointcode unit,且保持内部一致。

第一个2^16code pointUTF-16中被表示为单一的16位code unit。这被称之为Basic Multilingual Plane (BMP)。超出该范围的被认为是在一个辅助平面,code point已不能仅仅用16位来表示了。UTF-16通过引入surrogate pairs(代理对)来解决这个问题,一个code point可以由两个16位的code unit来表示。这意味着字符串中的任何单个字符可以由一个code unit或两个来表示。

ES5中所有的操作都是基于16位的code unit,这意味着你在处理包含代理对的字符串时会出现意想不到的结果。例如:

1
2
3
4
5
6
7
8
var text = "𠮷";

console.log(text.length); // 2
console.log(/^.$/.test(text)); // false
console.log(text.charAt(0)); // ""
console.log(text.charAt(1)); // ""
console.log(text.charCodeAt(0)); // 55362
console.log(text.charCodeAt(1)); // 57271

在这个例子中,单个Unicode字符是使用代理对来表示的,JavaScript字符操作会认为字符串由两个16位的字符组成。这意味着length为2,通过正则表达式匹配单个字符也失败了,charAt()也无法读取字符。charCodeAt()方法为每个code unit返回了正确的16为数字。

ES6中强制使用UTF-16来编码字符串。

codePointAt()

codePointAt()接受code unit的位置,并返回一个整数:

1
2
3
4
5
6
7
8
9
var text = "𠮷a";

console.log(text.charCodeAt(0)); // 55362
console.log(text.charCodeAt(1)); // 57271
console.log(text.charCodeAt(2)); // 97

console.log(text.codePointAt(0)); // 134071
console.log(text.codePointAt(1)); // 57271
console.log(text.codePointAt(2)); // 97

当字符为BMP范围内的字符时,codePointAt()charCodeAt()的行为是一样的,在非BMP字符时,charCodeAt()仅仅返回了位置0code unit,但是codePointAt()返回了整个code point,即使横跨多个code unit。对于位置1和位置2的,它俩返回值是相同的。

用以下方法可以判断是否为16位,还是32位:

1
2
3
4
5
6
function is32Bit(c) {
return c.codePointAt(0) > 0xFFFF;
}

console.log(is32Bit("𠮷")); // true
console.log(is32Bit("a")); // false

String.fromCodePoint()

String.fromCodePoint()就是和codePointAt()做相反的操作了。例如:

1
console.log(String.fromCodePoint(134071));  // "𠮷"

编码非BMP字符

ES5中允许字符包含用编码序列代表的16位Unicode字符。编码序列是通过\u加上4个16进制值,例如\u0061代表字符a

1
console.log("\u0061");      // "a"

但是当你用编码序列来代表大于FFFF的字符时,你会得到意想不到的结果:

1
console.log("\u20BB7");

因为Unicode编码序列总是包含4个16进制字符,ECMAScript计算\u20BB7为两个字符:\u20BB7。第一个字符为非打印的,第二个为数字7。

ES6通过引入扩展的Unicode编码序列解决了这个问题,将16进制数字包含在花括号内。这使得任何数量的十六进制字符可以指定为单个字符:

1
console.log("\u{20BB7}");     // "𠮷"

通过下面的函数可以判断是否支持这种写法:

1
2
3
4
5
6
7
8
function supportsExtendedEscape() {
try {
eval("'\\u{00FF1}'");
return true;
} catch (ex) {
return false;
}
}

正则表达式u修饰符

正则表达式也是基于16位的code unit来代表单个字符,这就对于𠮷/^.$/的正则表达式中不能匹配,ES6为正则表达式定义了一个新的修饰符,u也即Unicode。当一个正则表达式设置了u修饰符时,它将切换模式为工作在字符串,而不是code unit。这意味着正则表达式在包含代理对的字符串中不会迷惑。例如:

1
2
3
4
5
var text = "𠮷";

console.log(text.length); // 2
console.log(/^.$/.test(text)); // false
console.log(/^.$/u.test(text)); // true

在添加了u修饰符的正则表达式能以字符的方式来匹配字符串,而不是code unit

新增的String方法

includes()

如果给定字符在字符串的任何位置被找到,则返回true,否则返回false。例如:

1
2
3
var msg = "Hello world!";

console.log(msg.includes("ello"));

startsWith()

如果字符串以给定字符开始,则返回true,否则返回false。例如:

1
2
3
var msg = "Hello world!";

console.log(msg.startsWith("Hello"));

endsWith()

如果字符串以给定字符结束,则返回true,否则返回false。例如:

1
2
3
var msg = "Hello world!";

console.log(msg.endsWith("world!"));

repeat()

该方法接受一个数字作为参数,表示将原字符串重复的次数。例如:

1
2
var str = "cookfront";
console.log(str.repeat(3));

正则表达式修饰符y

ES6引入了新的正则表达式修饰符y,也即粘连,它与g修饰符比较类似,但是不同之处在于,g修饰符只确保剩余位置中存在匹配,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是粘连的涵义。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var text = "hello1 hello2 hello3",
pattern = /hello\d\s?/,
result = pattern.exec(text),
globalPattern = /hello\d\s?/g,
globalResult = globalPattern.exec(text),
stickyPattern = /hello\d\s?/y,
stickyResult = stickyPattern.exec(text);

console.log(result[0]); // "hello1 "
console.log(globalResult[0]); // "hello1 "
console.log(stickyResult[0]); // "hello1 "

pattern.lastIndex = 1;
globalPattern.lastIndex = 1;
stickyPattern.lastIndex = 1;

result = pattern.exec(text);
globalResult = globalPattern.exec(text);
stickyResult = stickyPattern.exec(text);

console.log(result[0]); // "hello1 "
console.log(globalResult[0]); // "hello2 "
console.log(stickyResult[0]); // Error! stickyResult is null

在上面的例子中,使用了三个正则表达式,一个使用了y修饰符,一个使用了g修饰符,一个没有使用修饰符。第一次匹配时我们可以看到结果都为hello1,匹配后我们将lastIndex置为1,即从第二个字符开始匹配,我们可以看到第二次匹配的结果没有修饰符的还是匹配hello1g修饰符的匹配hello2y修饰符在从第二个字符开始没有匹配任何东西所以返回null。我们也可以看出了y修饰符号隐含了头部匹配的标志^

ES5时,我们经常会在代码中看到类似下面这样的代码:

1
2
3
4
function (options) {
var a = options.a;
var b = options.b;
}

有了变量解构,我们就不用这么麻烦了,用一行代码就能实现,特别是在有多个变量时,这样写还是很烦的。用变量结构我们可以像下面这样:

1
2
3
function (options) {
var {a, b} = options;
}

变量解构包括对象解构数组解构,下面一一讲解。

对象解构

对象解构的语法是使用一个对象直接量在赋值操作符的左边,例如:

1
2
3
4
5
6
7
8
9
var options = {
a: 'a',
b: 'b'
};

var {a: localA, b: localB} = options;

console.log(localA);
console.log(localB);

上面的代码中,options.a的值会存储在变量localA中,options.b会存储在变量localB中。在对象解构时,左边的对象直接量中,key对应了需要在对象中解构的属性,value则为存储属性值的变量名。

如果你想使用对象中相同的属性名,你可以省略左边对象直接量中的冒号和后面的值,例如,对于上面的例子,你可以这样:

1
2
3
4
5
6
7
8
9
var options = {
a: 'a',
b: 'b'
};

var {a, b} = options;

console.log(a);
console.log(b);

当给定的属性名不存在对象中时,局部变量会得到undefined的值,例如:

1
2
3
4
5
6
7
8
9
10
var options = {
a: 'a',
b: 'b'
};

var {a, b, c} = options;

console.log(a);
console.log(b);
console.log(c); // undefined

对象解构也是可以嵌套的,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
var options = {
a: 'a',
b: 'b',
c: {
d: 'd'
}
};

var {a, b, c: {d}} = options;

console.log(a);
console.log(b);
console.log(d);

数组解构

数组解构和对象解构差不多,是使用一个数组直接量在赋值操作符的左边,例如:

1
2
3
4
5
6
7
8
var arr = ['a', 'b', 'c'];

var [a, b, c] = arr;


console.log(a);
console.log(b);
console.log(c);

要注意的是,数组解构时要按照元素在数组中的顺序。还有一点是数组解构不会修改原数组。

数组解构也是可以嵌套解构的,例如:

1
2
3
4
5
6
7
8
var colors = [ "red", [ "green", "lightgreen" ], "blue" ];

// later

var [ firstColor, [ secondColor ] ] = colors;

console.log(firstColor); // "red"
console.log(secondColor); // "green"

以上就是变量结构的内容了。

参考链接:

  1. https://leanpub.com/understandinges6/read
  2. http://es6.ruanyifeng.com/#docs/destructuring

今天学习的比较简单,主要学习ES6letconst

我们知道在ES6之前,是没有块级作用域这一说的,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getValue(condition) {

if (condition) {
var value = "blue";

// other code

return value;
} else {

// value exists here with a value of undefined

return null;
}

// value exists here with a value of undefined
}

你可能会觉得在else里面无法访问到value变量,其实在js内部会造成变量提升,这意味着我们可以在else里面访问到value变量,只是它未初始化,所以其变量值为undefined。实际解析时代码可能像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getValue(condition) {

var value;

if (condition) {
value = "blue";

// other code

return value;
} else {

return null;
}
}

letconst为我们带来了块级作用域,这意味着它只能在代码块内才能访问到,出了代码块就会抛出异常了,还有一点重要的是,letconst不会造成变量提升

let

let定义变量时和var有两个区别:块级作用域、不会变量提升和不能定义在块中已有标识符同名的变量。

我们用let来重新定义上面的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getValue(condition) {

if (condition) {
let value = "blue";

// other code

return value;
} else {

// value doesn't exist here

return null;
}

// value doesn't exist here
}

当用let定义value时,我们只能在if里面才能访问到value了,value变量也不会变量提升,从而我们在else里面不能访问到value

let最常用的场景应该是for循环了:

1
2
for (let i = 0; i < len; i++) {
}

块级作用域

块级作用域就不用多说,就是用let定义的变量只在定义它的块中有效,出了这个块你就不能访问到它了。

变量提升

变量提升应该是在面试的时候会经常考到,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function test () {
console.log(value);
var value = 'something';
console.log(value);
}

test();

function test2 () {
console.log(fn);

function fn () {

}

console.log(fn);
}

test2();

我们用let重新定义上面的test()函数:

1
2
3
4
5
6
7
function test () {
console.log(value);
let value = 'something';
console.log(value);
}

test();

此时浏览器就会抱怨了,在let定义前是无法访问到我们的变量的。

同名变量

var定义变量时,我们可以多次对它进行定义,例如:

1
2
3
var a = 1;
var a = 2;
var a = 3;

这样的代码是不会报错的,在let定义的相同块中定义同名变量时就会报错了,例如:

1
2
3
4
5
6
let a = 1;
let a = 2;

// or
var a = 1;
let a = 2;

要注意的是要与let定义时在相同的块中,下面的代码是不会出错的:

1
2
3
4
var a = 1;
if (something) {
let a = 2;
}

const

const除了具有let的块级作用域和不会变量提升外,还有就是它定义的是常量,在用const定义变量后,我们就不能修改它了,对变量的修改会默默的失败(在iojs中会抛出异常,在Chrome下会默默的失败)。例如:

1
2
3
4
5
6
7
const PI = 3.1415;

console.log(PI);

PI = 22;

console.log(PI);

参考链接:Understanding ECMAScript 6

NSURLSessioniOS7中新的网络接口,负责处理数据的加载以及文件和数据在客户端与服务端之间的上传和下载。本文将从以下几个方面介绍:

  • NSURLSessionConfiguration:在使用NSURLSession首先要创建一个NSURLSessionConfiguration来配置我们的NSURLSession
  • NSURLSession
  • NSURLSessionTask:
    • NSURLSessionDataTask:处理一般的 NSData 数据对象,比如通过GET或POST方式从服务器获取JSON或XML返回等等,但不支持后台获取
    • NSURLSessionUploadTask:用于上传文件,支持后台上传
    • NSURLSessionDownloadTask:用于下载文件,支持后台下载

NSURLSessionConfiguration

一个NSURLSessionConfiguration对象定义了当使用NSURLSession对象上传和下载数据时的行为和使用策略。当你需要上传和下载数据时,创建一个配置对象总是你必须采取的第一步。从指定可用网络,到 cookie,安全性,缓存策略,再到使用自定义协议,启动事件的设置,以及用于移动设备优化的几个新属性,你会发现使用 NSURLSessionConfiguration可以找到几乎任何你想要进行配置的选项。

NSURLSession在初始化时会把配置它的NSURLSessionConfiguration对象进行一次 copy,并保存到自己的configuration属性中,而且这个属性是只读的。因此之后再修改最初配置session的那个configuration对象对于session是没有影响的。也就是说,configuration只在初始化时被读取一次,之后都是不会变化的。

NSURLSessionConfiguration提供了三个工厂方法来创建我们的Session Configuration对象:

  • + defaultSessionConfiguration:该方法返回创建的一个默认Session Configuration对象。默认的Session Configuration会使用磁盘来缓存数据并在用户的keychain中存储凭证。它同样会存储cookie
  • + ephemeralSessionConfiguration:返回一个session configuration,且不会使用缓存,cookie和凭证。使用ephemeral sessions主要的优点就是隐私。因此,它可以用于实现像秘密浏览这种功能。
  • + backgroundSessionConfigurationWithIdentifier::返回一个后台的session configuration。后台session不同于常规的,普通的session,它甚至可以在应用程序挂起,退出或者崩溃的情况下运行上传和下载任务。初始化时指定的标识符,被用于向任何可能在进程外恢复后台传输的守护进程(daemon)提供上下文。

对于NSURLSessionConfiguration的属性配置,可以看这篇文章:http://objccn.io/issue-5-4/

NSURLSession

创建好了NSURLSessionConfiguration对象,我们就可以使用它来创建我们的NSURLSession对象了。NSURLSession提供了三个工厂方法来创建我们的session对象:

  • + sessionWithConfiguration::使用指定的session configuration来创建一个session,且会创建一个序列的NSOperationQueue对象来处理所有的委托方法和完成处理程序的调用
  • + sessionWithConfiguration:delegate:delegateQueue::使用指定的session configurationdelegateoperation queue.来创建session。这个方法可以更细粒度的创建session,可以设定回调的delegate(注意这个回调delegate会被强引用),并且可以设定delegate在哪个OperationQueue回调,如果我们将其设置为[NSOperationQueue mainQueue]就能在主线程进行回调非常的方便。
  • + sharedSession:返回一个共享的单例session对象

NSURLSessionTask

通过上面创建的session,我们就可以安排三种类型的任务:检索数据到存储器的数据任务、下载文件到硬盘的下载任务和从硬盘上传文件的上传任务。

NSURLSessionTask是一个抽象类,其下有 3 个实体子类可以直接使用:NSURLSessionDataTaskNSURLSessionUploadTaskNSURLSessionDownloadTask。这 3 个子类封装了现代程序三个最基本的网络任务:获取数据,比如 JSON 或者 XML,上传文件和下载文件。

所有的task都是可以取消,暂停或者恢复的。当一个 download task 取消时,可以通过选项来创建一个恢复数据(resume data),然后可以传递给下一次新创建的download task,以便继续之前的下载。

这里的task不同于其他的alloc-init初始化方法,它是需要通过session来创建的,NSURLSession提供了多个方法来创建task

Data Task:

  • - dataTaskWithURL:
  • - dataTaskWithURL:completionHandler:
  • - dataTaskWithRequest:
  • - dataTaskWithRequest:completionHandler:

实例:

1
2
3
4
5
6
7
8
9
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURL *url = [NSURL URLWithString:@"http://demo.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];

NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// do something
}];
[dataTask resume];

Download Task:

  • - downloadTaskWithURL:
  • - downloadTaskWithURL:completionHandler:
  • - downloadTaskWithRequest:
  • - downloadTaskWithRequest:completionHandler:
  • - downloadTaskWithResumeData:
  • - downloadTaskWithResumeData:completionHandler:

实例:

1
2
3
4
5
6
7
8
9
10
11
12
NSURL *URL = [NSURL URLWithString:@"http://example.com/file.zip"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSURL *documentsDirectoryURL = [NSURL fileURLWithPath:documentsPath];
NSURL *newFileLocation = [documentsDirectoryURL URLByAppendingPathComponent:[[response URL] lastPathComponent]];
[[NSFileManager defaultManager] copyItemAtURL:location toURL:newFileLocation error:nil];
}];

[downloadTask resume];

Upload Task:

  • - uploadTaskWithRequest:fromData:
  • - uploadTaskWithRequest:fromData:completionHandler:
  • - uploadTaskWithRequest:fromFile:
  • - uploadTaskWithRequest:fromFile:completionHandler:
  • - uploadTaskWithStreamedRequest:

实例:

1
2
3
4
5
6
7
8
9
10
NSURL *URL = [NSURL URLWithString:@"http://example.com/upload"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
NSData *data = ...;

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request fromData:data completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
// ...
}];
[uploadTask resume];

以上就是本文的全部内容啦。如有不对还请指出。

本文主要翻译自:View Programming Guide for iOS,内容有删减。

UIView类在屏幕上定义了一个矩形区域和一些在那个区域处理内容的接口。在运行时,一个view对象处理那个区域任何内容的渲染,还处理与这些内容的任何交互。UIView类它自己提供了用一个背景颜色填充它的矩形区域的基本行为。更复杂的内容可以通过继承UIView来呈现,并自身实现必要的绘制和事件处理代码。UIKit框架还包括一组标准的子类,从简单的按钮到复杂的表格可以使用。例如,一个UILabel对象绘制一个文本字符串,一个UIImageView对象绘制一张图片。

因为view对象是你的应用与用户交互的主要方式,所以它们具有许多职责。这里仅仅是几个:

  • 绘制和动画
    • 视图使用例如UIKitCore GraphicsOpenGL ES的技术在它们的矩形区域绘制内容
    • 一些视图属性可以动画到新的值
  • 布局和子视图管理
    • 一个视图可能包含0个或多个子视图
    • 每一个视图定义了相对于它的父视图的它们自己默认的尺寸调整行为
    • 视图可以根据需要限定的它的子视图的尺寸和位置
  • 事件处理
    • 视图是一个应答器,并能处理触摸事件以及由UIResponder类定义的其它事件
    • 视图可以使用addGestureRecognizer:方法安装手势识别以处理常见的手势

视图可以嵌入其他视图,并创建复杂的视觉层次。这在被嵌入的视图(被称为subview)和父视图做嵌入(被称为superview)之间创建了一种父-子关系。通常情况下,一个subview的可见区域不会在它的superview的边界被剪切,但是在iOS中你可以使用clipsToBounds来更新这种行为。一个父视图可以包含任意多个subview,但每个subview只有一个superview,该superview负责适当定位其子视图。

视图绘制周期

UIView类使用一个按需绘制的模式来呈现内容。当一个视图第一次出现在屏幕上时,系统要求它绘制它的内容。系统捕捉内容的快照,并使用快照作为视图的可视化表示。如果你从未改变过视图内容,视图的绘制代码可能永远不会被再次调用。快照图片对于涉及视图的大部分操作中被重用。如果你更改了内容,你通知系统视图被改变了。视图重复绘制视图和捕捉新结果的快照。

当视图内容改变的时候,你不需要直接重绘这些变化。相反,你使用setNeedsDisplaysetNeedsDisplayInRect:中任何一个方法使视图失效。这些方法告诉系统视图中的内容被改变了,需要在下次机会被重绘。

当到了渲染视图内容的时候,实际的绘制过程取决于视图和它的配置。系统视图通常实现私有绘图方法来呈现其内容。这些相同的系统视图通常暴露接口,使用接口你可以配置视图的实际外观。对于自定义的UIView子类,你通常覆盖你视图的drawRect:方法,并使用该方法来绘制视图的内容。也有其他的方式来提供视图的内容,例如,直接设计内容底部的layer,但是覆盖drawRect:方法是最常用的技术。

下面再来看下上面提到的三个方法:

  • - setNeedsDisplay

声明:

SWIFT

1
func setNeedsDisplay()

OBJECTIVE-C

1
- (void)setNeedsDisplay

你可以使用该方法或setNeedsDisplayInRect:方法来通知系统你的视图内容需要被重绘了。这个方法发起请求并立即返回。视图并不是真的在重绘直到下一个绘制周期,在该点所有失效的视图被更新。

你应该只在视图的内容和外观改变时使用该方法来请求视图重绘。如果你只是简单地更改视图的几何结构,视图通常不会重新绘制。作为代替,它的现有内容基于视图的contentMode属性的值进行调整。重新显示现有的内容通过避免重绘并没有改变的内容而提供了性能。

  • - setNeedsDisplayInRect:

声明:

SWIFT

1
func setNeedsDisplayInRect(_ invalidRect: CGRect)

OBJECTIVE-C

1
- (void)setNeedsDisplayInRect:(CGRect)invalidRect
  • - drawRect:

声明:

SWIFT

1
func drawRect(_ rect: CGRect)

OBJECTIVE-C

1
- (void)drawRect:(CGRect)rect

参数:

参数名 参数描述
rect 视图的边界部分。你的视图在第一绘制时,该矩形是通常是视图的整个可见边界。然而,在随后的绘制操作中,矩形可能被指定为视图的一部分。

该方法的默认实现不做任何事情。使用技术,例如Core GraphicsUIKit来绘制它们的视图内容时,子类应该覆盖该方法并在该方法中实现它们的绘制代码。如果你的视图通过其他方式来设置它的内容,则你不需要覆盖该方法。例如,你不需要覆盖该方法如果你的视图仅显示一个背景颜色时或者你的视图通过底层的layer对象直接设置它们的内容。

在该方法被调用时,UIKit已经正确的为您的视图配置好绘制环境,你可以简单的调用任何绘制方法和功能来渲染你的内容。具体来说,UIKit创建和配置了一个图形上下文来绘制和调整在那个上下文中的转换,从而它的原点匹配你视图边界矩形的原点。你可以通过UIGraphicsGetCurrentContext函数来获取到该图形上下文的引用,但是不要建立一个强引用到该图形上下文,因为它会因为drawRect:方法的调用被改变。

需要注意的是,你永远不要直接调用该方法。我们可以通过setNeedsDisplaysetNeedsDisplayInRect:方法来告诉视图需要重绘。

Content Mode

每个视图有一个内容模式,它控制视图在响应视图的几何结构改变时如何回收其内容,以及是否回收其内容。当视图第一次显示时,它和平常一样渲染内容,且结果在底层的位图中被捕获。在那之后,更改视图的几何形状并不总是导致位图重新被创建。作为替代,contentMode属性的值决定了位图是否应该缩放以适应新的边界,或简单的固定到一个角落或视图的边缘。

当你做以下事情时,一个视图的内容模式被应用:

  • 改变视图的framebounds矩形的宽度或高度
  • 分配一个包括一个伸缩因子的变换到视图的transform属性

默认情况下,对于大多数视图的contentMode属性被设置为UIViewContentModeScaleToFill,这将导致视图的内容被伸缩以适应新的框架尺寸。下面的图显示了几个contentMode属性值之间的比较:

enter image description here

内容模式有利于回收利用视图的内容,但是你也可以设置内容模式为UIViewContentModeRedraw,当你需要你的自定义视图在伸缩或调整操作时重绘它们自己。设置你的视图的内容模式为该值时,会强制系统调用你视图的drawRect:方法以响应几何尺寸的改变。一般情况下,只要有可能你应该避免使用这个值,你当然不应该在标准的系统视图中使用它。

内置动画支持(Built-In Animation Support)

在每一个视图后面有一个layer对象的一个好处是,你可以轻松动画许多视图相关的改变。动画是一种将信息传达给用户的非常有用的方式,在您的应用程序的设计过程中应该始终考虑它。UIView类的许多属性是可动画的——也就是说,半自动的支持从一个值到另一个值的动画。为了对这些动画属性之一执行动画,你所要做的是:

  1. 告诉UIKit你要执行的动画
  2. 改变属性的值

UIView对象上你可以设置动画的属性为以下这些:

  • frame:使用这个来动画视图的位置和尺寸
  • bounds:使用这个来动画视图的尺寸
  • center:使用这个来动画视图的位置
  • transform:使用这个来旋转或伸缩视图
  • alpha:使用这个来改变视图的透明性
  • backgroundColor:使用这个来改变视图的背景色
  • contentStretch:使用这个来改变视图如何伸缩

视图几何结构和坐标系统(View Geometry and Coordinate Systems)

UIKit中默认的坐标系统有一个原点在左上角,且坐标轴向原点处往右和下延伸。坐标值是使用浮点数来代表,这使得精确布局和内容定位不考虑底层的屏幕分辨率。下图展示了相对于屏幕的坐标系统。

enter image description here

因为每个视图和window定义了它们自己的局部坐标系统,你需要知道在任何给定时间是哪个坐标系在起作用。任何时候你在视图中绘制或改变它的几何结构时,你这样做,相对于一些坐标系统。在正在绘制的情况下,你指定相对于视图自己的坐标系统的坐标。在几何结构改变的情况下,你指定相对于superview坐标系统的坐标。UIWindowUIView类都包含了方法来帮助你从一个坐标系统转换为另一个。

frameboundscenter属性之间的关系

一个视图对象使用frameboundscenter属性来跟踪它的尺寸和位置:

  • frame:frame属性包含了框架矩形,它指定了视图相对于superview坐标系统的尺寸和位置
  • bounds:bounds属性包含了边界矩形,它指定了视图相对于它的局部坐标系统的尺寸和位置
  • center:center属性包含了视图在superview坐标系统的中电

你使用centerframe属性主要用于操作当前视图的几何结构。例如,当你在构建你的视图层级或在运行时改变一个视图的位置和尺寸时,你可以使用这些属性。如果你仅仅只是想改变视图的位置,center属性是首选的方法。center属性的值始终是有效的,即使伸缩或旋转因素被添加到视图的变换中。在同样情况下,对于frame属性则不正确,这被认为是无效的,如果视图的变换不等于恒等变换。

你使用bounds属性主要在绘制期间。边界矩形是在视图自己的局部坐标系统中的表示。该矩形的默认原点为(0, 0),且它的尺寸匹配框架矩形的尺寸。任何你在该矩形中绘制的东西是视图可见内容的一部分。如果你改变边界矩形的原点,任何你在新的矩形中绘制的东西称为视图可见内容的一部分。

下图显示了这三个属性之间的关系:

enter image description here

虽然你改变frameboundscenter属性独立于其他,但是改变其中一个属性会按以下方式来改变其他属性:

  • 当你设置frame属性,bounds属性中的尺寸值也相应改变以匹配框架矩形的新尺寸。center属性的值也同样改变以匹配框架矩形中xin的中点。
  • 当你设置center属性,在frame属性中的原点值也会相应改变。
  • 当你设置bounds属性的尺寸时,frame属性中的尺寸值也相应改变以匹配边界矩形中的新尺寸。

默认情况下,一个视图的框架(frame)不会被其superview的框架剪切。你可以改变这种行为,通过设置superviewclipToBounds属性为YES

坐标系统转换

坐标系统转换提供了一种方式来更快和更简单的更新你的视图。一个仿射变换是一个数学矩阵,它指定了点如何从一个坐标系统映射到另一个坐标系统。你可以应用仿射变换到你的整个视图,以改变尺寸、位置或相对于其superview的方向。你也可以使用仿射变换在你的绘制代码中,去以单个块的渲染内容来执行相同类型的操作。如何应用仿射变换取决于上下文:

  • 要改变你的整个视图,改变你视图中transform属性的仿射变换。
  • 要改变你视图的drawRect:方法的指定块的内容,改变关联当前活跃图形上下文的仿射变换。

通常,您可以修改视图的transform属性,当你想要实现动画时。例如,你可以使用该属性创建一个你视图围绕中点旋转的动画。你不能使用该属性永久改变你的视图,例如改变视图在它的superview坐标空间的尺寸和位置。对于那种类型的改变,你应该改变框架矩形作为替代。

在你的drawRect:方法中,你可以使用仿射变换来定位和适应你计划绘制的元素。而不是固定在你视图某个位置的一个对象的位置,去创建相对于一个固定点的每个对象是非常简单的,通常(0, 0),使用一个transform来定位该对象在立即绘制之前。在那种方式,该对象的位置在你的视图中改变,所有你所要做的就是修改变换,这是更快,更便宜的,相对于重新创建一个对象在它的新位置。你可以通过使用CGContextGetCTM函数来检索关联图形上下文的仿射变换,且你可以使用相关的Core Graphics函数在绘制期间设置和修改变换。

当前变换矩阵(current transformation matrix (CTM))是在任何给定时间当前正在使用的仿射变换。当操作整个视图的几何结构时,CTM是储存在你视图的transform属性的仿射变换。在你的drawRect:方法中,CTM是关联当前活跃图形上下文的仿射变换。

每个子视图的坐标系建立在其祖先的坐标系上。所以当你修改一个视图的transform属性时,这些改变会影响视图和它的所有子视图。然而,这些改变只影响视图在屏幕上的最后渲染。因为任何视图绘制它的内容和布局它的子视图是相对于它自己的边界,它在绘制和布局期间会忽略它的superview的变换。

创建和管理视图层次

管理视图层次是开发应用程序用户界面的重要组成部分。你的视图的组织影响你的应用程序的外观和应用程序如何响应变化和事件。例如,在视图层次中的父-子关系决定了哪个对象可能处理某个指定的触摸事件。同样的,父-子关系定义了每个视图如何响应界面方向改变。

添加和移除子视图

Interface Builder是建立视图层次的最方便的方式,因为你以图形化的方式组装视图,可以看到视图之间的关系,同样可以看到这些视图如何在运行时显示。当使用Interface Builder,你保存你的结果视图层级在一个nib文件中,你可以在运行时加载相应需要的视图。

如果你更愿意以程序的方式来创建你的视图,你创建和初始化它们,然后使用以下方法安排它们到层次:

  • 要添加一个子视图到父亲,在父视图上调用addSubview:。该方法添加子视图到父视图的子视图列表的末尾。
  • 要插入一个子视图到父视图的子视图列表的中间,可以在父视图上调用任何和insertSubview:...相关的方法。
  • 要重排父视图中现有的子视图,调用父视图的bringSubviewToFront:sendSubviewToBack:exchangeSubviewAtIndex:withSubviewAtIndex:方法。使用这些方法比移除子视图或重新插入它们更快。
  • 要从父视图中移除一个子视图,在子视图上调用removeFromSuperview方法。

添加一个子视图到另一个视图中最常见的例子发生在application:didFinishLaunchingWithOptions:方法中。下面显示了一个该方法的版本,它将视图从应用程序的主视图控制器安装到应用程序的windowwindow和视图控制器都是储存在应用程序的主nib文件中,它会在该方法调用前被加载。然而,由视图控制器管理的视图层级实际上不会加载,直到view属性被访问。

添加一个子视图到window

1
2
3
4
5
6
7
8
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
// Add the view controller's view to the window and display.
[window addSubview:viewController.view];
[window makeKeyAndVisible];
return YES; }

另一个常见的地方你可能添加子视图到视图层级中是在视图控制器的loadViewviewDidLoad方法中。如果你正在利用程序构建视图,你讲创建视图的代码放在视图控制器的loadView方法中。无论你是使用程序的方式,还是使用nib文件加载的方式来创建视图,你可能会在viewDidLoad方法中包含额外的视图配置代码。

添加视图到已存在的视图层级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)viewDidLoad
{
[super viewDidLoad];
self.title = NSLocalizedString(@"TransitionsTitle", @"");
// create the container view which we will use for transition animation (centered horizontally)
CGRect frame = CGRectMake(round((self.view.bounds.size.width - kImageWidth) /
2.0),
kTopPlacement, kImageWidth,
kImageHeight);
self.containerView = [[[UIView alloc] initWithFrame:frame] autorelease];
[self.view addSubview:self.containerView];
// The container view can represent the images for accessibility.
[self.containerView setIsAccessibilityElement:YES];
[self.containerView setAccessibilityLabel:NSLocalizedString(@"ImagesTitle",
@"")];
// create the initial image view
frame = CGRectMake(0.0, 0.0, kImageWidth, kImageHeight);
self.mainView = [[[UIImageView alloc] initWithFrame:frame] autorelease];
self.mainView.image = [UIImage imageNamed:@"scene1.jpg"];
[self.containerView addSubview:self.mainView];
// create the alternate image view (to transition between)
CGRect imageFrame = CGRectMake(0.0, 0.0, kImageWidth, kImageHeight); self.flipToView = [[[UIImageView alloc] initWithFrame:imageFrame] autorelease];
self.flipToView.image = [UIImage imageNamed:@"scene2.jpg"];
}

当你添加子视图到另一个视图,UIKit通知父视图和子视图这些改变。如果你实现了自定义视图,你可以覆盖以下方法来拦截这些通知:willMoveToSuperview:willMoveToWindow:willRemoveSubview:didAddSubview:didMoveToSuperviewdidMoveToWindow。您可以使用这些通知以更新与您的视图层次中相关的任何状态信息或执行其他任务。

隐藏视图

为了在视觉上隐藏视图,你可以通过设置hidden属性为YES或者设置alpha属性为0.0。一个隐藏的视图不从系统接收触摸事件。然而,隐藏的视图仍然参与到自动调整大小和其他关联视图层级的布局操作。因此,当你需要从视图层级移除视图,隐藏视图通常是一个方便的选择,特别是如果你计划在某个点再重新显示视图。

如果你想动画一个视图,从可见过渡到隐藏(或者相反),你必须使用视图的alpha属性。因为hidden属性不是一个可动画的属性,所以你在上面做的改变会立即发生。

在视图层级中定位视图

有两种方法在视图层级中定位视图:

  • 在适合的位置存储任何有关视图的指针,例如,在拥有该视图的视图控制器。
  • 分配一个唯一的整数到每个视图的tag属性,然后使用viewWithTag:来定位它。

储存相关视图的引用是最常见定位视图的方式,且使得访问这些视图非常容易。如果你使用Interface Builder来创建你的视图,你可以在你的nib文件中使用outlets连接对象到另一个。对于你用程序创建的视图,你可以在私有成员变量中保存这些视图的引用。无论你是使用outlets还是私有成员变量,你负责在需要时保留视图,或者释放它们。保证对象保留和释放最好的办法是使用声明的属性。

标签是减少硬编码依赖和支持动态和灵活的解决方案的有效方式。而不是保存视图的指针,你可以使用tag来定位它。标签也是引用视图的一种更持久的方式。

平移,缩放和旋转视图

每个视图有一个关联的仿射变换,你可以使用它来平移、伸缩和旋转视图的内容。视图变换改变视图的最终渲染外观,且通常用于实现滚动,动画,或其他视觉效果。

UIViewtransform属性包含了一个应用变换的CGAffineTransform结构。默认情况下,这个属性被设置为恒等变换,不修改视图的外观。你可以在任何时候给该属性赋一个新的变换。例如,将一个视图旋转45度,你可以使用以下代码:

1
2
3
// M_PI/4.0 is one quarter of a half circle, or 45 degrees.
CGAffineTransform xform = CGAffineTransformMakeRotation(M_PI/4.0);
self.view.transform = xform;

enter image description here

在视图层级中转换坐标

在不同的时间,特别是处理事件时,应用程序可能需要从一个框架的引用到另一个之间转换坐标值。例如,触摸事件是在window坐标系统中报告每一次触摸的位置,但是视图对象通常在视图的局部坐标系统中需要这些信息。UIView类定义了以下方法来转换坐标:

1
2
3
4
convertPoint:fromView:
convertRect:fromView:
convertPoint:toView:
convertRect:toView:

convert...:fromView:方法从其他视图坐标系统转换坐标到当前视图的局部坐标系统。相反的,convert...:toView:方法从当前视图坐标系统转换坐标到指定的视图的坐标系统。如果对于任何方法你指定nil为引用的视图,是从包含当前视图的window的坐标系统中转换。

除了UIView的转换方法,UIWindow类也同样定义了几个转换方法。

1
2
3
4
convertPoint:fromWindow:
convertRect:fromWindow:
convertPoint:toWindow:
convertRect:toWindow:

实现自定义视图的清单

自定义视图的工作是呈现内容和管理与这些内容的交互。一个成功的自定义视图的实现包含了不仅绘制和事件处理。在实现自定义视图时,下面的清单包括了你应该覆盖的比较重要的方法:

  • 为你的视图定义相应的初始化方法:
    • 对于你想用程序创建的视图,覆盖initWithFrame:方法,或者定义一个自定义初始化方法
    • 对于你想从nib文件加载的视图,覆盖initWithCoder:方法。
  • 实现一个dealloc方法处理任何自定义数据的清理工作。
  • 要处理自定义的绘制,覆盖drawRect:方法,并在其中实现你的绘制代码。
  • 设置视图的autoresizingMask属性以定义它的自动调整大小行为。
  • 如果你的视图类管理一个或多个完整的子视图,做以下几点:
    • 在你的视图初始化阶段创建这些子视图
    • 对于每个子视图在创建时设置autoresizingMask属性
    • 如果你的子视图需要自定义布局,覆盖layoutSubviews方法,并在其中实现你的布局代码
  • 为了处理触摸相关的事件,做以下几点:
    • 使用addGestureRecognizer:方法附加任何合适的手势识别到视图中
    • 对于你要自己处理触摸的情况下,覆盖touchesBegan:withEvent:touchesMoved:withEvent:touchesEnded:withEvent:touchesCancelled:withEvent:方法
  • 如果你想你视图的绘制版本看起来不同于屏幕上的版本,实现drawRect:forViewPrintFormatter:方法

动画

使用基于块的方法开始动画

在iOS4和以后,你可以使用基于块的类方法来初始化动画。有几个基于块的方法为动画块提供了不同级别的配置。它们为:

  • animateWithDuration:animations:
  • animateWithDuration:animations:completion:
  • animateWithDuration:delay:options:animations:completion:

因为它们是类方法,使用它们创建的动画块不依赖于单个视图。因此,你可以使用该方法来创建一个包含改变多个视图的动画。例如:

1
2
3
4
[UIView animateWithDuration:1.0 animations:^{
firstView.alpha = 0.0;
secondView.alpha = 1.0;
}];

如果你想对动画有更多的配置,那就要使用animateWithDuration:delay:options:animations:completion:了,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (IBAction)showHideView:(id)sender
{
// Fade out the view right away
[UIView animateWithDuration:1.0
delay: 0.0
options: UIViewAnimationOptionCurveEaseIn
animations:^{
thirdView.alpha = 0.0;
}
completion:^(BOOL finished){
// Wait one second and then fade in the view
[UIView animateWithDuration:1.0
delay: 1.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
thirdView.alpha = 1.0;
}
completion:nil];
}];
}

创建在视图间过渡的动画

当在视图层级中添加、移除、隐藏和展示视图时,视图间过渡帮助你隐藏立即的改变。你可以使用视图过渡去实现以下类型的改变:

  • 更改现有视图的可见子视图
  • 在你的视图层级中替换某个视图为另外一个视图

改变一个视图的子视图

在iOS4及以后,你可以使用transitionWithView:duration:options:animations:completion:方法来为视图初始化一个过渡动画。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (IBAction)displayNewPage:(id)sender
{
[UIView transitionWithView:self.view
duration:1.0
options:UIViewAnimationOptionTransitionCurlUp
animations:^{
currentTextView.hidden = YES;
swapTextView.hidden = NO;
}
completion:^(BOOL finished){
// Save the old text and then swap the views.
[self saveNotes:temp];
UIView* temp = currentTextView;
currentTextView = swapTextView;
swapTextView = temp;
}]; }

替换视图为不同的视图

在iOS4及以后,你可以使用transitionFromView:toView:duration:options:completion:方法在视图间过渡。这个方法实际会将第一个视图从你的层级中移除,然后插入另外一个,因此,你应该确保如果你想保持第一个视图时,你有一个到第一个视图的引用。如果你想隐藏视图来代替从视图层级中移除,传递UIViewAnimationOptionShowHideTransitionViews关键字作为选项。例如:

1
2
3
4
5
6
7
8
9
10
11
- (IBAction)toggleMainViews:(id)sender {
[UIView transitionFromView:(displayingPrimary ? primaryView : secondaryView)
toView:(displayingPrimary ? secondaryView : primaryView)
duration:1.0
options:(displayingPrimary ? UIViewAnimationOptionTransitionFlipFromRight : UIViewAnimationOptionTransitionFlipFromLeft)
completion:^(BOOL finished) {
if (finished) {
displayingPrimary = !displayingPrimary;
}
)];
}