--愿你内心有种不灭的火焰,将你与别人区分开来--

0%

RxJS初识

Rxjs 是 Reactive Extension 这种模式的 JavaScript 语言实现,通过学习了解 Rxjs 你将打开一扇通往全新编程风格的大门。

Rxjs 可以很大程度上可以解决很多困扰我们的问题:

  • 如何控制大量代码的复杂度
  • 如何保持代码可读
  • 如何处理异步操作

不过 Rxjs 的学习曲线非常陡峭,可以说是断崖式的学习路径。

可以把RxJS想象成为非同步的Loadsh

RxJS是基于观察者模式和迭代器模式以函数式编程思维来实现的,RxJS可以很好的解决异步和事件组合的问题。

它的两个核心的基本概念: OberservableObserverObservable作为被观察者,是一组值或者事件的流的集合。 而Obsever作为观察者,根据Observable进行处理,它们关系如下:

  • 订阅: Observer 通过ObservableSubcribe方法订阅Observable.
  • 发布: Observable 通过回调next方法向Observer发布事件。

创建 Observable

RxJS中,我们可以通过多种方式来创建一个Observable对象:

  • create
  • of
  • from
  • fromEvent
  • fromPromise
  • never
  • empty
  • throw
  • interval
  • timer

下面通过Observable.create()方法来创建Observable:

1
2
3
4
5
6
7
8
9
10
11
let observable = Observable.create(function (observer) {
// 创建 Observable 对象
observer.next('jack')
// 通过 next() 方法向 Observer 中发布数据
observer.next('mark')
})

observable.subscribe((value) => console.log(value))
// 订阅 Observerable
// jack
// mark

创建 Observer

观察者是一个包含三个方法的对象,每当Observable出发事件时,便会自动调用观察者对应的方法。

Observer的接口定义:

1
2
3
4
5
6
7
8
9
10
interface observer<T>{
// 标志是否已经取消对 Observable 对象的订阅
closed?: boolean;
//每当 Observable 发送新值的时候,next 方法会被调用
next:(value T) => void;
//当 Observable 内发生错误时,error 方法就会被调用
error:(err:any) => void;
// 当 Observable 数据终止后,complete 方法会被调用。在调用 complete 方法之后,next 方法就不会再次被调用
complete:() => void;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建被观察者
let observable$ = Observable.create((observe) => {
observe.next('Jack')
observe.next('mark')
observe.complete();
observe.next('haha')
})

// 创建观察者
let observe = {
next: (value) => console.log(value),
error: (error) => console.log(error),
complete: () => console.log('done')
}

observable$.subscribe(observe)

Subscription

Subscription 用来取消定义的资源。有些时候对于一些 Observable 对象 (如通过 interval、timer 操作符创建的对象),当我们不需要的时候,要释放相关的资源,以避免资源浪费。

1
2
3
let source$ = Observable.timer(1000,1000);
let subscription = source$.subscribe((value) => console.log(value))
setTimeout(() => subscription.unsubscribe(),10000)

pull VS push

pullpush 是生产者/消费者之间数据传输的两种不同的体系。

pull

pull 体系中,消费者决定何时从数据生产者那里获取数据。而生产者本身并不知道数据什么时候会发送个消费者。

JavaScript 中在函数就是一个 pull 的体系,函数是数据的生产者,负责产生数据(return 一个返回值),然后当调用该函数的时候,消费其所返回的值。

pull VS push

如上图所示,调用 iterator.next()x 是消费者,在 pull 体系中。

push

push 体系中,数据的生产者决定何时将数据发送给消费者,消费者不会在接收数据之前意识到它将要接收这个数据。

而 JavaScript 的 事件系统 就是一个 push 体系,比如我们通过 addEventListener 去监听 bodyclick 事件,当 click 被触发的时候便会便会将数据 event 对象传递给消费者,即是 addEventListener 中的处理函数。

git 的 pull 和 push 机制。

一个更加形象的理解是,git 的 pull 和 push 机制。

pull 时 ,本地是消费者,远程是生产者,当我们要更新本地代码的时候,总是会手动的向务器拉取代码;
push 时是本地是生产者,远程是消费者,远程不知道我们何时会产生新的数据(代码)推送给它。

Observable VS Promise

当然还有 Promise,他算是 JavaScript 中最常见的 push 体系了。一个 Promise (数据的生产者)发送一个 resolved value (成功状态的值)来执行一个回调(数据消费者),但是不同于函数的地方的是:Promise 决定着何时数据才被推送至这个回调函数。

而 Rx 中的 Observable(被观察对象) 是一个全新的 push 体系, Observable 是基于 推送(Push)运行时执行(lazy)的多值集合。.

ref

Rxjs 从观察者模式到迭代器模式系统

Webpack3.x 构建 Vue

0x00 相关依赖及版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"devpendencies": {
// 生产环境依赖
// npm install node_modules_name --save-prod
// yarn add node_modules_name
"vue": "^2.5.2",
"vue-router": "^3.0.1",
"vuex": "^3.0.1"
},
"devDependencies": {
//开发环境依赖
// npm install node_modules_name --save-dev
// yarn add node_modules_name --dev/-D
"url-loader": "^0.5.8",
"vue-loader": "^13.3.0",
"vue-style-loader": "^3.0.3",
"vue-template-compiler": "^2.5.2",
"webpack": "^3.6.0",
"webpack-merge": "^4.1.0"
}

!> 值得注意的是 vue-template-compilervue-loaderpeerDependencies,为了保证 vue-template-compiler 必须与 vue 版本保持一致,必须由用户来指定版本进行安装。 

关于 dependencies/devDevpendencies/peerDependencies 可参考:聊聊 node.js 中各种 dependency

0x01 webpack 多环境配置


webpack 多环境配置的方案是:先有一基本配置文件webpack.base.config.js然后使用webpack-merge合并这个基本配置和针对特定环境下的配置文件,比如webpack.dev.config.jswebpack.prod.config.js

webpack.base.config.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
60
61
62
63
64
65
66
67
68
69
70
71
// 基础配置
const webpack = require('webpack')
const path = require('path')
const utils = require('./utils')

function resolve(relPath) {
return path.resolve(__dirname, relPath)
}

module.exports = {
entry: {
main: resolve('../src/main.js')
},
output: {
filename: 'js/[name].js'
},
resolve: {
extensions: ['.js', '.vue', '.styl', '.stylus', 'pug'],
modules: [path.resolve(__dirname, '../node_modules')],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('../src'),
'router': path.resolve(__dirname, '../src/router'),
'plugins': path.resolve(__dirname, '../src/plugins'),
'store': path.resolve(__dirname, '../src/store'),
'views': path.resolve(__dirname, '../src/views'),
'util': path.resolve(__dirname, '../src/util'),
'theme': path.resolve(__dirname, '../src/util')
}
},
module: {
rules: [
{
test: /\.js$/,
use: "babel-loader",
include: [resolve('../src')]
},
{
test: /\.vue$/,
use: {
loader: "vue-loader",
options: utils.vueLoaderOptions()
}
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: [
{
loader: "url-loader",
options: {
limit: 10000,
name: 'images/[name].[hash:7].[ext]' // 将图片都放入images文件夹下,[hash:7]防缓存
}
}
]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: [
{
loader: "url-loader",
options: {
limit: 10000,
name: 'fonts/[name].[hash:7].[ext]' // 将字体放入fonts文件夹下
}
}
]
}
]
}
}

webpack.base.config.jsvue-loaderoptions提取出来,其主要是用来配置CSSCSS预处理语言的loader,开发环境可以不用配置,但是生产环境需要提取CSS、增加postcss-loader等,
因此需要提取出来针对不同环境返回相应的options

webpack.base.config.js不配置CSSloader的原因和vue-loader一样。

同样考虑到各种环境的差异性以及个性配置,将pathpublicPath放在各自的环境中配置。

webpack.dev.config.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
 // 开发配置
const webpack = require('webpack')
const merge = require('webpack-merge')

const HtmlWebpackPlugin = require('html-webpack-plugin')
const baseWebpackConfig = require('./webpack.base.config')
const utils = require('./utils')
const config = require('./config')

// 多入口热更新
// Object.keys(baseWebpackConfig.entry).forEach((name) => {
// baseWebpackConfig.entry[name] = [
// `webpack-dev-server/client?http://localhost:${config.dev.port}/`,
// 'webpack/hot/dev-server'
// ].concat(baseWebpackConfig.entry[name])
// })

module.exports = merge(baseWebpackConfig, {
devServer: {
// 热更新配置
hot: true,
quiet: true, // 保证 friendly-errors-webpack-plugin 生效
port: config.dev.port,
open: true,
noInfo: true,
publicPath: config.dev.publicPath
},
output: {
path: config.dev.path,
publicPath: config.dev.publicPath
},
module: {
rules: utils.styleLoaders()
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"development"',
'__ENV__': true,
'development': true,
}),
new webpack.HotModuleReplacementPlugin(),
// HtmlWebpackPlugin 会自动将生成的js代码插入到 index.html
new HtmlWebpackPlugin({
title: 'liangxiang',
// filename 是相对于 webpack 配置项 output.path(打包资源存储路径)
filename: './index.html',
// template 的路径是相对于webpack编译时的上下文目录,就是项目根目录
template: './index.html',
inject: true
})
]
})

config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const path = require('path')

module.exports = {
dev: {
path: path.resolve(__dirname, '../static'),
publicPath: '/',
port: 8000
},
test: {

},
prod: {
path: path.resolve(__dirname, '../dist'),
publicPath: '/static/'
}
}
  • path

打包后js、css、image等存放的目录; Webpack 2+ 要求output.path必须为绝对路径。

  • publicPath

静态资源文件夹路径, 如果不配置,则默认为 /, 在实际项目中,静态资源一般集中放在一个文件夹下, 比如static目录,那么这里就应该改成publicPath: '/static/', 相应的index.html中引用的js也要改成src="/static/build.js"publicPath可以解释为最终发布的服务器上build.js所在的目录, 其他静态资源也应当在这个目录下。

util.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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const isProd = process.env.NODE_ENV === 'production'

const cssLang = [
{
name: 'css',
reg: /\.css$/,
loader: 'css-loader'
},
{
name: 'stylus',
reg: /\.stylus$/,
loader: 'stylus-loader'
},
{
name: 'stylus',
reg: /\.styl$/,
loader: 'stylus-loader'
}
]

function genLoaders (lang) {
let loaders = ['css-loader', 'postcss-loader']
if (lang.name !== 'css') {
loaders.push(lang.loader)
}

if (isProd) {
// 生产环境需要提取 css
loaders = ExtractTextPlugin.extract({
// 提取 CSS
// 样式解析,其中css-loader用于解析,而vue-style-loader则将解析后的样式嵌入js代码
// 也可以使用如下的配置
// {
// test: /\.css$/,
// use: ["vue-style-loader", "css-loader"]
// }
// 可以发现,webpack的loader的配置是从右往左的,从上面代码看的话,就是先使用css-loader之后使用style-loader
// webpack1 loader 后缀可以不写, webpack2 则不可省略
// { test: /\.css$/, loader: 'vue-style!css' }
use: loaders,
allChunks: true, // extract-text-webpack-plugin 默认不会提取异步模块中的 CSS,需要加上配置,将所有额外的chunk都压缩成一个文件
filename: "css/[name].[contenthash].css"
})
} else {
// 开发环境需要 vue-style-loader 将 css 提取到页面头部
loaders.unshift('vue-style-loader')
}

return loaders
}

exports.styleLoaders = function () {
const output = []
cssLang.forEach(lang => {
output.push({
test: lang.reg,
use: genLoaders(lang)
})
})

return output
}

// vue-loader 的 options
exports.vueLoaderOptions = function () {
const options = {
loaders: {}
}

cssLang.forEach(lang => {
options.loaders[lang.name] = genLoaders(lang)
})

return options
}

关于webpack对样式文件的处理有篇不错的参考:webpack中关于样式的处理

关于使用import还是require引入CSS文件,实际应用中并不推荐使用import引入css,参考:Getting postcss to process imported css

webpack.prod.config.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
// 生产环境
process.env.NODE_ENV = 'production'
const webpack = require('webpack')
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const baseWebpackConfig = require('./webpack.base.config')
const uitls = require('./utils')
const config = require('./config')

module.exports = merge(baseWebpackConfig, {
output: {
path: config.prod.path,
publicPath: config.prod.publicPath
},
module: {
rules: uitls.styleLoaders()
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"',
'__ENV__': false
}),
new webpack.optimize.UglifyJsPlugin({
// UglifyJsPlugin 它既可以压缩js代码也可以压缩css代码。
// 开启代码压缩
compress: {
// 去除代码注释
warnings: false
}
}),
new ExtractTextPlugin({
filename: 'css/style.css?[contenthash:8]'
}),
new HtmlWebpackPlugin({
title: 'ahah',
filename: 'index.html',
template: 'index.html',
inject: true
})
]
})

cli 启动

1
2
3
4
5
// package.json
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server --progress --config build/webpack.dev.config.js",
"prod": "cross-env NODE_ENV=development webpack --progress --config build/webpack.prod.config.js",
}

参考资料

webpack使用优化
从零搭建 vue2 vue-router2 webpack3 工程
Webpack 打包优化之速度篇
使用happypack时loaders的配置方式
webpack优化
webpack打包分析与性能优化

函数柯理化及其应用

高级阶数

高阶函数 (一个函数的参数是函数,或是将函数作为返回值的函数) 在 Javascript 中随处可见(比如数组中的 forEach(),sort(),map()……),比起普通函数,高阶函数要灵活太多。除了传统意义上的函数调用返回,还形成了一种后续传递的风格(Continuation Passing Style) 的结果接收方式。

函数柯里化

Currying 是函数式编程的一个特性,我们可以将多个参数的处理转化成单个参数的处理,类似链式调用。同时柯里化也是一个逐步传参,逐步缩小函数的适用范围,逐步求解的过程。

柯里化有3个常见作用:1. 参数复用;2. 提前返回;3. 延迟计算/运行。

普通的函数求解过程是这样的:

1
2
3
function commFn(a,b,c){
return a + b + c;
}

而函数柯理化是分部求解,先传一个a参数,再传一个b参数,再传一个c参数,最后将这三个参数相加:

1
2
3
4
5
6
7
function fn(a){
return function(b){
return function(c){
return a+b+c
}
}
}

参考:

下面是柯理化的基础版本。

柯理化基础版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function currying (fn){
var args = Array.prototype.slice.call(arguments, 1);
// 获得最外层函数的参数,fn 不算在内
return function(){
var innerArgs = Array.prototype.slice.call(arguments)
// 获得内层函数的参数
var finalArgs = args.concat(innerArgs);
// 组装外层函数和内层函数的参数
return fn.apply(null, finalArgs);
};
};

function add() {
let result = 0
Array.from(arguments).map(item => result+=item)
return result;
}

let curredAdd = currying(add,4,5,6)(1,2,3);//21

那么,进一步,如果我们想传任意多个参数,而当不传参数时输出结果呢?下面是柯理化的延迟计算版本

柯理化任意多参数延迟计算版本

1
2
3
4
5
6
7
8
9
10
11
function currying(fn) {
let args= [].slice.call(arguments,1)
return function A () {
let innerArgs = [].slice.call(arguments)
args.push(...innerArgs)
if (innerArgs.length === 0){
return fn.apply(null,args)
}
return A
}
}

假如,我们有一个 add 函数,并对其进行柯理化:

1
2
3
4
5
6
7
8
9
10
function add() {
let result = 0
Array.from(arguments).map(item => result+=item)
return result;
}

let curredAdd = currying(add);
curredAdd(100,200)(300);
curredAdd(400)
console.log(curredAdd())

curredAdd 是柯里化了的函数,它返回一个新的函数,新的函数接收可分批次接受新的参数,延迟到最后一次计算。我们可以任意传入参数,当不传参数的时候,输出计算结果!

类似函数柯里化等 高阶函数 (一个函数的参数是函数,或是将函数作为返回值的函数),在 Node 的异步编程中十分常见;而且,函数柯里化也是 ES6 的函数尾调用优化的技术基础。

函数绑定 bind 方法

除此以外,函数柯里化还常作为函数绑定的一部分包含在其中,以构造出更为复杂的绑定函数:

1
2
3
4
5
6
function bind(fn, context){
var args = Array.property.slice.call(arguments, 2);
return function (){
return fn.apply(context, args);
}
}

其实 ECMAScript5 已经函数定义了一个原生的 bind() 方法,我们可以直接使用它。

JavaScript 中的函数柯里化和函数绑定提供了强大的动态函数创建功能,它们都能用于创建复杂的算法和功能,但是两者都不应滥用,因为每个函数都会带来额外的开销。

类型检测

关于 typeof

typeof使用以及null和undefined的判断区分

阮一峰的网络日志

1.typeof 是一个运算符而不是一个方法;

2.使用 typeof 操作符检测数据类型只会返回七个结果;

string,number,undefined,function,object,Boolean,Symbol,注意 typeof 可以检测 function 类型,但是当检测类型是 nullObject 时都是返回object

3.使用 typeof 检测未实例化的对象(想对于检测原型的构造函数)返回function;

1
2
3
4
5
console.log(typeof  Object)
console.log(typeof Date)
console.log(typeof Error)
console.log(typeof Math)
// function

4.使用 typeof 检测 new 以后的实例,除了 new Function 返回 function 外,其它均返回 object ,包括自定义的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function test (){       }
var obj = new test();
console.log(obj.constructor);//function test(){}
console.log(obj);//test{}
console.log(typeof obj);//Object

var obj1 = new Array();
console.log(obj1.constructor);//function Array() { [native code] }
console.log(obj1);//[]
console.log(typeof obj1);//Object

var obj2 = new Function();
console.log(obj2.constructor);//function Function() { [native code] }
console.log(obj2);//function anonymous() {}
console.log(typeof obj2);//function

5.使用 typeof 检测 BOM 对象返回 object;

1
2
3
4
5
console.log(typeof window)
console.log(typeof navigator)
console.log(typeof screen)
console.log(typeof history)
console.log(typeof location)

插播一个 null 和 undefined 的区别

1.null 是一个表示“无”的对象,转换为数字的时候值为0,其典型应用场景是:

  • 用来初始化一个变量,该变量未来可能被赋值成一个对象
  • 用来和一个已经初始化的对象进行比较,用来和一个已经初始化的对象进行比较,这个变量可以是一个对象,也可以不是一个对象
  • 当函数的参数期望是对象时,被用作参数传入
  • 当函数返回值期望是对象时,被当做返回值输出
  • 删除事件绑定,事件本身是一个 null ,是一个空的对象,可以添加
  • 作为对象原型链的终点

如何判断一个变量是 null:

1
2
3
4
5
var exp =  null;
if(typeof(exp) == 'object' && exp == null){
// 因为 undefined == null 返回 true
console.log("该变量为空")
}

2.undefined是一个表示“无”的原始值,转换为数值的时候为0.

  • 变量被声明了,但是没有赋值,那么该变量的值就是undefined
  • 调用一个函数的时候,如果应该提供的参数没有提供,那么该参数默认是undefined
  • 如果一个对象的属性没有赋值,那么该属性值为undefined
  • 函数没有返回值的时候,默认返回undefined;

如何判断一个变量是 undefined:

1
2
3
4
var exp = undefined;
if (typeof(exp) == 'undefined') {
console.log("该变量是undefined")
}

关于 instanceof

我们先试着回忆片刻,我们知道,在 JavaScript 中对基本类型值的类型判断要用 typeof 操作符,对引用类型的判断要用 instanceof 操作符。typeof 的问题在在于无法判断除了原始类型值以外的其它对象类型。而 instanceof 操作, 只是检测一个构造函数的原型对象所指向的对象是不是在被检测对象的原型链上。它检测的是一个原型对象是否在原型链上,至于原型对象内容是什么它不管。instanceof 的缺点是存在多个全局作用域(比如,一个页面中有多个框架)的问题。此外,由于 JSON 的存在,在检测某个对到底是原生对象还是开发人员自定义的对象时也有问题。

针对上述问题,都有一终极解决办法,就是 在任何值上调用 Object 原生的 toString() 方法,该方法会返回一个 [object NativeConstructorName] 格式的字符串。而每个类在内部都有一个 [[Class]] 属性,它指向上述字符串中的构造函数名。

利用这一点,可以创建以下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 检测数组类型
function isArray(value){
return Object.prototype.toString.call(value) == "[ object Array]";
}

// 检测检测是否是原生函数
function isFunction(value){
return Object.prototype.toString.call(value) == "[ object Function]";
}

// 检测正则表达式
function isRegExp(value){
return Object.property.toString.call(value) == "[object RegExp]";
}

其实,还有很多类型检测,照这么写下去,代码冗余太多,所以,我们可以引入一个新的函数,而这个新的函数可以像工厂一样批量创建一些类似的函数:

1
2
3
4
5
6
var isType = function(type){
var toString = Object.prototype.toString;
return function(obj){
return toString.call(obj) == '[object' + ' '+type + ']';
};
};

如上,通过 isType() 函数,预先指定了参数 type 的, 这种通过指定部分参数来产生一个新的定制函数的形式就是 函数柯里化

使用柯理化封装类型检测函数

如此,通过 isType() 函数来创建一个新的函数就要简单得多了

1
2
3
4
5
var isString = isType('String');
var isFunction = isType('Function');
var isArray = isType("Array")
var str = 'hello world'
console.log(isString(str))//true

变量对象与执行环境

变量对象 基础数据类型 引用数据类型

JavaScript 的执行上下文生成以后,会创建一个变量对象(Variable Object),JavaScript 的基础数据类型会保存在变量对象中。JavaScript 中有 6 中基本数据类型,Undefined,Null,Boolean,String,Number,Symbol。而基础数据类型是按值访问的,所以我们可以直接操作保存在变量中的值。

而对于引用数据类型的值是保存在堆内存中的对象。JavaScript 不允许直接访问堆内存中的位置,而是通过对象的引用去操作的,而这个引用可以理解为是保存在变量对象中的一个地址。所以,引用类型的值是按引用访问的。

严格意义上来说,变量对象也是存放于堆内存中,但是由于变量对象的特殊职能,我们在理解时仍然需要将其于堆内存区分开来。

变量对象与堆内存

执行上下文(Execution Context)

执行上下文可以理解为但当前代码的执行环境,它会形成一个作用域。 JavaScript 中的运行环境包括三种情况,全局环境,函数环境,eval。

因此,当程序运行时必然会产生多个执行上下文,JavaScript 引擎会以堆栈的方式来处理他们,而这个堆栈即是 函数调用栈(call stack)。栈底永远是全局上下文,而栈顶永远是当前正在执行的上下文。

我们可以得出关于执行上下文的一些结论:执行上下文是单线程同步执行的,只有栈顶的上下文处于执行状态,其它的需要等待,而每个某个函数被调用,就会有个新的执行上下文为其创建,即使是调用的函数本身。

变量对象与执行上下文

执行上下文是有生命周期的,而它的生命周期可以分为两个阶段:

  • 创建阶段:
    在这个阶段,执行上下文会分别创建变量对象,建立作用域链,以及确定 this 指向。

  • 代码执行阶段:
    创建完成以后,开始执行代码,这时会完成变量赋值,函数引用,以及执行其它代码。

    执行上下文的生命周期

深入理解变量对象与声明提升

变量对象的创建,依次执行了如下过程:

(1) 建立 arguments 对象。

(2) 检查当前上下文的函数声明。使用 function 关键字声明的函数。在变量对象中,会以函数名建立属性,属性值是指向该函数所在内存地址的引用。如果函数名的属性已存在,那么该函数将会被新的引用所覆盖。

(3) 检查当前上下文的变量声明。每找到一个变量声明,就在变量对象中以该变量名建立一个属性,属性值为 undefined。如果该变量名的属性已经存在,则直接跳过,原属性值不会被修改。

比如,我们一段代码是这样的:

1
2
3
4
5
6
7
8
9
10
// Code One
function test (){
console.log(a)
console.log(foo());
var a = 1;
function foo() {
return 2;
}
}
test();

下面是执行长下文的创建过程:

1
2
3
4
5
6
7
8
9
// 创建阶段
testEC = {
// 变量对象
VO: {
arguments: {...},
foo: <foo reference>,
a: undefined
}
}

这里需要注意的是,未进入执行阶段之前,变量对象中的属性都不能访问。只要进入到执行阶段,等到变量对象变为活动对象之后,里面的属性才能被访问到,然后开始执行执行阶段的操作。

1
2
3
4
5
6
7
// 执行阶段
VO -> AO
VO = {
arguments: {...},
foo: <foo reference>,
a: 1
}

OK,我们从变量对象的角度理解了变量声明提升和函数声明提升的过程,总结下,就是,函数声明提升具备覆盖能力,即是后声明的函数会覆盖之的函数声明,同样会覆盖同名的变量声明。但是我们需要注意的是,一旦变量被初始化,那么便不会存在覆盖问题。

1
2
3
4
5
6
7
8
9
function a(x) {
x && a(--x);
};
var a;
alert(typeof a)
// 'function'
a = 1;
alert(typeof a);
// 'number'

另一种情况是函数表达式,其实函数表达式与变量是一回事儿:

1
2
3
4
5
6
7
console.log(getName())

var getName = function () { console.log(4);};
console.log(getName())

function getName() { console.log(5);}
console.log(getName())

A: 5 4 4
B: 报错 4 4
C: 4 4 5
D: 报错 4 5

JavaScript 的代码的执行分为两个阶段,编译阶段和执行阶段。编译阶段由编译器完成,将代码翻译成为可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文会在该阶段被创建。

作用域可执行上下文 是两个不同的概念,在 JavaScript 中,我们可以将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符(即变量名或者函数名)名称进行变量查找。

它们之间的关系是: 作用域在编译阶段确定规则,作用域链在可执行上下文中被创建。作用域链是对作用域这一套规则的具体实现。

作用域链其实是,由当前环境与上一层环境的一系列变量对象组成,保证了当前执行环境对符合访问权限的变量和函数的有序访问。

再接再厉,我们直接抛出闭包的定义:当函数可以记住并访问所在的作用域时,就产生了闭包。

附加题:

你接触(有贡献过代码)或者实现过的一个完整的系统:

  1. 系统的主要功能有哪些?

  2. 系统的模块划分(代码组织结构)是怎样的,请画一下系统代码组织结构(前后端都有的请分开画)?

  3. 用到了哪些技术栈或者知识点(前后端都有的请分开描述)?

  4. 这个系统中你遇到的最难的或者你最得意的实现点是什么?

暂时性死区

ref

详解js执行环境——声明提升的本质

发布/订阅模式

发布-订阅(Publish/Subscribe)模式、又叫观察者模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。

观察者模式(Observer Pattern):定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。

这是设计模式中最常用的一种模式了。最简单的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
function EventTarget() {
// EventTarget 其实就是发布者(主体),也可以理解为母体,因为是通用模式,她将构造出所有发布者

this.handlers = {}
// 存放订阅者的消息(回调函数)的缓存对象
// handlers 用于存储事件处理程序的缓存对象
}

EventTarget.prototype = {
constructor: EventTarget,
addHandler: function (type,handler/*接收两个参数:自定的事件类型和事件处理程序*/) {
if (typeof this.handlers[type] === 'undefined') {
// 初次判断,是否存在该类型的消息
this.handlers[type] = [];
// 为每一种事件类型建立一个事件类型数组
}
this.handlers[type].push(handler)
// 将订阅的消息存放进缓存列表
// 将对应的事件处理函数推入到事件类型数组中
},

fire:function (event) {
// 发布消息
console.log(this)
event.target || (event.target = this)
// 为自定义事件构造 event 对象
if (this.handlers[event.type] instanceof Array){
// 如何消息池中没有消息,就什么都不干
var handlers = this.handlers[event.type];
// 获取要发布的消息类型
for(var i = 0;i< handlers.length;i++){
handlers[i](event);
// 发布消息
}
}
},
removeHandler:function (type,handler) {
// 取消订阅
if(this.handlers[type] instanceof Array) {
var i = this.handlers[type].indexOf(handler)
if (i > -1) {
this.handlers[type].splice(i,1)
}

}
}
}
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
subject = new EventTarget()
// 新建一个主体,即发布者,一个发布者可以对应多个订阅者

function message1(event) {
console.log(event.message)
}
function message2(event) {
console.log(event.message)
}

function message3(event) {
event.message;
}

observable1 = subject.addHandler('sayHello', message1)
// 订阅者二 订阅 sayHello 种类的消息,消息的内容是 message1
observabler2 = subject.addHandler('sayWorld', message2)
// 订阅者二 订阅 sayWorld 种类的消息,消息的内容是 message2
observabler3 = subject.addHandler('say', message3)
// 订阅者三 订阅 say 种类的消息,消息的内容是 message3

/*先订阅消息,才能发布消息*/

subject.fire({
/*fire 的参数就是 event 对象,
通过在 fire 内部对 event 对象的改造,
使得 event 对象最少具有 target 和 type 属性。
然后将 event 对象传递给订阅者,即是要发布的消息
*/
// 发布 message 消息
type:'sayHello',
message: 'hello'
})

subject.fire({
// 发布 message2 消息
type: 'sayWorld',
message: 'world'
})

subject.removeHandler('say',message3)
// 取消订阅 消息3

JS设计模式(1)单例模式

0x00 全局变量

首先需要明确的一点是,全局变量并不是单例模式。虽然,我们经常在 JavaScript 的开发中将全局变量当做单例来使用。

而对于全局变量,其最大的问题莫过于造成命名空间污染。

而为了最大程度的减少命名冲突,最常用的手段是使用闭包构建块级作用域以封装私有变量。

0x01 惰性单例

惰性单例 指的是在需要的时候才创建实例,并且只创建一个实例。

1
2
3
4
5
6
7
8
let getSingle = function (fn) {
let result;
// result 变量存在于闭包中,永远不会被销毁,而在将来的请求在中,如果
// result 已经被赋值,那么它将返回该值。
return function () {
return result || (result = fn.apply(this, arguments));
}
}

单例模式将会非常有用,比如可以用来创建一个唯一的登录浮窗对象,但是,我们并不希望这个浮窗在页面加载完成的时候就已经创建完成,因为有时候用户可能并不会登录,而是希望当用户点击登录按钮时才创建浮窗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let createLoginLayer = function () {
let div = document.createElement('div');
div.innerHTML = "我是登录浮窗";
div.id = "loginLayer";
div.style.display = "none";
document.body.appendChild(div);
return div;
}

let createSingleLoginLayer = getSingle(createLoginLayer);

document.querySelector("#logBtn").onclick = function (e) {
let loginLayer = createSingleLoginLayer();
loginLayer.style.display = 'block';
}

单例模式最巧妙的地方在于,创建对象和管理单例的职责 被分布在两个不同的方法中,而只有当这两个方法组合起来才能发挥出单例模式的威力。

单例模式的核心是确保只有一个实例,并提供全局访问

JS设计模式(2)策略模式

策略模式的定义是: 定义一系列算法,把他们一个个封装起来,并且使他们可以相互替换

0x01 策略模式实现缓动动画

策略模式的一个经典运用场景就是缓动动画。

其核心思想是使用策略模式把算法传入动画类库中,来达到给种不同的缓动效果,而这些算法可以轻易的被另一个算法替代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let tween = {
// 动画类库,用于封装缓动算法
// t:动画已经消耗的时间
// b:小球的原始位置
// c:小球目标位置
// d:动画持续的总时间
linear: function (t, b, c, d) {
return c * t / d + b;
},
easeIn: function (t, b, c, d) {
return c * (t /= d) * t + b;
},
strongEaseIn: function (t, b, c, d) {
return c * (t /= d) * t * t * t * t + b;
}
};

完整代码:策略模式-缓动动画

0x02 策略模式实现表单验证

其实,仅仅把策略模式用于封装算法有点大材小用,实际开发中,通常会把算法的含义扩散开来,使策略模式可以用来封装一系列的 业务规则

在一个 Web 项目中,表单验证往往是在所难免的,当需要验证的字段很少的时候,比如几条,十几条,没问题,我们完全可以重复的使用 if...else 去验证表单字段,但如一个表单中的字段多大几十个,甚至上百个时,考虑到代码的可复用性和后期的可以维护性,便十分有必要使用一些模式去组织我们的代码了。 比如,策略模式。

使用策略模式编写表单校验代码的第一步就是将校验逻辑都封装成为策略对象:

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
let strategies = {
// 策略对象,封装验证表单的规则
isNotEmpty: function (value, errorMsg) {
if (value == '') {
return errorMsg;
}
},

minLength: function (value, length, errorMsg) {
if (value.length < length) {
return errorMsg;
}
},

legalPhone: function (value, errorMsg) {
let regPhone = /^1[345678]\d{9}$/g;
if (!regPhone.test(value)) {
return errorMsg;
}
},

legalEmail: function (value, errorMsg) {
let regEmail = /^\w{1,18}@([a-z][0-9]){2,7}.[a-z]{2,4}$/i;
if (!regEmail.test(value)) {
return errorMsg;
}
}

};

然后,我们会创建 Validate 类,它的作用是作为 Context,负者接收用户的输入并将用户输入的内容委托给 strategy 对象。

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
let validateFunc = function () {
// validateFunc 执行表单验证函数
// 添加验证规则
let validator = new Validate();
// 创建一个 Validate 类,作用是作为 `Context`,负者接收用户的输入并将用户输入的内容委托给 `strategy` 对象。
validator.add(regForm.userName, [
{
strategy: "isNotEmpty",
errorMsg: "用户名不能为空",
}, {
strategy: "minLength:6",
errorMsg: "密码长度不能小于六位"
}]);
validator.add(regForm.userPassword, [{
strategy: "minLength:6",
errorMsg: "密码长度不能小于六位"
}]);
validator.add(regForm.phoneNumber, [{
strategy: "legalPhone",
errorMsg: "手机号格式不正确"
}]);
validator.add(regForm.userEmail, [{
strategy: "legalEmail",
errorMsg: "邮箱格式不正确"
}]);

return validator.start();
// 返回校验结果
};

如上,validate 类的 add 方法用于向我们的 strategy 的对象添加校验规则,它接收两参数:参与校验的 input 输入框以及一个数组,数组中存放的是验证的策略规则和验证失败返回的提示信息。

下面是 validate 对象的具体实现:

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
let Validate = function () {
// 验证表单的类
// Validate 类作为 Context,负者接收用户的请求并委托给 strategies 对象
this.cache = [];
// 保存验证规则的数组
};

Validate.prototype = {
constructor: Validate,
add: function (dom, rules) {
// 规则关联,将规则与对应的元素进行绑定,并返回验证结果

let self = this;
for (let i =0, rule; rule = rules[i++];){
(function (rule) {
let strategyArr = rule.strategy.split(":");
// 参数解析
let errorMsg = rule.errorMsg;
// 获取错误信息
self.cache.push(function() {
// 推入策略规则
let strategy = strategyArr.shift();
// 首先截取策略名称
strategyArr.unshift(dom.value);
// 添加 dom 的内容
strategyArr.push(errorMsg);
// 推入错误信息
return strategies[strategy].apply(dom, strategyArr);
// 信息策略,并返回校验结果
});
})(rule);
}
},

start: function () {
for (let i = 0, validaterFunc; validaterFunc = this.cache[i++];) {
let msg = validaterFunc();
// 开始校验,并取得校验后的返回信息
if (msg) {
// 如若有返回值,说明校验不成功
return msg;
}
}

},
};

当我们往 validate 对象添加完校验规则以后,便调用 validate.start 方法来启动校验,该方法返回的 errorMsg 字符串代表校验没有通过,那么便需要调用 regForm.onsubmit 方法返回 false 阻止表单的提交。

1
2
3
4
5
6
7
8
9
    regForm.onsubmit = function () {
let errorMsg = validateFunc();
// 若,validateFunc 没有任何值返回,则代表校验通过
if (errorMsg) {
alert(errorMsg);
return false;
// 阻止表单提交
}
}

策略模式实现表单验证

CSS CSS视觉效果

0x00 投影的绘画机制

当为一个元素添加 box-shadow 时,我们便会从视觉上得到一个投影的效果。

比如:

1
2
3
4
5
6
#box{
width: 100px;
height: 100px;
background: deeppink;
box-shadow: 5px 6px 4px rgba(0,0,0,0.5);
}

box-shadow

我们对 div#box 添加了 box-shadow 属性,并指定了三个长度值和一个颜色值。对于这样的用法,我们再熟悉不过了。要得到上图的效果,浏览器渲染引擎其实进行了四步:

  1. 以该元素相同的位置和尺寸,画一个 rgba(0,0,0,0.5) 的矩形。
  2. 把它向右偏移 5px,向下偏移 6px
  3. 使用高斯模糊算法对其进行 4px 的模糊处理。
  4. 模糊后的矩形与原始元素的交集部分会被裁切掉。

box-shaow的绘制原理

所以,从投影绘制的机制来看,绘制的投影其实是在元素的上层的。

单侧投影

box-shadow 鲜为人知的第四个参数,称为 扩张半径。这个参数会根据指定的值去扩大(当指定负值时)或缩小投影的尺寸。比如,一个 -5px 的扩张半径会把投影的宽度和高度各减少 10px (即每边各 5px)。

那么,当应用一个负的扩张半径,而它的值刚好等于模糊半径,那么投影的尺寸就会与投影所属元素的尺寸一致,如果不使用偏移参数来移动它,将看不见任何投影。

这正是我们想要的。

1
box-shadow: 0px 5px 4px -4px black;

我们给了投影一个正的垂直偏移量,而在另外三侧是没有投影的。

单侧投影

双侧投影

目前为止,还无法指定投影在水平方向上放大,而在垂直方向上缩小,要实现双侧投影的效果唯一的办法就是使用两块投影来达到目的。

1
2
box-shadow: 6px 0px 5px -4px yellow,
-6px 0px 5px -4px green;

双侧投影

JS高级定时器

0x00 非异步

setTimeoutsetInterval 并非异步调用, 所谓的”异步调用”, 只是因它们都往 js 引擎的 待处理任务队列 末尾插入代码, 看起来像”异步调用”而已.

0x01 重复定时器

很多情况下,我们都需要使用 setInterval() 重复的执行同一段代码去做同一件事情,而在这时,最大的问题在于定时器可能在代码再次被添加到队列之前还没有被执行完成,从而导致某些间隔被跳过或者多个定时器的代码执行时间间隔被缩短。

为了避免以上缺点,可以使用链式调用 setTimeout() 模式

1
2
3
4
5
setTimeout(function(){
// do something

setTimeout(arguments.callee, interval);
}, interval)

一个例子:

1
2
3
4
5
6
7
8
9
setTimeout(function(){
$("#block").css({
'left': $('#block').position().left -1,
})
if($('#block').position().left > 0){
setTimeout(arguments.callee, 30);
}

}, 30)

0x01 数组分块

为了防止恶意程序猿将用户的计算机搞挂,浏览器对 JavaScript 能够使用的资源进行了限制,如果代码的运行时间超过特定时间或者特定语句数量就不让其继续运行。

而脚本运行时间过长的两个主要原因是:1)过长,过深嵌套的函数调用;2)进行大量处理的循环。

针对第二种问题,使用定时器是解决方法之一。使用定时器分隔循环,是一种叫作 数组分块(array chunking) 的技术。

在数组分块模式中,array 变量本质上就是一个 “代办事项” 列表,它包含了要处理的项目,而 shift() 可以获取队列中下一个要处理的项目,然后将其传递个某个函数。当队列中还剩下其它项目时,则设置另一个定时器,并通过 arguments.callee 调用同一个匿名函数。

1
2
3
4
5
6
7
8
9
10
function chunk(array, process, context){
setTimeout(function(){
var item = array.shift()
process.call(context, item)

if(array.length > 0){
setTimeout(arguments.callee, 100)
}
}, 100)
}

chunk() 方法接收三个参数: 要处理项目的数组,用于处理项目的函数,可选的运行该函数的环境。

在函数内部,通过 call() 调用 process() 函数,这样可以设置一个合适的执行环境。为定时器设定的时间间隔使得 JavaScript 进程有时间在处理项目的事件之间转入空闲。

调用实例:

1
2
3
4
5
6
7
var data = [12,124,343,56,76767,43,654,34645,56456,767,4645]
function printValue(item){
var div = $('#block').html()
$('#block').html(div + item + '<br>')
}

chunk(data, printValue)

如上,函数 printValue()data 数组中的每个值输出到一个 div 元素中。由于函数处于全局作用域中,因此无需给 chunk() 函数传递 context 对象。

如果想保持原数组不变,则应将该数组的克隆传递给 chunk()

1
chunk(data.concat(), printValue)

调用某个数组的.contact(),如果不传递任何参数,将返回和原来数组中项目一样的数组。