原作:You-Dont-Know-JS
本文的99.9%的内容都来自《You dont know JS》的电子中文版

传送门:《You don’t know JS》 上(入门&作用域&对象)

类型和文法

内建类型

  • 7种类型
  • 值才有类型,变量没有
  • undefined ≠ is not defined(undeclared)。undefined表示定义却没有赋值的变量类型。然而typeof一个未声明的变量也会返回undefined,这是typeof的安全机制,它给了我们更多空间检查变量是否可用。

由于JS里String的只读性,所有String的相关方法都是返回一个新字符串。

使用二进制浮点数的最出名(臭名昭著)的副作用是(记住,这是对 所有 使用 IEEE 754 的语言都成立的 —— 不是许多人认为/假装 仅 在 JavaScript 中存在的问题):0.1 + 0.2 === 0.3 // false。不过可以用Number.EPSILON做最小误差得到足够精确的近似结果。ES6下已经可以用Number.isInteger()Number.isSafeInteger()检查数字是不是整数/安全整数。

特殊值

undefinednull是JS里比较特殊的两类值,它们既是类型又是唯一的值。更加不幸的是,在非strict模式下,undefined还可以作为标识符,像下面这样:

1
undefined = 2;

另外,在特别需要undefined时,void操作符会变得很有用。

Infinity / Infinity == undefined。

针对一些特殊的等价情况(NaN和-0),ES6使用Object.is()判断其相等性。

值与引用

在JS中没有指针,只有引用,同时页没有语法上的提示可以控制值和引用的赋值/传递。取而代之的是,值的类型用来唯一控制值是通过值拷贝,还是引用拷贝来赋予(复合值)。引用指向的是值本身而不是变量,不能使用一个引用来改变另一个引用所指向的值。

底层的基本标量值是不可变的(String和Boolean也一样)。比如一个Number对象持有一个基本标量值2,那么这个Number对象就永远不能再持有另一个值;你只能用一个不同的值创建一个全新的Number对象:

1
2
3
4
5
6
7
8
9
10
function foo(x) {
x = x + 1;
x; // 3
}

var a = 2;
var b = new Number( a ); // 或等价的 `Object(a)`

foo( b );
console.log( b ); // 2, 不是 3

在其中x = x + 1这一步,包装值内的x被取出+1后,赋值给x,将其从一个引用变成一个基本标量值3。

类型转换

对于最简单的值,JSON字符串化行为基本上和toString()转换是相同的,在对String字符串化时,结果也会包含"",如JSON.stringify("11") // ""11""。另外,对于JSON不安全值(即不能移植到消费JSON的语言中),有下面的处理:

  • 忽略undefinedfunctionsymbol
  • Array中遇到这种类型的值,会被替换为null(避免修改位置信息)
  • Object的属性中遇到时,属性会被简单的忽略掉
  • 带有循环引用时,JSON.stringify()会报错

另外,对于有toJSON()方法的对象,JSON字符串化会优先使用该方法。JSON.stringify()的第二个参数可以指定Array或Function说明可以编辑的对象属性。第三个参数是填充符,填充在各级开头,用来友好展示结果,最多取入参的前10个字符。

在对象上使用toNumbertoString方法,首先会找到其原始类型(toPrimitives()),即使用其valueOf()toString()方法(也会在[[prototype]]上寻找)。

-> Number

可以用Date.now()代替+new Date()获取更好的语义。

~除了可以用来检查-1这个特殊的值,还可以通过~~对小数取整,因为执行位操作时会先将数字转为Int32类型。

parseInt以及parseFloat+Number()强制类型转换存在区别。它们的作用是,从字符串中解析出一个number出来。两者是不能相互替换的。后者是不能容忍非数字字符的。另外,**请在字符串上使用parseIntparseFloat**,这也是它们的设计目的。对非字符串类型使用它们可能得到意外的结果:

1
parseInt( 1/0, 19 ); // 18,惊不惊喜,意不意外

原因是,parseInt会把第一个参数toString(这不能责怪它,因为它本来就是设计对String使用的)。类似的例子还能举出很多:

1
2
3
4
5
6
7
parseInt( 0.000008 );       // 0   ("0" from "0.000008")
parseInt( 0.0000008 ); // 8 ("8" from "8e-7")
parseInt( false, 16 ); // 250 ("fa" from "false")
parseInt( parseInt, 16 ); // 15 ("f" from "function..")

parseInt( "0x10" ); // 16
parseInt( "103", 2 ); // 2

另外,parseInt会通过前缀试图猜测数字进制,默认是10进制。以0x开头表示16进制,以0b开头表示2进制,以0o开头表示8进制。

-> Boolean

使用!!强制转换类型。

&&||在JS中的逻辑和C++以及Java中的不大一样,它并不一定返回boolean类型的值,而是根据比较的两个数判断返回哪一个。其中&&可以用来进行短路操作。

另外,对于Symbol来说,只能通过String()的形式转为String类型,却不能转为Boolean类型。

等价

等价分为=====

StringNumber进行比较时,会对String使用强制类型转换(类似+Number());

在和Boolean比较时,会首先把Boolean类型转为Number类型,再进行比较。这会产生下面这样比较迷惑的情况:

1
2
"42" == true  // false
"42" == false // false

Object和非Object比较时,会先对Object进行toPrimtives,即先使用valueOf()看能否转成基本类型,再使用toString()

下面有一些疯狂的例子,但却可以由上面的规则解释:

1
2
3
4
5
6
7
8
"0" == false    // true
false == [] // true
0 == [] // true
[] == ![]; // true
2 == [2]; // true
"" == [] // true
"" == [null]; // true
0 == "\n" // true

通过上面的坑可以看到,等号的两边总有[]""false0。建议在这些情况使用===

下面是由Alex Dorey(@dorey on GitHub)制作的一个方便的表格,将各种比较进行了可视化:

大小关系比较

首先对值进行toPrimitives转换,如果有一个不是String,则使用Number类型比较。见下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// toNumber
var a = [ 42 ];
var b = [ "43" ];

a < b; // true

// toString
var a = { b: 42 };
var b = { b: 43 };

a < b; // false
a == b; // false
a > b; // false

a <= b; // true
a >= b; // true

在下面的例子里,a和b在比较时,都会转成”[object Object]”。而等价比较上会比较引用是否相同。因此都返回false,而JS中的<=>=操作会去对应计算><的结果再取反,从而会得到两个true

语法

语句和表达式

JS中的表达式都有一个隐式的返回值,但是它只会打印在控制台中,并不会真实返回。块语句的返回值是块中最后一个语句的返回值。ES7中可能会引入do语句显式地使用这个返回值。

JS中的++和C风格类似,表示自增,有前后之分。但是++a++这种用法是不合法的。

赋值表达式的返回是赋予的值,这在链式赋值时很好用:

1
2
var a, b, c;
a = b = c = 42;

这里,c = 42被求值得出42(带有将42赋值给c的副作用),然后b = 42被求值得出42(带有将42赋值给b的副作用),而最后a = 42被求值(带有将42赋值给a的副作用)。

另一种用法是直接将之放在&&||的前后,检查赋值语句的真值。

上下文

{}包裹的内容作为表达式结果可以直接赋值给变量,但是直接声明时会被当做代码块,但是可能仍然是合法的,如:

1
2
3
{
foo: bar()
}

因为,JS中允许使用语句标签,便于breakcontinue跳转(JS中没有goto)。而JSON中带有""的键则不会被这么解释,因为语句标签不允许出现引号。

我们现在来解决下面的问题:

1
2
[] + {}; // "[object Object]"
{} + []; // 0

为什么交换顺序会对+的规则有影响?在第一个式子里一切都是正常的,[]转换成""{}转换成[object Object];在第二个式子里,{}被理解成一个空代码块儿,[]被强制转换为0。

操作符优先级

  • &&优先于||这里有完整的表格。
  • &&||有短接的特点,即当第一个表达式为true或false时直接返回结果。
  • 赋值表达式和三元表达式? : 一样是从右向左结合的

ASI(自动分号)

尽量避免ASI,只在确认没有歧义的地方依赖ASI。

错误

  • JS有早期错误一说,即运行前编译期间的错误
  • let会造成块域内的TDZ(Temporal Dead Zone,时间死区),typeof在此时会报错,而不会返回undefined。TDZ是指变量还没到能使用它的时候,还需要初始化。下面还有一个例子:
    1
    2
    3
    4
    5
    var b = 3;

    function foo( a = 42, b = a + b + 5 ) {
    // ..
    }
  • ES6提供了剩余参数来代替原有的arguments对象,这更加安全。

finally子句

try catch在和finally一起使用时,finally的语句一定会被执行,而且一定会在try语句执行完后立即执行,即使try中有return或者throwcontinue等控制语句。可以在finally中修改try中的结果,但是最后不要这么做,因为会影响程序可读性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 function foo() {
try {
throw 42;
}
finally {
console.log( "Hello" );
}

console.log( "never runs" );
}

console.log( foo() );
// Hello
// Uncaught Exception: 42

宿主环境

由于浏览器的遗留行为,使用id属性创建DOM元素会创建同名的全局变量。

1
<div id="foo"></div>
1
2
3
4
5
if (typeof foo == "undefined") {
foo = 42; // 永远不会运行
}

console.log( foo ); // HTML元素
  • 永远不要修改内建类型。
  • JS的函数和变量声明提升只在同一<script>标签内

保留字

Let this long package float, Goto private class if short. While protected with debugger case, Continue volatile interface. Instanceof super synchronized throw, Extends final export throws.

Try import double enum?

False, boolean, abstract function, Implements typeof transient break! Void static, default do, Switch int native new. Else, delete null public var In return for const, true, char …Finally catch byte.

来自StackOverflow用户“art4theSould”创造性的一首小诗

另外,在ES6+中,可以使用保留字作为对象字面量中的属性名或键。

异步与性能

JS引擎对时间没有天生的感觉,只是一个任意JS代码段的按需执行环境。是周围的宿主环境在不停地安排“事件”(JS代码的执行)。举例来说,当你的JS程序发起一个从服务器取得数据的Ajax请求时,你在一个函数(通常称为回调)中建立好“应答”代码,然后JS引擎就会告诉宿主环境,“嘿,我就要暂时停止执行了,但不管你什么时候完成了这个网络请求,而且你还得到一些数据的话,请回来调这个函数。”

然后浏览器就会为网络的应答设置一个监听器,当它有东西要交给你的时候,它会通过将回调函数插入事件轮询来安排它的执行。

关于事件轮询队列,之前也有过一些介绍。

异步概览

异步≠并行。异步本质上还是串行的。工作依然有先后之分,没有线程、线程池的概念。从而,在JS中的函数都是原子的,即不会与别的函数的代码相互穿插(除非使用Generator)。

并发

并发是当两个或多个“进程”(或任务)在同一时间段内同时执行,而不管构成它们的每个操作是不是同时进行的。在JS中,单线程事件轮询是并发的一种表达。

不互动

当程序中运行多个“进程”(或任务),如果它们之间没有逻辑联系,那么不互动是完全可以接受的。

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

function foo(results) {
res.foo = results;
}

function bar(results) {
res.bar = results;
}

// ajax(..) 是某个包中任意的Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

互动

相反,如果它们之间有依赖关系,或者前后次序而产生互动时,let it alone就会出事。

1
2
3
4
5
6
7
8
9
var res = [];

function response(data) {
res.push( data );
}

// ajax(..) 是某个包中任意的Ajax函数
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );

协调

跳过使用全局变量等丑陋的协作手段,有一种方法,将长时间处理的任务打断成多个小段的请求外加setTimeout,以便将任务穿插完成。

Jobs

ES6在事件轮询队列之上引入了一层新概念,称为“工作队列(Job queue)”。它和轮询队列的关系类似于Macrotask和Microtask。

回调

顺序的大脑

回调不符合正常思维逻辑顺序 & 回调地狱。

信任问题

(本人并不完全赞同)回调遭受着控制反转的蹂躏,它们隐含地将控制权交给第三方(通常第三方工具不受你控制!)来调用你程序的延续。

Promise

Promise的thencatch

可靠的Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var p3 = new Promise( function(resolve,reject){
resolve( "B" );
} );

var p1 = new Promise( function(resolve,reject){
resolve( p3 );
} );

var p2 = new Promise( function(resolve,reject){
resolve( "A" );
} );

p1.then( function(v){
console.log( v );
} );

p2.then( function(v){
console.log( v );
} );

// A B <-- 不是你可能期望的 B A

这是因为p1由p3解析的结果所解析,这个过程是异步地。

作者认为Promise在很大程度上,解决了下面的问题:

  • 调的太早/太晚(本人并不赞同)
  • 根本不调回调(勉强成立),Promise通知状态改变是由编程者自己代码控制的,用resolvereject(用户只能借助外部环境API发起异步操作,resolve一样要么放在传统的回调,要么转交给第三方完成)。
  • 调太少或太多次(成立),一个Promise一旦resolve或者reject,状态就不再发生变化
  • 没能传入任何参数/环境(勉强成立),原因与第二条相同
  • 吞掉所有错误和异常(勉强成立),Promise中在catch字句里捕获异常。

Promise.resolve(p)会把thenable的入参p转换为合法的Promise。这里猜测下这个resolve(p)的实现(个人猜想):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Promise.resolve = thenable => {
// if `thenable` is a promise, just return it
// ...

// if `thenable` is plain, just resolve
if (typeof thenable.then != 'function') {
return new Promise(resolve => {
resolve(thenable);
});
}

return new Promise((resolve, reject) => {
thenable.then(resolve, reject);
});
}

// 一个thenable的例子
const p = {
then(cb, err) {
Math.random < 0.5 ? cb(42) : err("oops! Something bad happens.");
}
}

链式调用

看看RxJS的Introduction。就可以很轻松地理解Promise的流程和链式过程了。

Promise模式

Promise.all()Promise.race()。除了这两个官方钦定的方法外,其他的Promise库还实现了像是any()none()first()last()这样的方法,看看RxJS的operators会有更多选择。

Promise的限制

  • 顺序的错误处理
  • 只能传单一的值(其实就是状态改变不可逆)
  • 单次解析(同上),文章也在惰性的上方提到了观察者模式的RxJS,的确在设计时间概念的领域,RxJS要厉害多了
  • 惰性(生产生产Promise函数的工厂函数)
  • 不可反悔(即不能中途撤销)
  • 性能

Generator

使用同步风格书写异步代码的基础在Generator。关于这部分的更详细介绍见本人之前参考阮一峰大神写的博文

打破运行至完成

generator(生成器)是一个可以和别的代码穿插执行的非原子的特殊函数。使用new构造generator得到的只是一个迭代器,迭代器在执行到yield时会让出执行权。真正执行这个迭代器需要用调用或者执行器的方式。

yield和next是generator可以和外部甚至是其他generator双向通信。但是generator只是声明了自己将要以什么样的形式去执行。还需要一个下面这样的帮助函数去推动它执行:

1
2
3
4
5
6
7
8
9
function step(gen) {
var it = gen();
var last;

return function() {
// 不论`yield`出什么,只管在下一次时直接把它塞回去!
last = it.next( last ).value;
};
}

生成器

  • 可以把generator像状态机一样使用。
  • for of需要迭代器的实现
  • 可以在generator上使用for of
  • 使用return而非next可以终止生成器执行

在异步流程中使用generator

generator的yield暂停特性不仅意味着我们可以从异步的函数调用那里得到看起来同步的return值。

带有promise的generator

在ES6的世界中最棒的就是将generator(看似同步的异步代码)与Promise(可靠性和可组合性)组合起来。

co与koa。

ES7中的await和async

像下面这样,没有run函数,没有生成器函数的*

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function foo(x,y) {
return request(
"http://some.url.1/?x=" + x + "&y=" + y
);
}

async function main() {
try {
var text = await foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
}

main();

yield委托

使用yield * foo可以把其他的生成器函数整合进当前生成器中。除了生成器外,还可以委托一个非generator的iterator。错误可以委托,promise可以委托,委托还可以递归。

结合yield可以很方便地协调多个generator

thunk

同步的thunk即包装了所有预设形参的函数执行的函数。异步thunk指需要指定callback的包装所有其他预设形参异步函数的函数。像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 同步thunk
function foo(x,y) {
return x + y;
}

function fooThunk() {
return foo( 3, 4 );
}

// 异步thunk
function foo(x,y,cb) {
setTimeout( function(){
cb( x + y );
}, 1000 );
}

function fooThunk(cb) {
foo( 3, 4, cb );
}

一旦来说会有一个工具thunkify帮你完成制造函数thunk的工作(放心,总会有人这么做的)。它的用法是下面这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var fooThunkory = thunkify( foo );

var fooThunk1 = fooThunkory( 3, 4 );
var fooThunk2 = fooThunkory( 5, 6 );

// 稍后

fooThunk1( function(sum) {
console.log( sum ); // 7
} );

fooThunk2( function(sum) {
console.log( sum ); // 11
} );

包装函数生产一个thunkory,之后指定除cb以外的其他参数得到thunk函数。

thunk和Promise本质上其实是等价的。只不过是回调所在的为之不一样罢了。所以使用Promise.wrap包装得到的promise还是thunkify包装得到的thunk函数其实都可以yield出来。因为,它们都能通过指定回调来让generator进一步推动下去。

当然了无论是在可组合性还是错误处理上,Promise都有更胜一筹。所以,thunk通常作为替代性的前ES6方案。

前ES6的Generator

当然了Generator也是可以通过其他方式实现的。

性能

Web Worker

近HTML5时代被加入web平台的特性,称为“Web Worker”。这是一个浏览器(也就是宿主环境)特性,而且几乎和JS语言本身没有任何关系。这里简单说了下它和Service Worker的区别。

asm.js

asm.js”是可以被高度优化的JavaScript语言子集的标志。通过小心地回避那些特定的很难优化的(垃圾回收,强制转换,等等)机制和模式,asm.js风格的代码可以被JS引擎识别,而且用主动地底层优化进行特殊的处理。

基准分析(BenchMark)和调优

  • Benchmark.js用统计学的方式避免时间戳测量语句性能时的不准确
  • jsPerf.com基于Benchmark.js的代码性能测试平台

编写好的测试

  • 注意上下文的影响
  • “过早的优化是万恶之源”
  • 尾部调用优化

ES6与未来

ES?现在与未来

  • polyfill与转译

语法

尽管ES6算是JS最新的官方特性,下面说的大部分特性已经被很经常地使用了。

  • 块作用域(之前的部分已经提到过了)
  • 扩散、剩余,...操作符,用在函数入参,数组和对象中
  • 函数默认参数值(是不是很神奇),默认参数值可以是合理的表达式甚至是函数调用
  • 解构赋值,也可以有默认参数值
  • 对象字面量拓展,简约声明/简约方法/getter,setter/计算型属性名/__proto__/super
  • 模板字面量
  • 箭头函数,词法this
  • for of和iterator
  • 正则表达式拓展
    • Unicode标识
    • 粘性标志
  • 数字,八进制
  • Unicode
    • 合理的string长度,String.prototype.normalize()
    • charCodeAt => codePointAt
    • fromCharCode => fromCodePoint
    • Unicode标识符名称
  • Symbol,新的基本类型,它是一个新的包装器对象,可以认为每个EVT_LOGIN持有一个不能被其他任何值所(有意或无意地)重复的值。
    • Symbol.for()先查询是否有一个同名的Symbol,如果有就返回,没有就创建一个

组织

迭代器

迭代器Iterator接口有一个必选接口next(),和两个可选接口return()throw(),它的result被规定为包括属性valuedone,下面是一个数组的迭代:

1
2
3
4
5
6
7
8
9
var arr = [1,2,3];

var it = arr[Symbol.iterator]();

it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }

it.next(); // { value: undefined, done: true }

但通常使用for of就足够了。我们可以依照这个接口,定义一个自己的迭代器:

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 Fib = {
[Symbol.iterator]() {
var n1 = 1, n2 = 1;

return {
// 使迭代器成为一个可迭代对象
[Symbol.iterator]() { return this; },

next() {
var current = n2;
n2 = n1;
n1 = n1 + current;
return { value: current, done: false };
},

return(v) {
console.log(
"Fibonacci sequence abandoned."
);
return { value: v, done: true };
}
};
}
};

Generator

这个上一章已经提到了。它可以用来:

  • 生产一系列值,即状态机
  • 串行执行的任务队列,化异步同步

模块

importexport

  • ES6引入了元属性的概念,用new.target表示。在任意的构造器中,new.target总是指向new实际直接调用的构造器。

集合

ArrayBuffer

它表示一组比特位,但是这些比特的实际意义是由结构化数组控制的,由它表示这些比特上的“视图”究竟是8位有符号整数还是字符串。

1
2
3
4
var buf = new ArrayBuffer( 32 );
buf.byteLength; // 32字节
var arr = new Uint16Array( buf );
arr.length; // 16

一个单独的缓冲可以连接多个视图

1
2
3
4
5
6
7
8
var buf = new ArrayBuffer( 2 );

var view8 = new Uint8Array( buf );
var view16 = new Uint16Array( buf );

view16[0] = 3085;
view8[0]; // 13
view8[1]; // 12

在ES6中可以使用下面的类型化数组构造器:

  • Int8Array(8位有符号整数),Uint8Array(8位无符号整数)
  • Uint8ClampedArray(8位无符号整数,每个值都被卡在0 - 255范围内)
  • Int16Array(16位有符号整数),Uint16Array(16位无符号整数)
  • Int32Array(32位有符号整数),Uint32Array(32位无符号整数)
  • Float32Array(32位浮点数,IEEE-754)
  • Float64Array(64位浮点数,IEEE-754)

Maps

摆脱对象只能使用字符串做键值的限制。有getsetdeletehasclear等方法。类似地还有WeakMap,不过它只能使用对象做键。

Sets

一个集合。类似Map,不过set换成了add,且没有get。Set和Map都有自己的迭代器。也可以通过keysvaluesentries来访问里面的内容。

新增API & 元编程

略,参考原文

ES6以后

  • asnyc function
  • Object.observe
  • 指数运算符**
  • Array#includes替代~Array.indexOf(value)
  • SIMD(多个数据),用于多个元素的并行数学操作,参考下面
    1
    2
    3
    4
    5
    var v1 = SIMD.float32x4( 3.14159, 21.0, 32.3, 55.55 );
    var v2 = SIMD.float32x4( 2.1, 3.2, 4.3, 5.4 );

    SIMD.float32x4.mul( v1, v2 );
    // [ 6.597339, 67.2, 138.89, 299.97 ]
  • WASM(Web Assembly)

-END-

原作:You-Dont-Know-JS
本文的99.9%的内容都来自《You dont know JS》的电子中文版

传送门:《You don’t know JS》 下(类型文法&异步&ES6与未来)

入门与进阶

值和类型

JavaScript只有带类型的值,没有带类型的变量。大家都知道JS的基本类型共6类:

  • undefined
  • null
  • boolean
  • number
  • string
  • object

但是在ES6之后,需要新增一类symbol。另外,对null使用typeof将得到“object”的结果。

JavaScript中对“falsy”的定义包括:

  • “”
  • 0, -0, NaN
  • null, undefined
  • false

除此之外的值都是truthy。

关于JavaScript中的=====,作者的看法是在必要的时候==会很好地改善程序。然而==判断规则比较复杂,可以总结出一些情况便于我们选择是否使用==

  • 如果一个比较的两个值之一可能是truefalse,避免==而使用===。
  • 如果一个比较的两个值之一可能是0/""/[],避免==而使用===。
  • 其他情况下,放心使用==。不仅安全,在许多情况下它可以简化你的代码并改善可读性。

变量

一个变量标识符必须以a-z,A-Z,$,或_开头。它可以包含任意这些字符外加数字0-9,但不能使用保留字。

变量作用域提升(var定义)和嵌套就不多说了。

Strict模式

让代码更严谨,同样可以选择用在全局或是函数中。

函数作为值

IIFE(立即执行函数)和闭包是JS中值得玩味的特性。除了使用()包裹,还可以用void打头开始一个IIFE。

闭包经常用来包装模块。

this指代和prototype

新的特性

填充(polyfill)和转译(transpile)

作用域与闭包

作用域

作用域与LHS,RHS。在非Strict模式下,如果到全局作用域还找不到变量,会在作用域内创建一个新的同名变量。在Strict模式下,不允许这种行为(意外地创建全局变量),此时会抛出ReferenceError,即找不到变量。如果找到了值,但是并不能对它做一些事情,就会抛出TypeError。

词法作用域

JavaScript使用词法作用域,即变量和作用域在编写代码阶段已经确定。JS引擎也可以在这个阶段针对作用域和变量对代码进行优化,但是eval()with()会在代码中动态改变作用域,从而使得引擎无法进行优化,使代码运行得更慢。在strict模式下,eval()的不安全用法(修改作用域)以及with()都是不允许使用的。

词法作用域是编写时的,而动态作用域(和this)是运行时的。词法作用域关心的是函数在何处被声明,但是动态作用域关心的是函数从何处被调用

this在JS中始终是运行时的,即根据运行时的调用情况有不同的值。在箭头函数中则是词法this的,即声明时决定。

块作用域

封装、匿名函数、IIFE。

for循环、if、while、switch等流程控制语句的{},都是假的块作用域,其中的内容都依附于外部的函数作用域。with(不建议使用),try catch,let,const可以形成新的块作用域。

在ES6到ES5的转译时,具有块作用域的代码,会采用try catch来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ES6
{
let a = 1;
console.log(a);
}


// ES5
try {
throw undefined;
} catch(a) {
a = 1;
console.log(a);
}

提升

  • 在代码被执行前,所有的声明,变量和函数,都会首先被处理。处理的只有“声明”,而没有“赋值”。
  • 函数提升优先于变量的提升
  • 后续的提升会覆盖此前的同名提升

闭包

闭包就是函数能够记住并访问它的词法作用域,即使当这个函数在它的词法作用域之外执行时。

循环加闭包会出现面试中的经典问题:

1
2
3
4
5
for (var i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}

上面的代码为啥不好用?

从之前关于作用域的讨论来看,每次setTimeout只是完成了函数声明,并丢进队列里而已。当定时器函数在其词法作用域外执行时,因为闭包的特点会保留有父级的作用域。而这5个函数都定义在同一个父级函数作用域内,对变量i的引用自然是同一个了。

1
2
3
4
5
6
7
for (var i=1; i<=5; i++) {
(function(j){
setTimeout( function timer(){
console.log( j );
}, j*1000 );
})( i );
}

有IIFE的加持,父级作用域现在变成了每个IIFE而非for循环所在的作用域。即每个变量i来自不同的独立作用域,自然就可以得到理想的效果了。

1
2
3
4
5
6

for (let i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}

不就是想要个块作用域嘛,使用let关键字后变量将不是只为循环声明一次,而是为每次迭代声明一次。每次都能得到一个新的块作用域,自然得到和IIFE一样的效果。

this与对象

this是什么

也许JS已经入门的前端程序员们早就对this在不同环境下的不同值烂熟在心。但可能没有想过这种情况的本质:上一部分提到的JS中的this是运行时的,和作用域完全不一样。

对比一下按照传统OOP理解下的JS代码,从不同的角度看,能进一步得到对this的认识:

1
2
3
4
5
6
7
8
9
10
function foo() {
var a = 2;
this.bar();
}

function bar() {
console.log( this.a );
}

foo(); //undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function foo(num) {
console.log( "foo: " + num );

// 追踪 `foo` 被调用了多少次
this.count++;
}

foo.count = 0;

var i;

for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9

// `foo` 被调用了多少次?
console.log( foo.count ); // 0 -- 这他妈怎么回事……?

虽然看上去很愚蠢,但是从词法作用域的角度去理解,是不是能更清楚看到JS中this的特殊之处。

this豁然开朗

根据上面的描述,this是根据调用点确定含义的。下面的4个规则,在准备JS面试的时候肯定都见过:

  • 默认绑定,独立函数调用。可以认为这种this规则是在没有其他规则适用时的默认规则。此时this指向全局对象,在strict mode下,this指向undefined。
  • 隐含绑定,调用点有一个环境对象,即作为函数方法,但是下面的情况下会回退到默认绑定,因为调用点实际位于独立函数内
    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
    function foo() {
    console.log( this.a );
    }
    var obj = {
    a: 2,
    foo: foo
    };

    var bar = obj.foo; // 函数引用!

    var a = "oops, global"; // `a`也是一个全局对象的属性

    bar(); // "oops, global"

    function foo() {
    console.log( this.a );
    }

    ========

    function doFoo(fn) {
    // `fn` 只不过 `foo` 的另一个引用

    fn(); // <-- 调用点!
    }

    var obj = {
    a: 2,
    foo: foo
    };

    var a = "oops, global"; // `a`也是一个全局对象的属性

    doFoo( obj.foo ); // "oops, global"
  • 显式绑定,callapply可以显式attach context到函数上,使用bind可以避免前面那种this丢失的情况。
  • new绑定,函数作为构造函数调用时,this指向即将返回的新对象。

从优先级上看,new > 硬绑定 > 隐含绑定 > 默认绑定。其中“new > 硬绑定”有趣的一点是,使用bind在第一个后的参数实际上会作为函数的默认入参(类似于函数柯里化),如下:

1
2
3
4
5
6
7
8
9
10
11
function foo(p1,p2) {
this.val = p1 + p2;
}

// 在这里使用 `null` 是因为在这种场景下我们不关心 `this` 的硬绑定
// 而且反正它将会被 `new` 调用覆盖掉!
var bar = foo.bind( null, "p1" );

var baz = new bar( "p2" );

baz.val; // p1p2

除了上面的规则,还有一些特例:

传递nullundefinedcallapplybind时,那么这些值会被忽略掉,取而代之的是默认绑定规则将适用于这个调用。单纯使用apply数组化输入参数(现在已经可以用[...foo]了)和bind柯里化函数时常用到。

不过,这么做还是有风险,建议用Object.create(null)创建的对象替代null,既能表示无意义的值,又能避免默认绑定的行为。

作者给出了软绑定的工具方法,提高了硬绑定的灵活性,又避免了默认绑定的问题。逻辑是在绑定时检查this是否是全局对象,如果是才使用输入的this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this,
curried = [].slice.call( arguments, 1 ),
bound = function bound() {
return fn.apply(
(!this ||
(typeof window !== "undefined" &&
this === window) ||
(typeof global !== "undefined" &&
this === global)
) ? obj : this,
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}

另外,前面提到的箭头函数具有词法this,等同在调用前声明self = this,再把self传入的效果。

对象

内建对象中,只有Date()是必须要使用new创建的。

对象的属性有两种访问方法:.操作符或[ ]操作符。不同的是.操作符后只能使用标识符兼容的属性名,[...]操作符后可以使用任何合理的UTF-8的字符串。另外,对象的属性名总是字符串,如果使用了其他类型值,会进行强制转换:

1
2
3
4
5
6
7
8
9
var myObject = { };

myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";

myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"

计算型属性名

ES6中新增了计算型属性名,允许使用表达式作为一个键名称,表达式用[ ]括起来。像下面这样:

1
2
3
4
5
6
7
8
9
var prefix = "foo";

var myObject = {
[prefix + "bar"]: "hello",
[prefix + "baz"]: "world"
};

myObject["foobar"]; // hello
myObject["foobaz"]; // world

深、浅复制与对象属性描述符(description),writableconfigurablegetOwnPropertyDescriptor()defineProperty()。在JS中,delete仅用于直接从目标对象移除该对象的(可以被移除的)属性,与释放内存并无直接关系。

Immutability

注意:所有这些方法创建的都是浅不可变性。也就是,它们仅影响对象和它的直属属性的性质。如果对象拥有对其他对象(数组、对象、函数等)的引用,那个对象的内容不会受影响,任然保持可变

属性描述符里的writableconfiguratable限制了对属性和属性值的修改。preventExtensions()方法可以防止对象被添加新属性。

  • seal() = configuratable: false + preventExtensions()
  • freeze() = seal() + writable: false

getter与setter

除了使用defineProperty外,可以直接用字面量的形式,通过get prop1()set prop1(val)的形式设置getter和setter。

for infor of

混合(淆)“类”的对象

有些语言(比如Java)不给你选择,所以这根本没什么 选择性 —— 一切都是类。其他语言如C/C++或PHP同时给你过程式和面向类的语法,在使用哪种风格合适或混合风格上,留给开发者更多选择。

类意味着拷贝。

当一个传统的类被实例化时,就发生了类的行为向实例中拷贝。当类被继承时,也发生父类的行为向子类的拷贝。多态也是拷贝行为的结果。

但是:

  • 第一,JavaScript并不会自动地 (像类那样)在对象间创建拷贝;
  • 第二,显式mixin只能复制函数或对象的引用,而不是自身。

正如我们在第四章讲解的,在 JavaScript 中,对于对象来说没有抽象模式/蓝图,即没有面向类的语言中那样的称为类的东西。JavaScript 只有 对象。

实际上,在所有语言中,JavaScript 几乎是独一无二的,也许是唯一的可以被称为“面向对象”的语言,因为可以根本没有类而直接创建对象的语言很少,而 JavaScript 就是其中之一。

在 JavaScript 中,类不能(因为根本不存在)描述对象可以做什么。对象直接定义它自己的行为。这里 仅有 对象。

总之,JavaScript中面向对象的部分和大多数OOP语言不完全一样。这使得在JS中模拟类编程将既累又会埋下很多坑。

原型

使用[[get]]查询属性时,也会在[[prototype]]链上寻找,因此修改对象属性的时候,应该注意属性遮蔽(即在[[prototype]]中找到)的情况。它会增加代码的复杂度和可读性,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var anotherObject = {
a: 2
};

var myObject = Object.create( anotherObject );

anotherObject.a; // 2
myObject.a; // 2

anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false

myObject.a++; // 噢,隐式遮蔽!

anotherObject.a; // 2
myObject.a; // 3

myObject.hasOwnProperty( "a" ); // true

for in循环中,同样注意用hasOwnProperty()排除[[prototype]]上的属性。

所有用constructor构建的对象都有所指向的prototype,而在prototype中的.constructor又会指回constructor。但是这个关系是可以被覆盖的。

原型继承

作者认为,JS中的对象是通过链接组织起来的。说是原型继承,实际上就是在两个原型间建立了[[prototype]]的关系。这个关系的建立方法很多,各有优劣。最简单的还是用ES5提供的Object.create()方法,对__proto__constructor等视而不见。它的polyfill像下面这样:

1
2
3
4
5
6
7
if (!Object.create) {
Object.create = function(o) {
function F(){}
F.prototype = o;
return new F();
};
}

另外,Object.create()第一个后面的参数可以用来声明对象属性描述符,不过用得不多。

总结

虽然这些JavaScript机制看起来和传统面向类语言的“初始化类”和“类继承”类似,而在JavaScript中的关键区别是,没有拷贝发生。取而代之的是对象最终通过[[Prototype]]链链接在一起。

相反,“委托”是一个更确切的术语,因为这些关系不是拷贝而是委托链接。

从这个角度去看new Foo()过程中发生的事,除了返回一个新的对象外,Foo()还会将这个对象和Foo.prototype链接起来(通过指定[[prototype]]),Foo.prototype和别的对象并没有本质区别。

行为委托

在上面一章提到,[[prototype]]是存在于对象内部的引用另一个对象的内部连接。当一个属性/方法引用在一个对象上发生,而这样的属性/方法又不存在时,这个链接就会被使用。在这种情况下,[[Prototype]]链接告诉引擎去那个被链接的对象上寻找该属性/方法。接下来,如果那个对象也不能满足查询,就沿着它的[[Prototype]]查询,如此继续。这种对象间的一系列链接构成了所谓的“原形链”。

其重要的实质全部在于被连接到其他对象的对象

下面是一段OLOO(链接到其他对象的对象)风格的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var Task = {
setID: function(ID) { this.id = ID; },
outputID: function() { console.log( this.id ); }
};

// 使 `XYZ` 委托到 `Task`
var XYZ = Object.create( Task );

XYZ.prepareTask = function(ID,Label) {
this.setID( ID );
this.label = Label;
};

XYZ.outputTaskDetails = function() {
this.outputID();
console.log( this.label );
};
// ...

它的特点在于:

  • 状态保留在委托者上
  • 避免[[prototype]]链上的重复命名
  • 行为委托用在内部实现,避免暴露在API的设计上

思维的转变

放弃传统OO思路在JS中的蹩脚实现(像下面这样),抓住[[prototype]]链接对象以及“原型链”的特殊性,可以让思路更加自然且符合JS的特点(像下面的下面那样)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Foo(who) {
this.me = who;
}
Foo.prototype.identify = function() {
return "I am " + this.me;
};

function Bar(who) {
Foo.call( this, who );
}
Bar.prototype = Object.create( Foo.prototype );

Bar.prototype.speak = function() {
alert( "Hello, " + this.identify() + "." );
};

var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" );

b1.speak();
b2.speak();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var Foo = {
init: function(who) {
this.me = who;
},
identify: function() {
return "I am " + this.me;
}
};

var Bar = Object.create( Foo );

Bar.speak = function() {
alert( "Hello, " + this.identify() + "." );
};

var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );

b1.speak();
b2.speak();

在这种委托的思路下,不存在严格的父子关系,甚至不存在继承和类的说法。全程通过Object.create()建立起对象和对象的联系,连new也是不建议使用的。

但是这种思路也有个明显的问题,“子类”没法定义“父类”的同名方法,因为整个程序建立在[[prototype]]联系的基础上,重复命名将会隔断连接。

作者并不推荐匿名函数的使用,认为:1,追踪调试栈困难;2,难以自引用;3,代码变得不好理解。这点上我是保留意见的。

类型自省

类型自省即instanceof,而这个操作符是依赖于[[prototype]]中的constructor属性的,这个属性除了不可枚举外,相较其他属性并没有特别之处。重写或者[[prototype]]的改变就可以改变它。因此,instanceof在很多情况下可能并不会那么可靠。

使用鸭子类型的类型自省就更加不可靠了。

在作者提出的OLOO范式中,采取isPrototypeOf()Object.getPrototypeOf()进行类型自省。

新的class关键字

为了便于“类”思维编程者,class可以说是一大福音。

  • 不再有.prototype的困扰
  • extends一键式继承
  • super对多态的支持
  • 语法上使用更加贴近OOP语言

但实际上,这里的class只是语法糖,它还是没实现从类(“蓝图”)到实例(“建筑”)以及从父类到子类的复制,还建立在[[prototype]]的基础上。原文给出了很多例子说明这点。

在传统面向类的语言中,你从不会在晚些时候调整类的定义,所以类设计模式不提供这样的能力。但是JS的一个最强大的部分就是它是动态的,而且任何对象的定义都是(除非你将它设定为不可变)不固定的可变的东西。

换句话说,class 好像在告诉你:“动态太坏了,所以这可能不是一个好主意。这里有看似静态语法,把你的东西静态编码。”

关于 JavaScript 的评论是多么悲伤啊:动态太难了,让我们假装成(但实际上不是!)静态吧。

最近看看要不要在网上学习下性能监测和告警的解决方案,加在项目里。已经调研了一下才发现,项目里已经用上Raven.js了。实际上,各大公司也都有自己的实现方式,除了sentry的Raven.js外,还有腾讯的badjs,淘宝的JSTracker,阿里巴巴的FdSafe,支付宝的saijs等。早在几年前,就已经有许多解决方案了。

异常监测和信息采集的需要实现的主要功能点包括:

  • 前端SDK实现包括错误拦截和监控,错误信息包装、信息上报、API设计等
  • 提供一个可视化的管理后台
  • 可以正确定位错误位置
  • 可以对上报的日志进行筛选、查询、聚类等操作
  • 可以用邮件、短信或集成在其他平台中通知开发者

从一个前端初学者的角度,下面更多聊一下前端SDK的细节。

前端SDK实现

前端实现上的技术重点有三:错误捕获和封装AJAX上报JSON字符串化参数

在raven-js的vendor目录下,引用json-stringify-safeTracekit。前者为了避免JSON.stringify中出现的循环引用的情况,下面主要介绍后者。

Tracekit

常见的方案就是拦截window.onerror方法,在做完自己的工作后,调用原来的window.onerror。自己的工作里包括对错误信息的同一美化和包装。raven.js在这里是借助Tracekit.js完成的。

Tracekit主要分为两部分,Tracekit.report()Tracekit.computeStackTraceWrapper()。前者主要用来绑定和解绑错误监听函数、拦截错误;后者主要用来格式化错误信息。

Tracekit.report()

report()里,整体的设计和基本的观察者设计模式一样,内部成员handlers保存所有的事件消费者,与事件处理函数相关的有四个:

  • subscribe(),绑定一个监听错误的函数,并在绑定第一个函数时替换原有的window.onerror
  • unsubscribe(),解绑一个监听错误的函数,需要提供函数的引用
  • unsubscribeAll(),解绑所有监听错误的函数,还原原有的window.onerror
  • notifyHandlers(),触发错误时,将处理过的错误分发给各handlers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function notifyHandlers(stack, isWindowError) {
var exception = null;
if (isWindowError && !TraceKit.collectWindowErrors) {
return;
}
for (var i in handlers) {
if (handlers.hasOwnProperty(i)) {
try {
handlers[i].apply(null, [stack].concat(_slice.call(arguments, 2)));
} catch (inner) {
exception = inner;
}
}
}

if (exception) {
throw exception;
}
}

另外,函数installGlobalHandler()uninstallGlobalHandler()就是上文中用来拦截window.onerror的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function installGlobalHandler() {
if (_onErrorHandlerInstalled) {
return;
}
_oldOnerrorHandler = _window.onerror;
_window.onerror = traceKitWindowOnError;
_onErrorHandlerInstalled = true;
}
function uninstallGlobalHandler() {
if (!_onErrorHandlerInstalled) {
return;
}
_window.onerror = _oldOnerrorHandler;
_onErrorHandlerInstalled = false;
_oldOnerrorHandler = undefined;
}

report()中最主要的函数是traceKitWindowOnError()。它的工作流程如下:

  1. 查看lastException是否有正在处理的error,如果有则说明是当前错误引起的,使用computeStackTrace.augmentStackTraceWithInitialElement()追加到当前的错误栈前。调用processLastException(),将lastException的信息交给handler处理,并将lastException置空。
  2. 如果lastException为空,且Error为错误对象,使用computeStackTrace()格式化错误信息,再交给错误消费者。
  3. 如果lastException为空,且Error不是错误对象(如字符串),则自行包装错误信息,交给消费者
  4. 使用原来的window.onerror()处理事件
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 traceKitWindowOnError(message, url, lineNo, colNo, ex) {
var stack = null;

if (lastExceptionStack) {
TraceKit.computeStackTrace.augmentStackTraceWithInitialElement(
lastExceptionStack,
url,
lineNo,
message
);
processLastException();
} else if (ex && utils.isError(ex)) {
// non-string `ex` arg; attempt to extract stack trace
stack = TraceKit.computeStackTrace(ex);
notifyHandlers(stack, true);
} else {
// 自行封装
// ...
notifyHandlers(stack, true);
}

if (_oldOnerrorHandler) {
return _oldOnerrorHandler.apply(this, arguments);
}
return false;
}

Tracekit.computeStackTraceWrapper()

这一部分主要由下面几个函数组成:

  • computeStackTraceFromStackProp(),处理Chrome和Gecko浏览器下的错误信息格式化
  • computeStackTraceByWalkingCallerChain(),处理IE和Safari浏览器下的错误信息格式化
  • augmentStackTraceWithInitialElement(),在当前错误栈底新增新的错误信息,用于computeStackTraceByWalkingCallerChain()和第一部分的processLastException()
  • computeStackTrace(),格式化错误栈信息

其中computeStackTraceFromStackProp()通过换行符得到stack信息,并通过正则格式化所需要的错误信息,computeStackTraceByWalkingCallerChain()是利用arguments.caller得到错误栈信息并格式化。

computeStackTrace()代码如下:

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
function computeStackTrace(ex, depth) {
var stack = null;
depth = depth == null ? 0 : +depth;

try {
stack = computeStackTraceFromStackProp(ex);
if (stack) {
return stack;
}
} catch (e) {
if (TraceKit.debug) {
throw e;
}
}

try {
stack = computeStackTraceByWalkingCallerChain(ex, depth + 1);
if (stack) {
return stack;
}
} catch (e) {
if (TraceKit.debug) {
throw e;
}
}
return {
name: ex.name,
message: ex.message,
url: getLocationHref()
};
}

除了Tracekit所做的工作外,raven本身也对console的log/warning/assert/error方法,setTimeoutsetInterval,requestAnimationFrame()以及各种事件handler进行了拦截。

这里有个坑,跨域的问题无法拦截错误,解决办法就是对跨域的script标签加入crossorigin属性,并在后台配置Access-Control-Allow-Origin=*

Raven

实际上,Tracekit本身已经完成对错误捕获和封装。Raven为了便于在管理后台展示和管理,进一步提出了DSN、context等设计。raven-js的源码主要在src/raven.js中。剩下两部分也是在其中实现的。下面分部分介绍一些:

DSN

DSN(Data Source Name)是Sentry对一个项目的定义。它由协议、端口、用户、密码、后台Sentry服务器地址、项目名组成。通过Raven.config()设置。在config()中通过正则匹配用户输入的DSN字符串,得到后台地址。

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
config: function(dsn, options) {
var self = this;

// ...
if (!dsn) return self;

var globalOptions = self._globalOptions;

// 设置全局参数
if (options) {
each(options, function(key, value) {
// tags and extra are special and need to be put into context
if (key === 'tags' || key === 'extra' || key === 'user') {
self._globalContext[key] = value;
} else {
globalOptions[key] = value;
}
});
}

self.setDSN(dsn);

// 屏蔽跨域的无效错误
globalOptions.ignoreErrors.push(/^Script error\.?$/);
globalOptions.ignoreErrors.push(/^Javascript error: Script error\.? on line 0$/);

// ...

// return for chaining
return self;
},

安装和卸载

install()uninstall()函数中完成。

install()中完成了下面的工作:

  • 借助Tracekit监听了全局的错误事件
  • 监听try catch和一些浏览器事件过程(如console,click,fetch等)中的信息
  • 安装插件
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
install: function() {
var self = this;
if (self.isSetup() && !self._isRavenInstalled) {
// 订阅所有错误事件
TraceKit.report.subscribe(function() {
self._handleOnErrorStackInfo.apply(self, arguments);
});

// 下方的函数会修改原回调函数
// 需要修改函数的toString方法
self._patchFunctionToString();

// 封装定时器和事件回调函数以提供更好的错误监控
if (self._globalOptions.instrument && self._globalOptions.instrument.tryCatch) {
self._instrumentTryCatch();
}

// 一些浏览器原生方法的封装,以捕获事件
// 设置里可关闭
if (self._globalOptions.autoBreadcrumbs) self._instrumentBreadcrumbs();

// 安装所有插件
self._drainPlugins();

// 更新状态
self._isRavenInstalled = true;
}

Error.stackTraceLimit = self._globalOptions.stackTraceLimit;
return this;
}

uninstall中还原了对浏览器原方法的修改,并卸载了Tracekit的report。

封装函数

相关函数:context()wrap()。完成的主要工作是对浏览器原生方法的拦截,使得能更好地捕获其中的错误,在对象内部使用。

capture相关

用来捕获事件,有三种用法。

  • captureException(),最典型的用法,借助Tracekit捕获页面的异常,之后进一步封装成frame后交给_send()发送
  • captureMessage(),最常用的用法,类似埋点,将信息封装成frame后交给_send()发送
  • captureBreadcrumb,类似captureMessage(),不过储存信息在this._breadcrumbs,并不交给_send()
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
captureException: function(ex, options) {
// ex不是错误时的处理
// ...

// Get actual Error from ErrorEvent
if (isErrorEvent(ex)) ex = ex.error;

// Store the raw exception object for potential debugging and introspection
this._lastCapturedException = ex;

// TraceKit.report will re-raise any exception passed to it,
// which means you have to wrap it in try/catch. Instead, we
// can wrap it here and only re-raise if TraceKit.report
// raises an exception different from the one we asked to
// report on.
try {
var stack = TraceKit.computeStackTrace(ex);
this._handleStackInfo(stack, options);
} catch (ex1) {
if (ex !== ex1) {
throw ex1;
}
}
return this;
}

值得注意的是captureMessage中可以设置rate,使一些消息不上报。白名单、正则过滤也是在这里完成的。captureException则是在_processException中完成的。

设置context

context包括三部分:

  • tags,用于从不同维度标识错误或信息,使用setTagsContext()全局配置
  • users,用于标识错误来源,使用setUsersContext()配置
  • extra,用来携带额外的信息,这部分信息不会被索引,使用setExtraContext()配置

它们都放在Raven._globalContext中。涉及的函数还有clearContext()getContext()

同时environmentrelease也放在Raven._globalContext中,可以通过setEnvironmentsetRelease设置

BreadCrumb

这部分功能是在_instrumentTryCatch_instrumentBreadcrumbs方法里实现的。它们通过重写原方法,捕获其中的错误和事件。在卸载时,通过restoreBuiltin还原。

发送

  • send()方法中,会使用封装好的数据附加上_globalOptions中的数据,附带浏览器的状态信息(_getHttpdata()中实现)之后交由_sendProcessedPayload()
  • _sendProcessedPayload()中,会裁剪过长的信息(message, stack, url, referer等)添加请求头,设置发送目标,传入成功和失败回调调用发送函数_makeRequest()
  • _makeRequest()中,为了跨域发送,会优先尝试fetch,然后尝试带有withCredentials字段的XMLHttpRequest,最后采用XDomainRequest对象发送。
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
_makeRequest: function(opts) {
// Auth is intentionally sent as part of query string (NOT as custom HTTP header) to avoid preflight CORS requests
var url = opts.url + '?' + urlencode(opts.auth);

if (supportsFetch()) {
return _window
.fetch(url, {
method: 'POST',
body: stringify(opts.data)
})
.then(function(response) {
if (response.ok) {
opts.onSuccess && opts.onSuccess();
} else {
// ..
}
})
['catch'](function() {
opts.onError &&
opts.onError(new Error('Sentry error code: network unavailable'));
});
}

var request = _window.XMLHttpRequest && new _window.XMLHttpRequest();
if (!request) return;

// if browser doesn't support CORS (e.g. IE7), we are out of luck
var hasCORS = 'withCredentials' in request || typeof XDomainRequest !== 'undefined';

if (!hasCORS) return;

if ('withCredentials' in request) {
request.onreadystatechange = function() {
if (request.readyState !== 4) {
return;
} else if (request.status === 200) {
opts.onSuccess && opts.onSuccess();
} else if (opts.onError) {
var err = new Error('Sentry error code: ' + request.status);
err.request = request;
opts.onError(err);
}
};
} else {
request = new XDomainRequest();
// xdomainrequest cannot go http -> https (or vice versa),
// so always use protocol relative
url = url.replace(/^https?:/, '');

// onreadystatechange not supported by XDomainRequest
if (opts.onSuccess) {
request.onload = opts.onSuccess;
}
if (opts.onError) {
request.onerror = function() {
var err = new Error('Sentry error code: XDomainRequest');
err.request = request;
opts.onError(err);
};
}
}

request.open('POST', url);
request.send(stringify(opts.data));
}

至此,错误捕获和封装AJAX上报JSON字符串化参数都已完成。

可视化后台

在自己设计异常监控系统时,需要和后台商量好接口的设定。用Express + React/Vue等方案快速搭建。

参考

构建工具升级

因为要拆分代码,便于管理,需要使用importexport,因此必须要引入webpack这样的打包工具到gulp中,使用webpack-stream,具体使用方法和其他的gulp插件类似,在pipe在这样插入就行了.pipe(webpack()),配置方式和webpack一样。(webpack中引入babel-loader的过程就不赘述了)引入webpack后,开发流程和一起类似,gulp启动测试服务器,使用webpack通过entry.js打包代码,CSS和JSON相关流程不变。release时,增加了minify的流程,让js流程后的代码再压缩一遍。

另外,引入babel后,可以用ES6语法改写gulpfile.js。最后的gulpfile.babel.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
48
49
50
51
52
53
54
55
56
57
58
59
import gulp from 'gulp';
import rename from 'gulp-rename';
import uglify from 'gulp-uglify';
import cleanCSS from 'gulp-clean-css';
import jsonminify from 'gulp-jsonminify';
import webserver from 'gulp-webserver';
import webpack from 'webpack-stream';

gulp.task('js', function () {
return gulp.src('src/index.js')
.pipe(webpack({
module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
]
}
}))
.pipe(rename("index.js"))
.pipe(gulp.dest('dist'));
});

gulp.task('minify', function () {
return gulp.src('dist/index.js')
.pipe(uglify())
.pipe(rename("index.min.js"))
.pipe(gulp.dest("dist"));
})

gulp.task('css', function () {
return gulp.src(['src/index.css'])
.pipe(cleanCSS({compatibility: 'ie8'}))
.pipe(gulp.dest('dist'));
});

gulp.task('json', function () {
return gulp.src('src/meta*.json')
.pipe(jsonminify())
.pipe(gulp.dest('dist'))
});

gulp.task('webserver', function() {
gulp.src('./')
.pipe(webserver({
livereload: true,
directoryListing: true,
open: true
}));
});

gulp.task('watch', function() {
gulp.watch(['src/*.js', 'src/**/*.js', 'src/**/*.vue'], ['js']);
gulp.watch('src/*.css', ['css']);
gulp.watch('src/*.json', ['json']);
})

gulp.task('assets', ['json', 'css', 'js']);
gulp.task('default', ['assets', 'webserver', 'watch']);
gulp.task("release", ['assets', 'minify']);

使用单文件组件

引入webpack后,开始高高兴兴地分模块拆分代码,却发现分组件使用Vue时,不是单纯地定义组件配置信息,然后传给入口组件就行。必须要引入全家桶,vue-loader等工具,文件不得不用.vue这样的形式组织(现在开始觉得React组件的组织比Vue舒服了)。本来使用单文件的形式就是想尽量精简,可随着功能逐渐健全,看来重构也是避免不了的啊。在vue-loader的介绍里,居然还要通过vue-cli来大一统,但是我一是想维持项目尽量轻量精简,使用gulp的工具链;二是项目已经写了很久了,全部迁移过去成本有些大。于是,通过vue-cli新建样本项目,对着package.jsonwebpack.config.js一抄了事。

加上种种.vue文件的相关配置,gulp.babel.js最后长下面这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ...
gulp.task('js', function () {
return gulp.src('src/index.js')
.pipe(webpack({
module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" },
{ test: /\.vue$/, loader: 'vue-loader'},
{ test: /\.(png|jpg|gif|svg)$/, loader: 'file-loader', options: { name: '[name].[ext]?[hash]' } }
]
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
},
extensions: ['*', '.js', '.vue', '.json']
}
}))
.pipe(rename("index.js"))
.pipe(gulp.dest('dist'));
});
// ...

下面需要开始拆分代码了。根据React/Vue这样框架通常的设计经验,需要下面一些组成:

  • actions 存储状态管理的动作
  • components 存储相互解耦的”dumb”组件,最好和业务无关
  • constants 存储全局常量
  • containers 存储组织components的业务容器组件
  • entry 存储入口文件
  • helper 存储工具函数
  • reducers 存储状态管理的reducers
  • settings 存储全局配置,通常用来初始化store
  • store 存储全局状态
  • templates 存储引入js的html文件

我的项目比较简单,一没有状态管理(后面复杂了之后可能会引入😂),二只有三个组件,只要上面的components, constants, containers, helper的就够用了。最后src下的文件目录大概像下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
├── App.vue
├── components
│   └── column
│   └── Column.vue
├── constants
│   └── index.js
├── containers
│   ├── info
│   │   └── Info.vue
│   └── wall
│   └── Wall.vue
├── helper
│   └── utils.js
├── index.css
└── index.js

在拆分时遇到了一些数据需要从最外层透传到子组件的情况,如res, tag_list, tag_keys。不过他们是只读的,而且数目很少,所以并不需要状态管理,只用通过props传下去就行了。

新功能

重构完之后,终于可以写新功能了。新功能主要是增加两个伪路由,方便页面的分享(这个需求我之前遇到过几次了)。页面是spa类型的,所以前端路由可以采用hash或history H5 API来实现。同时也有许多在这个基础上了前端路由库,提供一站式解决方案。我的需求目前其实不需要完整的路由方案:

  • 图片详情页可以分享
  • 搜索结果可以分享

因此,设计上使用hash的方案,对于图片详情页,用!开头,后接图片序号。对于搜索结果页,则没有开头的!,仅使用/隔开每一个搜索关键字。对hash的读写上,没有什么困难的地方:

  • App.vue根据hash注入对应的数据,更改默认视图,对于图片详情页,更改展示组件
  • :切换组件时,记录当前数据到location.hash,方便直接复制链接分享

后面的计划

现在网站还是有点单调了。只能自娱自乐,没有互动。后面应该会考虑在每张图片接入Disqus的问题。

《人类简史》是几年前的畅销书,早有耳闻。它不是对智人几万年来的历史事件像讲故事似的一个个娓娓道来,反而从生物学、社会学、宗教、经济、人文等各个方面提出了相当有深度的观点和见解,包括认知革命、农业革命、帝国与金钱、科技革命等方面的理解让我大开眼界。虽然了解了人类在这些方面的种种真相,对日常生活似乎也不会有什么改善,这并不妨碍它成为一本好书。总而言之,《人类简史》值得一读。

认知革命

智人不过是地球生物发展长河中的一种物种而已,与它类似的还有尼安德特人、丹尼索瓦人、梭罗人等其他人种。然而随着智人向地球的各个大陆蔓延时,这些人种却渐渐消失在历史长河中。在这个过程中,智人似乎发生了认知革命,在智人之前,许多动物都有自己的语言,作者认为不同的是,首先智人的语言能够传达更多的信息,从而执行复杂的计划;其次只有智人可以谈论并不真正存在的事物,同时相信一些不大可能的事情。“虚构”这件事的重点在于人类可以拥有想象,同时可以一起想象,编制出共同的虚构故事。这种“想象的现实”也是人类后面建立秩序的基础。

早期的智人(农业革命前)主要靠狩猎和采集生存,营群体游牧生存。不论从工作时间、生存本领还是幸福程度甚至都比现代人高。他们的营养充分、能够免受饥饿或营养不良的影响、由于不依赖单一作物,受自然灾害影响较小,也很少受传染病影响。在度过艰难的童年时光后,多半能活到不错的岁数。在这个阶段,智人的信仰为泛神论,即一切皆有灵,几乎任何一个地点,任何一个动物,任何一个植物都有自己的声明和情感。

随着智人向各个大陆迁移、在从大陆向太平洋岛屿迁移,这股毁天灭地的人类洪水带来了大批的动植物灭绝,尤其是大型陆地动物。

农业革命

作者认为,农业革命是史上最大的谎言,看上去是人类驯服的小麦和水稻,实际上却是小麦和水稻驯服了人类。农业革命包括农业作物的驯服(植物),包括小麦、水稻、玉米、马铃薯、豆类等,也包括动物的驯服,如牛、狗、羊、鸡等。主要发生在中东、中国、中美洲,原因很简单:大多数的动植物无法驯化,这些好驯化的动植物只生长在特定的地方

人类以小麦等为主食完全是巧合,人们发现在作物丰盛的情况下,终于不用再四处迁移,从而转而通过种植这些农业作物为生,永久聚落开始形成,生活方式的变化使得人类需要考虑自然灾害等的影响,需要预留多余的粮食,同时还要从早到晚照料这些作物免受其他物种侵袭。这似乎是一个陷阱,智人的身体构造其实并不是为了成天弯腰除草和挑水准备的,身体劳累的同时,传染病和暴力行为也威胁到人类的生存。人类以为自己再辛苦点,生活就会更好,等到群落规模达到上百上千人的时候才发现已经无法回头到以狩猎和采集为生的生活,因为此时只有这些农作物能养活这么多人。可受苦的不只是人类,农业革命中的动物也在饱受着痛苦,只因人类需要它们来过上更好的生活。

随着农民生产出来的食物越来越多,加上运输技术的成熟,住在一起的人越来越多,渐渐形成村落和城镇,这个速度快到人类根本没有进化出让这么多人协同工作的本能。幸运的是,智人是个可以讲“谎话”和相信“谎话”的物种。于是一系列“虚构的故事”帮助人类建立稳定的秩序。同时为了保证秩序不会崩塌,还需要暴力的维持。除了暴力还需要一些真正的信徒。为了做到这一点,

  • 第一,对外的说法绝对要坚持它们千真万确、绝非虚构,并永远强调下去
  • 第二,在教育上也要彻底贯彻同一套原则,要在一切事物中融入这套由想象建构出的秩序

最后,想象构建的秩序深深与真实的世界结合,想象构建的秩序塑造了我们的欲望,想象出的秩序存在于人与人的思想连接。作为人类,我们已经不可能摆脱想象建构出的秩序,因为为了让所有人都摆脱,我们需要想象出更强大的东西,从而只是换了种想象而已。即使最后真正摆脱了这种秩序,所谓犬儒主义者,将不可能建立起稳定的帝国。

记忆过载

随着人口规模越来越大,“虚构的故事”越来越多,秩序也越来越复杂。下面几个原因催生出新的表达方式:

  1. 人类并不像蜜蜂,将秩序深植在基因之中,随着人死亡,秩序并不会遗传下去
  2. 大脑容量有限,储存不了这么多信息
  3. 人类大脑的演化并不是为了储存抽象信息设计的,只习惯于存储动植物、地形等具体信息

于是,数字和文字逐渐形成,让“虚构的故事”能够更久地传承下去,文字也从记录这些秩序逐渐发展到完备的可以表达日常的生活。在管理机构里,专门有人记忆这些秩序,便于随时取出使用。文字对人类的重要影响是,逐渐改变了人类思维和看待这个世界的方式,从过去的自由连接、整体思考到分割思考、官僚主义。而数字符号更是如今程序语言的基石。

历史从无正义,这些“想象的故事”并不公平,总把人分成一些其实并不存在的分类,上层人享有各种权力和特权,下等人有的只有歧视和压迫,阶级由此产生。有趣的是,大多数人都会认为只有自己社会的阶级才是自然的,而其他社会的阶级分法实在虚伪。不幸的是,复杂的人类社会似乎确实需要这种由想象建构出来的阶级制度和歧视,而这种歧视往往会造成恶性循环(被歧视->没有发展机会->缺乏教育、贫穷->被歧视),成为底层人士被歧视的所谓客观道理。

关于性别歧视和偏见,许多人认知的所谓“自然”和“不自然”并不是生物学的概念,而是人为(如基督教神学)想象出的规则,指的是“符合创造自然的神的旨意”。各种规定男人就该如何、女人就该怎样的法律、规范、权利和义务,反映的多半只是人类的想象,而不是生物天生的现实。

人类的融合统一

农业革命后,人类社会规模变得更大,维系社会秩序的想象故事也更为精致。人类从出生到死亡被各种虚构的故事和规则围绕,人们往往遵照着这种人造而非天生的直觉,这种直觉就叫做“文化”。原本整个世界是可以大致划分成多个相互隔离并具有一定规模的世界的。随着亚非世界的发展,不断吞噬了其他世界。如今,几乎所有人类都认同同一套地缘政治体系,即地球被划分出不同的国家;使用同一套经济制度,采用同一套法律制度和科学体系。有三种秩序促成了现在的大统一:金钱、帝国、宗教。

农业革命后,随着城镇和王国的出现,人类的分工越来越细,从前一个部落内就可以完成的需求,如今可能需要找完全的陌生人。以物易物的交易有诸多的问题,某些社会采用集中的以物易物系统,但是大多数社会通过发明“钱”的概念解决。这里的“钱”完全是一个概念,存在于人们共同想象中的概念。金钱不仅能够交换物品,还能用来积累财富,相比农作物等,金钱要好储存多了,同时也更便于携带。金钱在转换、存储、运输上的优势,造就了现如今如此复杂的商业系统。可以说,“金钱是人类有史以来最成功的互信系统”。

帝国是一种政治秩序,往往统治许多民族,有着灵活的疆域。它对人类最大的益处在,四处征服、掠夺财富后,不只是拿来养活军队,同时也赞助了艺术、科学、司法等的发展。全球的帝国有个共同点就是,在征服它国时,都带着“征服你们是为你们好”,“统治全世界,为全人类带来福祉”的动机,与排外相反,帝国展现更多的是包容。在帝国的统治下,帝国中的“它们”也渐渐认同了“我们”的概念,和帝国融为一体。在公元前200年左右,大多数人已经活在各个帝国之下。现如今,似乎要形成一个全球性的帝国。

宗教的特点有二:1. 认为世界上有一种超人类的秩序,2. 宗教会以此发展出具有约束力的规范和价值观。只有具有普世特征推广特质的才算得上是宗教。偶像崇拜发展大致是从泛神论到多神论再到一神论。多神论认为世界由一群神威浩荡的神灵统治,而主宰世界的最高权利不带有私心和偏见,在需要时,和特定的神进行交易就可以得偿所愿。随着时间推移,许多多神论者开始对自己信仰的某位神灵越来越虔诚,也慢慢偏离多神论概念,形成一神论。一神论者通常认为自己信奉的神就是唯一的神,自然会批评其他的神不可信,这点不同于多神论。

二元论宗教为了解决“恶的难题”,即为什么世界会有苦难,人为什么会犯错,应运而生。对二元论者来说,掌握世界的不是一个无所不能的神,还有不受控制的恶。佛教认为人遇到事情会产生欲念,欲念会造成不满,释迦牟尼认为重点要看清事情本质,而不是它带来的感受,在专注于实际感受而非欲求和幻想时,原来的欲求就换来了圆满和寂静,称为涅槃。

后世的崇拜对象由神变成了人或是智人的群体,从而产生了人文主义,而根据人文主义者对人性定义的不同,又有自由人文主义、社会人文主义等。

科学革命

现代科学和以往的知识体系最大的不同在:

  • 愿意承认自己的无知
  • 以观察和数学为中心
  • 通过科学获得新能力(即技术)

但是,若是假设我们并非无所不知,现有知识也非全对,那这也适用于那些让数百万人得以有效合作的虚构故事,社会秩序岂不是要崩溃。因此,要维持社会政治秩序稳定,只能依靠不科学的方法,别无选择。毕竟科学还是要依靠宗教、意识形态才能获得经费,并让研究正当化。

在精确科学的趋势下,连生物学、经济学、社会学、心理学等学科也得依靠上数学工具,从而发展出数学的新分支——统计学。知识就是力量,意在考验科学的一大标准就是能否应用于实践。不能免俗的是,科学活动也像其他文化活动一样,收到经济、政治和宗教利益的影响,科学研究一定得和某些宗教或意识形态联手,才有蓬勃发展的可能。

近现代,在欧洲,帝国带着征服的心态不断扩张。同时,不得不提的说资本主义的发展来自对未来的信任,信用->贷款->发展->回报->信用的良性循环,现代经济建立在信任的基础上,使得饼越来越大,有钱也不是一件可耻的事。然而上面循环的一个基础就是:生产的利润必须再投资于提高产量。这也是资本主义的原则,因此即使你是某个辛苦的工人,把收入的一部分拿出投资股票,你也算是个资本主义者。资本主义认为,经济发展就是至善。欧洲的商业型帝国就建立在资本主义的基础上,毕竟靠投资提高收入要好于缴税。

哥伦布探索新大陆的举动,催生了帝国资本主义的奇妙循环:信贷->新发现->殖民地->利润->信任->信贷。然而探险也是有风险的,股份有限公司随着产生,一次探险失败所有股东分担,避免血本无归。荷兰、英国等国家在此基础上,渐渐采用出售股份的方式,让部分债权人也能享有部分获利,这种股票的转手和买卖大行其道,从而催生了证券交易所,专门进行股票交易。以至于后面的战争也可能成为商品的一种。

伴随科技革命的是工业的发展壮大。蒸汽机的出现前,只能靠人体来进行能量间的转化,学会驾驭和转化这些能量的同时,人类可以使用的能源也大大提升。工业催生了冷漠和传送带,一切都以产量为标的。与此同时,产生了全新的问题:谁来买这些产品?

为了避免这种灾难,一种伦理观便被发明出来:消费主义。有史以来,人们生活窘迫,多以勤俭为口号。然而消费主义的没得就是消费更多的产品和服务,鼓励所有人善待自己,宠爱自己,即使因此走上绝路,也在所不惜。在传统农业社会,饥荒的阴影挥之不去,而今天,肥胖却成为一大健康问题。资本主义和消费主义的伦理可以分为两面:

  • 有钱人的指导原则是——“投资”
  • 其他人的指导原则是——“购买”

现代生活

随着工业的出现和发展,时刻表和生产线的概念随之出现,人们关心上下班时间,列车也开始设计列车时刻表,时间变得越来越精细。1847年,英国的火车业者齐聚一堂,同意以格林尼治天文台的时间为准,协调火车时刻表。最终在1880年,英国成为是一个统一全国时间的国家。而现代的一切都得按时完成,时间无所不在。

工业革命后个人的力量逐渐摆脱家庭和社群,个人的命运不再完全由家庭长辈或是家族左右,人类的生活和思考方式也不再预设自己属于社群。家庭和社群力量减弱的同时,国家和市场力量变强,从而进一步使得个人不避依赖社群生存。如今,社群更多是发挥一些重要的情感功能。现代兴起了两大想象社群——“民族”和“消费大众”,前者是国家的想象社群,而后者是市场的想象社群。后者是指消费者们可能彼此并不认识,却因为相同的消费习惯和兴趣成为“同一伙人”(如粉丝)。

过去两世纪,社会秩序变动不休。狄更斯曾在法国大革命时说过:“这是最好的年代,也是最坏的年代”。这句话同样可以用于现在。

快乐并不存在于任何财富、健康和社群之类的客观条件,而是客观条件和主管期望之间是否相符。所谓的快乐更可能只是让个人对意义的错觉和现行的集体错觉达成同步而已。

0%