MaYue's Notes

Quick notes


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于

  • 搜索

Vue源码解析(v2.x)

发表于 2020-02-20   |   分类于 Vue   |  

一、源码目录设计、编译

Flow

facebook 出品的 JavaScript 静态类型检查工具。类型检查是当前动态类型语言的发展趋势,所谓类型检查,就是在编译期尽早发现(由类型错误引起的)bug,又不影响代码运行(不需要运行时动态检查类型),使编写 JavaScript 具有和编写 Java 等强类型语言相近的体验。

  • 类型推断:通过变量的使用上下文来推断出变量类型,然后根据这些推断来检查类型。
  • 类型注释:事先注释好我们期待的类型,Flow 会基于这些注释来判断。

源码目录设计

1
2
3
4
5
6
7
src
├── compiler # 编译相关
├── core # 核心代码
├── platforms # 不同平台的支持
├── server # 服务端渲染
├── sfc # .vue 文件解析
├── shared # 共享代码
  • compiler

包括把模板解析成 ast 语法树,ast 语法树优化,代码生成等功能。编译的工作可以在构建时做(借助 webpack、vue-loader 等辅助插件);也可以在运行时做,使用包含构建功能的 Vue.js。

  • core

包括内置组件、全局 API 封装、Vue 实例化、观察者、虚拟 DOM、工具函数等。

  • platform

是 Vue.js 的入口,2 个目录代表 2 个主要入口,分别打包成运行在 web 上和 weex 上的 Vue.js。

  • server

所有服务端渲染相关的逻辑都在这个目录下。服务端渲染主要的工作是把组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记”混合”为客户端上完全交互的应用程序。

  • sfc

把 .vue 文件内容解析成一个 JavaScript 的对象。

  • shared

定义一些工具方法,这里定义的工具方法都是会被浏览器端的 Vue.js 和服务端的 Vue.js 所共享的。

阅读全文 »

2019 Year in Review

发表于 2019-12-22   |   分类于 年度总结   |  

2019前端体系及核心知识点总结

深冻结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let person = {
name: 'aaa',
lesson: {
name: 'english',
type: 'basic'
}
};

function deepFreeze(obj) {
let propNames = Object.getOwnPropertyNames(obj);

for (let name of propNames) {
let value = obj[name];

obj[name] = value && typeof value === 'object' ? deepFreeze(value) : value;
}

return Object.freeze(obj);
}

deepFreeze(person);

person.lesson.name = 123;

防抖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
debounce(fn, delay) {
let timer = null;

return (...args) => {
if (timer) {
clearTimeout(timer);
timer = null;
} else {
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
}
};
}

节流

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
// 定时器实现
throttle(fn, delay) {
let timer = null;

return (...args) => {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
clearTimeout(timer);
timer = null;
}, delay);
}
};
}

// 时间戳实现
throttle(fn, delay) {
let startTime = Date.now();

return (...args) => {
let curTime = Date.now();

if (curTime - startTime >= delay) {
fn.apply(this, args);
startTime = Date.now();
}
};
}

// 时间戳 + 定时器实现
// 保证第一次触发事件就能立即执行事件处理函数和每隔 delay 时间执行一次事件处理函数
throttle(fn, delay) {
let timer = null;
let startTime = Date.now();

return (...args) => {
let curTime = Date.now();
let remainTime = delay - (curTime - startTime);

clearTimeout(timer);

if (remainTime <= 0) {
fn.apply(this, args);
startTime = Date.now();
} else {
timer = setTimeout(fn, remainTime);
}
};
}
阅读全文 »

webpack-bundler-demo-2

发表于 2019-01-03   |   分类于 Demo   |  

参考链接

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
const fs = require("fs");
const path = require("path");
const babylon = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");

let ID = 0;

//读取文件信息,并获得当前js文件的依赖关系
function createAsset(filename) {
//获取文件,返回值是字符串
const content = fs.readFileSync(filename, "utf-8");

//讲字符串为ast(抽象语法树, 这个是编译原理的知识,说得简单一点就是,可以把js文件里的代码抽象成一个对象,代码的信息会存在对象中)
//babylon 这个工具是是负责解析字符串并生产ast。
const ast = babylon.parse(content, {
sourceType: "module"
});

//用来存储 文件所依赖的模块,简单来说就是,当前js文件 import 了哪些文件,都会保存在这个数组里
const dependencies = [];

//遍历当前ast(抽象语法树)
traverse(ast, {
//找到有 import语法 的对应节点
ImportDeclaration: ({ node }) => {
//把当前依赖的模块加入到数组中,其实这存的是字符串,
//例如 如果当前js文件 有一句 import message from './message.js',
//'./message.js' === node.source.value
dependencies.push(node.source.value);
}
});

//模块的id 从0开始, 相当一个js文件 可以看成一个模块
const id = ID++;

//这边主要把ES6 的代码转成 ES5
const { code } = babel.transformFromAstSync(ast, null, {
presets: ["@babel/preset-env"]
});

return {
id,
filename,
dependencies,
code
};
}

//从入口开始分析所有依赖项,形成依赖图,采用广度遍历
function createGraph(entry) {
const mainAsset = createAsset(entry);

//既然要广度遍历肯定要有一个队列,第一个元素肯定是 从 "./example/entry.js" 返回的信息
const queue = [mainAsset];


for (const asset of queue) {
const dirname = path.dirname(asset.filename);

//新增一个属性来保存子依赖项的数据
//保存类似 这样的数据结构 ---> {"./message.js" : 1}
asset.mapping = {};

asset.dependencies.forEach(relativePath => {
const absolutePath = path.join(dirname, relativePath);

//获得子依赖(子模块)的依赖项、代码、模块id,文件名
const child = createAsset(absolutePath);

//给子依赖项赋值,
asset.mapping[relativePath] = child.id;

//将子依赖也加入队列中,广度遍历
queue.push(child);
});
}
return queue;
}

//根据生成的依赖关系图,生成对应环境能执行的代码,目前是生产浏览器可以执行的
function bundle(graph) {
let modules = "";

//循环依赖关系,并把每个模块中的代码存在function作用域里
graph.forEach(mod => {
modules += `${mod.id}:[
function (require, module, exports){
${mod.code}
},
${JSON.stringify(mod.mapping)},
],`;
});

//require, module, exports 是 cjs的标准不能再浏览器中直接使用,所以这里模拟cjs模块加载,执行,导出操作。
const result = `
(function(modules){
//创建require函数, 它接受一个模块ID(这个模块id是数字0,1,2) ,它会在我们上面定义 modules 中找到对应是模块.
function require(id){
const [fn, mapping] = modules[id];
function localRequire(relativePath){
//根据模块的路径在mapping中找到对应的模块id
return require(mapping[relativePath]);
}
const module = {exports:{}};
//执行每个模块的代码。
fn(localRequire,module,module.exports);
return module.exports;
}
//执行入口文件,
require(0);
})({${modules}})
`;

return result;
}

const graph = createGraph("./example/entry.js");

const ret = bundle(graph);

// 打包生成文件
fs.writeFileSync("./bundle.js", ret);

webpack-bundler-demo

发表于 2019-01-03   |   分类于 Demo   |  

Webpack 简单实现

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/**
* 读取内容
*
* yarn add babylon babel-core babel-traverse babel-preset-env
* 首先我们传入一个文件路径参数,然后通过 fs 将文件中的内容读取出来
* 接下来我们通过 babylon 解析代码获取 AST,目的是为了分析代码中是否还引入了别的文件
* 通过 dependencies 来存储文件中的依赖,然后再将 AST 转换为 ES5 代码
* 最后函数返回了一个对象,对象中包含了当前文件路径、当前文件依赖和当前文件转换后的代码
*/
function readCode(filePath) {
// 读取文件内容(以字符串的形式)
const content = fs.readFileSync(filePath, 'utf-8')

// 生成 AST(转换字符串为 AST 抽象语法树)
const ast = babylon.parse(content, {
sourceType: 'module'
})

// 寻找当前文件的依赖关系
const dependencies = []
// 遍历抽象语法树
traverse(ast, {
// 每当遍历到 import 语法的时候
ImportDeclaration: ({ node }) => {
// 依赖文件的相对路径
// 把依赖的模块加入到数组中
dependencies.push(node.source.value)
}
})

// 转换为浏览器可运行的代码
// 通过 AST 将代码转为 ES5
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})

return {
filePath,
dependencies,
code
}
}

/**
* 提取依赖关系
*
* 调用 readCode 函数,传入入口文件
* 分析入口文件的依赖
* 识别 JS 和 CSS 文件
* 首先我们读取入口文件,然后创建一个数组,该数组的目的是存储代码中涉及到的所有文件
* 接下来我们遍历这个数组,一开始这个数组中只有入口文件,在遍历的过程中,如果入口文件有依赖其他的文件,那么就会被 push 到这个数组中
* 在遍历的过程中,我们先获得该文件对应的目录,然后遍历当前文件的依赖关系
* 在遍历当前文件依赖关系的过程中,首先生成依赖文件的绝对路径,然后判断当前文件是 CSS 文件还是 JS 文件
* 如果是 CSS 文件的话,我们就不能用 Babel 去编译了,只需要读取 CSS 文件中的代码,然后创建一个 style 标签,将代码插入进标签并且放入 head 中即可
* 如果是 JS 文件的话,我们还需要分析 JS 文件是否还有别的依赖关系
* 最后将读取文件后的对象 push 进数组中
*/
function getDependencies(entry) {
// 读取入口文件
// 从入口开始,分析所有依赖项,形成依赖图,采用深度优先遍历
const entryObject = readCode(entry)
// 定义一个保存依赖项的数组
const dependencies = [entryObject]

// 遍历所有文件依赖关系
for (const asset of dependencies) {
// 获得文件目录
const dirname = path.dirname(asset.filePath)

// 遍历当前文件依赖关系
asset.dependencies.forEach(relativePath => {
// 获得绝对路径
const absolutePath = path.join(dirname, relativePath)
// CSS 文件逻辑就是将代码插入到 `style` 标签中
if (/\.css$/.test(absolutePath)) {
const content = fs.readFileSync(absolutePath, 'utf-8')
const code = `
const style = document.createElement('style')
style.innerText = ${JSON.stringify(content).replace(/\\r\\n/g, '')}
document.head.appendChild(style)
`
dependencies.push({
filePath: absolutePath,
relativePath,
dependencies: [],
code
})
} else {
// JS 代码需要继续查找是否有依赖关系
const child = readCode(absolutePath)
// 给子依赖项赋值
child.relativePath = relativePath
// 将子依赖也加入队列中,循环处理
dependencies.push(child)
}
})
}

return dependencies
}

/**
* 实现打包的功能,生成浏览器可执行文件
*
*(Babel 将我们 ES6 的模块化代码转换为了 CommonJS,浏览器不支持,所以自己实现 CommonJS 相关的代码)
* 首先遍历所有依赖文件,构建出一个函数参数对象
* 对象的属性就是当前文件的相对路径,属性值是一个函数,函数体是当前文件下的代码,函数接受三个参数 module、exports、require
* module 参数对应 CommonJS 中的 module
* exports 参数对应 CommonJS 中的 module.export
* require 参数对应我们自己创建的 require 函数
* 接下来就是构造一个使用参数的函数了,函数做的事情很简单,就是内部创建一个 require 函数,然后调用 require(entry),也就是 require('./entry.js'),这样就会从函数参数中找到 ./entry.js 对应的函数并执行,最后将导出的内容通过 module.export 的方式让外部获取到
* 最后再将打包出来的内容写入到单独的文件中
*/
function bundle(dependencies, entry) {
let modules = ''
// 构建函数参数,生成的结构为
// { './entry.js': function(module, exports, require) { 代码 } }

dependencies.forEach(dep => {
const filePath = dep.relativePath || entry
modules += `'${filePath}': (
function (module, exports, require) { ${dep.code} }
),`
})

// 构建 require 函数,目的是为了获取模块暴露出来的内容
// module, exports, requir 不能直接在浏览器中使用,这里模拟了模块加载、执行、导出操作
const result = `
(function(modules) {
// 创建一个 require 函数: 它接受一个 模块ID 并在我们之前构建的模块对象查找它
function require(id) {
const module = { exports : {} }
//
modules[id](module, module.exports, require)

return module.exports
}

// 执行入口文件
require('${entry}')

})({${modules}})
`

// 当生成的内容写入到文件中
fs.writeFileSync('./bundle.js', result)
}

简单实现Vue数据响应式

发表于 2018-03-09   |   分类于 Demo   |  
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
function observe(obj) {
// 判断类型
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}

class Dep {
constructor() {
this.subs = []
}
// 添加依赖
addSub(sub) {
this.subs.push(sub)
}
// 更新
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 全局属性,通过该属性配置 Watcher
Dep.target = null

class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
Dep.target = null
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 调用 update 方法更新 Dom
this.cb(this.value)
}
}

function defineReactive(obj, key, val) {
// 递归子属性
observe(val)
let dp = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get value')
// 将 Watcher 添加到订阅
if (Dep.target) {
dp.addSub(Dep.target)
}
return val
},
set: function reactiveSetter(newVal) {
console.log('change value')
val = newVal
// 执行 watcher 的 update 方法
dp.notify()
}
})
}

var data = { name: 'aaa' }
observe(data)
function update(value) {
document.querySelector('div').innerText = value
}
// 模拟解析到 `{{name}}` 触发的操作
new Watcher(data, 'name', update)
data.name = 'bbb'

通过使用基于Vue的Onsen UI Tab切换提高用户体验

发表于 2017-10-26   |   分类于 JavaScript   |  

原文地址:https://medium.com/the-web-tub/improve-ux-with-swiping-tab-bar-using-onsen-ui-for-vue-4c7d0e5171f0

Tab Bar滑动切换

现如今Cordova应用和PWA应用在移动设备上的应用是非常流行的。提供原生页面的外观和感觉是提高用户体验的关键,但是这往往不是一件容易的事情。值得肯定的是,创建生成一个有样式的复选框和单选按钮根本就不是问题而是一个实实在在的功能,我们追求的应用质量提升是基于用户交互的。

一个可滑动切换的tab bar将应用的内容分割成不同的页面,并且可以允许让用户自己的手指打开他期待的页面进入可视区。如果与此同时由于用户拖动了页面,应用改变了它自身的外观会怎么样?听上去很有趣但是很难吗?让我们看看使用Vue.js实际上是多么的简单。

入门

首先,我们需要一个tab bar滑动切换组件,这里有一大堆提供不同功能的可选择产品,这里我们使用Onsen UI提供的tab bar组件,它能够支持在切换时自定义操作行为。也许你并不了解它,Onsen UI其实有着一套iOS和Android基于Vue应用的组件库。对于一个现有的项目,可以使用NPM或Yarn来安装它。

1
2
$> npm install onsenui vue-onsenui --save-dev
$> yarn add onsenui vue-onsenui -D

在app中必须包含一些必要的文件;

1
2
import 'onsenui/css/onsenui.css'; // Webpack CSS import
import 'onsenui/css/onsen-css-components.css'; // Webpack CSS import
1
2
import VueOnsen from 'vue-onsenui';
Vue.use(VueOnsen);

因此,新的项目可以使用Vue CLI快速开始,并且可以选择性的添加Vuex和其他的一些功能。

1
2
$> vue init OnsenUI/vue-cordova-webpack # For Cordova apps
$> vue init OnsenUI/vue-pwa-webpack # For PWA
阅读全文 »

AngularJS和React的比较介绍

发表于 2017-09-07   |   分类于 Framework   |  

原文地址:https://medium.com/@kellywhiting/a-comparative-introduction-to-front-end-frameworks-angularjs-and-react-dd7c7410afc0

我一直在努力提高自己对AngularJS和React这两大前端框架的理解力和掌握能力,因为它们在硅谷看上去似乎是最受欢迎的。当然,React似乎是这其中的领先者。像Facebook,Instagram,Airbnb,Dropbox,ProductHunt,Netflix都在使用它。AngularJS在Google,PayPal,YouTube,Vevo,Upwork都有落地项目中使用。

因此如果你也正在尝试使用这两框架中的一个或二者都想,那这篇介绍的目的就是帮助大家去理解它们的基本实现。作为研究学习的开始,我已经把一个应用程序设置分成了4个步骤:

  1. 用Properties和Methods建立一个Controller

  2. 初始化和渲染

  3. 在Scopes间传递变量

  4. 添加事件

一旦你掌握了所有的知识,你可以自己决定你更喜欢哪一个框架。分享你的评论让我知道你是怎么认为的。

作为开始,我将做一个MVC(Model-View-Controller)架构模式的快速总结。以下是我最喜欢的一种交互视觉解释,因为它也包含了数据库和客户端或者路由的关系。通常来说,MVC设计模式的实现是为了在函数之间建立一个清晰的分离。控制器Controller处理程序逻辑和客户端交互。模型层Model处理真实的数据。视图层View管理着客户端的表现层的页面渲染。当然这种设计模式是一个标准化的概念,实际上,每一个框架的实现都有它自己独特的方式。

来源: CodeBox.in

阅读全文 »

事件代理:模式还是反模式

发表于 2017-08-05   |   分类于 JavaScript   |  

原文地址:Event Delegation: Pattern or Anti-Pattern?

JavaScript toolkits和框架所做的大量工作,都集中于尝试修复、规范或优化浏览器的功能实现。此类工作需要做出许多假设,这些假设包括:问题是什么,开发人员将如何使用我们的工具,以及我们对未来的期望。

但这些假设经常是错误的,更有甚者,在很长一段时间内,这些选择可能貌似正确,直至某天我们被问题反咬一口。在这个无知的幸福时期当中,我们的工具包可能变得相当受欢迎,并成为大型复杂代码库的重要组成部分。

事件冒泡与事件代理

事件冒泡允许源自子节点的事件向父级节点“冒泡”。这种行为导致JavaScript开发者使用松散的设计模式来识别我们所关心的接收事件的节点,通常使用 CSS 选择器,同时将事件监听器添加到该节点的父级节点上。

一旦这种模式进入工具包之中,设计API时须做出一些假设。在开始阶段,这些假设主要围绕性能与效率展开。

事件代理是处理事件的实际方法之一。然而这种方法论适用于所有项目吗?实际上,更好的问题可能是,每个工具包的所基于的假设是否与你的项目需求相符。要想知道某个API是否适合当下项目,就要了解这些工具是建立在哪些假设之上的,并且理解每个工具包如何解释它们。

假设

一起来看看,在思考如何有效管理DOM事件时可能会产生的一些假设。

本机事件注册机制太慢

在你能够提出API存在的继发原因之前,不要创建新的API。随着浏览器厂商们对运行时的投入增加,你的功能实现总有一天会比原生实现慢。我所在的 SitePen有一个项目依赖于数组拼接速度。我们发现在某些情况下,手动操作索引和数组长度能够带来显著的性能提升。但我们无法定位到特定浏览器、浏览器版本或平台,因为无法进行运行时功能测试以确定我们的实现是否比原生API快。

新的原生API不会出现

保持谨慎,确保已收集到足够的信息,可以降级使用原生实现,无论是已存在的,还是理想情况下可能存在的。这项工作的另一个名字叫“预防过时”(future proofing)。在某些情况下,你可能会使用必需参数超出绝对需要的API,但如果它能够保证轻松地过渡到更优秀的原生API,那么完全可以如此。一个很好的例子是最终获得原生支持的querySelectorAll API,之前许多开发人员假设这种事永远不会发生。

不常见用例没有性能损失

事件代理可能会以数种方式呈现。例如两种特殊情况:大量节点上的少量事件,以及少数节点上的大量事件。如果针对其中之一进行优化,则可能会为另一个带来明显的瓶颈。虽然使用事件代理可能只需要向单个节点添加一个事件侦听器,但识别触发回调的节点的复杂方法对性能的影响可能不成比例。快速触发大量事件(例如鼠标移动或滚动事件)正是使用事件代理的场景。

条件与背景

在考虑事件代理时,很容易认为我们只需要关心用户交互。这可能导致我们假设节点始终是文档的一部分,然后开始思考,为何不在document对象上添加单个事件处理程序呢?DOM事件并非总是用户交互的结果,我们也有人为事件、自定义事件以及加载事件等。如果想要监听的节点不在文档中,而监听器却绑定在document对象上,我们永远得不到通知。如果在API中无法区别监听器是添加到document上,抑或是添加到我们所传递的参数上,则能够理解为什么会出现这种情况。

抽象

如果一个工具包提供一个仅用于支持代理的事件处理API,需要父级节点和标识子节点的选择器,则无法将事件监听器直接添加到某个节点。即使是使用CSS选择器,也引入了更高级的功能,可以轻松地使用另一种选择器语法或简单函数。

不会发生副作用

如上所述,DOM事件冒泡是事件代理模式存在的前提。但是了解完整规范所涉及的内容之后,你会发现,事件冒泡是可以取消的。你的实现可能会将 stopPropagation 方法为空函数的自定义事件传递给回调函数;或者,你可能只会记录问题,并限制事件代理API的使用。这两种方法都有问题,但是如果你打算像为document对象事件处理程序那样工作,添加大量可取消的事件层可能放大副作用。

不受时间影响

一旦代码编写完成,很可能就会弃而不顾。但浏览器正在以我们无法想象、预测的方式向前发展,我们在编写代码时所做的假设可能会被证明是错误的,尽管我们尽了最大的努力。

总结

为什么要在项目中使用事件代理?

  • 原生实现太慢了吗?对现代浏览器来说不太可能。
  • 是否有更好的API来执行事件代理?目前还没有如果你需要事件代理,这是一个很好的模式。
  • 该工具包的性能优化是否符合项目需求?如果它专注于特殊情况,可能不会。
  • 工具包的实现中有没有什么不适用于你的项目的内容?阅读文档,这些通常都会标出。
  • 是否有副作用?遇到错误前你可能不会发现这一点,所以要特别注意。

JavaScript的那些奇技淫巧

发表于 2017-06-30   |   分类于 JavaScript   |  

No1.柯里化(currying)与部分应用(partial application)

柯里化(currying)

柯里化是使一个函数

f: X * Y -> R

转变为

f’: X -> (Y -> R)

与用两个参数调用f不同,我们用一个参数运行f’。返回的结果是一个函数,然后用第二个参数调用此函数,得到结果。

如此,如果未柯里化的函数f这样调用

f(3,5)

柯里化后的函数f’是这样调用的

f(3)(5)

比如: 未柯里化的函数add()

1
2
3
4
5
function add(x, y) {
return x + y;
}

add(3, 5); // returns 8

柯里化后的add()

1
2
3
4
5
6
7
function addC(x) {
return function (y) {
return x + y;
}
}

addC(3)(5); // returns 8
柯里化的规则

柯里化将一个二元函数,转变为一元函数,这个函数将返回另一个一元函数。

curry: (X × Y → R) → (X → (Y → R))

Javascript Code:

1
2
3
4
5
6
7
function curry(f) {
return function(x) {
return function(y) {
return f(x, y);
}
}
}
部分应用(partial application)

部分应用将一个函数

f: X * Y -> R

的第一个参数固定而产生一个新的函数

f`: Y -> R

f’与f不同,只需要填写第二个参数,这也是f’比f少一个参数的原因。

比如:将函数add的第一个参数绑定为5来产生函数plus5。

1
2
3
4
5
function plus5(y) {
return 5 + y;
}

plus5(3); // returns 8
部分应用的规则

部分应用使用一个二元函数和一个值产生了一个一元函数。

partApply : ((X × Y → R) × X) → (Y → R)

Javascript Code:

1
2
3
4
5
function partApply(f, x) {
return function(y) {
return f(x, y);
}
}

No2.优化嵌套的条件语句

我们怎样来提高和优化javascript里嵌套的if语句呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
if (color) {
if (color === 'black') {
printBlackBackground();
} else if (color === 'red') {
printRedBackground();
} else if (color === 'blue') {
printBlueBackground();
} else if (color === 'green') {
printGreenBackground();
} else {
printYellowBackground();
}
}

一种方法来提高嵌套的if语句是用switch语句。虽然它不那么啰嗦而且排列整齐,但是并不建议使用它,因为这对于调试错误很困难。这告诉你为什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
switch(color) {
case 'black':
printBlackBackground();
break;
case 'red':
printRedBackground();
break;
case 'blue':
printBlueBackground();
break;
case 'green':
printGreenBackground();
break;
default:
printYellowBackground();
}

如果可以重构的话,我们可以试着简化函数。比如不需要为每个颜色写一个函数,而是将颜色作为函数的参数。

1
2
3
4
5
function printBackground(color) {
if (!color || typeof color !== 'string') {
return;
}
}

但是如果不能重构的话,我们必须避免过多的条件检查,避免过多使用switch。我们必须考虑最有效率的方法,使用object。

1
2
3
4
5
6
7
8
9
10
11
12
let colorObj = {
'black': printBlackBackground,
'red': printRedBackground,
'blue': printBlueBackground,
'green': printGreenBackground,
'yellow': printYellowBackground
};


if (color in colorObj) {
colorObj[color]();
}

No3.运用存储加速递归

大家对斐波那契(Fibonacci)数列都很熟悉。我们可以再20秒内写出下面这样一个方法。

1
2
3
var fibonacci = function(n){
return n < 2 ? n : fibonacci(n-1) + fibonacci(n-2);
}

它可以运行,但并不高效。它做了太多重复的运算,我们可以通过存储这些运算结果来使其加速。

1
2
3
4
5
6
7
8
9
10
11
var fibonacci = (function() {
var cache = [0, 1]; // cache the value at the n index
return function(n) {
if (cache[n] === undefined) {
for (var i = cache.length; i <= n; ++i) {
cache[i] = cache[i-1] + cache[i-2];
}
}
return cache[n];
}
})()

我们也可以定义一个高阶函数,它接收一个方法作为参数,返回一个该方法运用存储后的新方法。

1
2
3
4
5
6
7
8
var memoize = function(func){
var cache = {};
return function(){
var key = Array.prototype.slice.call(arguments).toString();
return key in cache ? cache[key] : (cache[key] = func.apply(this,arguments));
}
}
fibonacci = memoize(fibonacci);

ES6版本的memoize函数如下:

1
2
3
4
5
6
7
8
var memoize = function(func){
const cache = {};
return (...args) => {
const key = [...args].toString();
return key in cache ? cache[key] : (cache[key] = func(...args));
}
}
fibonacci = memoize(fibonacci);

我们可以将memoize()用在很多其他地方

  • GCD(最大公约数)
1
2
3
4
5
6
7
var gcd = memoize(function(a,b){
var t;
if (a < b) t=b, b=a, a=t;
while(b != 0) t=b, b = a%b, a=t;
return a;
})
gcd(27,183); //=> 3
  • 阶乘运算
1
2
3
4
var factorial = memoize(function(n) {
return (n <= 1) ? 1 : n * factorial(n-1);
})
factorial(5); //=> 120

Original:http://www.jstips.co/

如果想关注最新技术,请关注微信公众号:AutoHome车服务前端团队

6个字符的JavaScript之旅

发表于 2017-05-12   |   分类于 JavaScript   |  

原文地址:http://www.jazcash.com/a-javascript-journey-with-only-six-characters

JavaScript是一门奇妙而且有趣的语言,它可以让我们写一些疯狂但仍然有效合理的代码,它试图通过把事情转换到基于我们如何对待他们的特定类型上来帮助我们。

如果我们添加一个字符串,它会假定我们希望是文本形式表示,所以它将为我们转换成一个字符串。

如果我们添加一个加号或者是减号前缀,它会假定我们希望是数值形式呈现,如果可能的话那么就会为我们把一个字符串转换成数字。

如果我们添加一个否定符号,它会将字符串转换为一个布尔值。

我们可以用Javascript中的[,],(,),! and+这六个符号写一些神奇的代码。

如果你现在不是在移动设备上,你可以打开你浏览器的JS控制台跟着下面的步骤一起操作,你可以将任何示例代码拷贝粘贴到JS控制台,并且代码值应该都为true。

让我们从最基础的开始,记住一些黄金规则:

  1. 前面加!会被转换成布尔值

  2. 前面加+会被转换成数值

  3. 添加[]会被转换成字符串

这里他们是相等的:

1
2
3
4
5
![] === false

+[] === 0

[]+[] === ""

另一件事你应该知道的是,它可以从字符串使用方括号检索特定的字母:

1
"hello"[0] === "h"

还可以使多个数字通过添加字符串表示在一起,然后把整个表达式转换成一个数字:

1
+("1" + "1") === 11

好的,接下来让我们把一些东西结合在一起最后得到字母a:

1
2
3
4
5
6
7
8
9
![] === false

![]+[] === "false"

+!![] === 1

---------------------------------------------

(![]+[])[+!![]] === "a" // same as "false"[1]

我们可以通过true和false得到相似的字母a,e,f,l,r,s,t,u。那么我们可以从其他地方得到字母吗?

我们可以通过一些特别的式子像[]、[[]]得到undefined,用我们上面的黄金法则得到另外的字母d,i和n。

1
[][[]] + [] === "undefined"

到目前为止我们获得的所有字母,我们可以拼fill,filter和find。当然我们也可以拼一些其他的单词,这些单词最重要的是他们都是Array methods。这也就意味着他们是Array对象的一部分,可以直接调用数组实例(如[2,1].sort())。

现在了解另一件重要的JS的特性是一个对象的属性可以通过点符号.或方括号[]访问。上述数组方法是数组对象本身的属性,我们可以使用方括号代替点符号调用这些方法。

所以[2,1]["sort"]()等价于[2,1].sort()。

让我们继续看看到底发生了什么当试图使用一个数组的方法的时候,可以使用到目前为止拼写的但没有调用过的字母。

1
[]["fill"]

得到function fill() { [native code] },可以使用我们的黄金法则把这个方法头作为一个字符串。

1
[]["fill"]+[] === "function fill() { [native code] }"

现在我们又得到le其他的字符:c,o,v,(,),{,[,],}。

新得到的c和o现在可以形成 constructor这个单词了,到目前为止我们已经处理的对象可以得到它用字符串表示的构造器函数:

1
2
3
4
5
6
7
true["constructor"] + [] === "function Boolean(){ [native code] }"

0["constructor"] + [] === "function Number() { [native code] }"

""["constructor"] + [] === "function String() { [native code] }"

[]["constructor"] + [] === "function Array() { [native code] }"

Number类型的toString方法有一个称为radix(“基数”)的秘密的论点。它可以将数值在转换为一个字符串之前先经过基数换算。

1
2
3
4
5
6
7
(12)["toString"](10) === "12" // base 10 - normal to us

(12)["toString"](2) === "1100" // base 2, or binary, for 12

(12)["toString"](8) === "14" // base 8 (octonary) for 12

(12)["toString"](16) === "c" // hex for 12

但是为什么基数只写到16?最大值是36包括所有的字符0-9和a-z,因此现在我们可以得到任何我们想要的字符。

1
2
3
(10)["toString"](36) === "a"

(35)["toString"](36) === "z"

棒极了!但是其它字符如标点符号和大写字母如何呢?让我们继续接着往下深入研究。

这一点依赖于你的js正在运行在什么地方,它可能会或可能不会访问特定的预定义的对象或数据。如果你的JS运行在浏览器中,那么你可以访问到一些保留的HTML wrapper methods。

比如,bold是用一个用b标签包裹字符串的字符串方法。

1
"test"["bold"]() === "<b>test</b>"

这样我们也能够得到字符<,>and/。

你或许已经听说过escape方法。它主要是将一个字符串转换为一个URI友好的格式,这样可以让浏览器解释编译。这个方法是我们研究的一个重要部分,因此我们要去探究它。我们可以拼写它但是如何真正执行它呢?它不是一个属于我们到目前为止处理过的任何类型的函数,实际上它是一个全局函数。

任意一个函数的构造器是什么样的?

答案是function Function() { [native code] },它本身是一个函数对象。

1
[]["fill"]["constructor"] === Function

用这个方法我们可以给函数传递一个字符串代码。

1
Function("alert('test')");
1
2
3
Function anonymous() {  
alert('test')
}

现在我们可以像这样使用我们的escape方法。

1
[]["fill"]["constructor"]("return escape(' ')")() === "%20"

如果我们想得到我们忘记的其他字符大写C是很重要的。

1
[]["fill"]["constructor"]("return escape('<')")()[2] === "C"

用这个方法我们现在可以写一个fromCharCode方法,它可以将一个给定的十进制表示的数返回成一个 Unicode字符。

1
2
3
""["constructor"]["fromCharCode"](65) === "A"

""["constructor"]["fromCharCode"](46) === "."

我们可以使用Unicode lookup去找到一个任意字符用十进制表示的数。

为什么它是有用的呢?

eBay不久前做了一些bad things,他们允许商家使用这些字符执行JS在页面中。但这是一个很少见的攻击量。一些人提到了混淆,但实际上有更好的混淆方式。

Sources:

  • https://en.wikipedia.org/wiki/JSFuck
  • https://esolangs.org/wiki/JSFuck
  • https://github.com/aemkei/jsfuck
  • http://patriciopalladino.com/blog/2012/08/09/non-alphanumeric-javascript.html
123
Marico

Marico

Front-End Developer

21 日志
13 分类
18 标签
GitHub Stack Overflow 微博 Twitter
© 2016 - 2021 Marico
由 Hexo 强力驱动
主题 - NexT.Pisces