Hawei

Say Hi.


  • Home

  • About

  • Tags

  • Categories

  • Archives

Chapter 3Managing Function Inputs.md

Posted on 2020-04-12 | In light-functional

Managing Function Inputs

all of one

多元转一元

funtion 描述

1
2
3
4
5
function unary (fn) {
return function onlyOneArg(arg) {
return fn(arg)
}
}

arrow function 描述

1
const unary = fn => arg => fn(arg)

explames

1
2
3
4
5
[1, 2, 3].map(parseInt); // [1, na, na]
// 解释一下为什么, parseInt 接收两个参数
// parseInt(a, b)
// a 要转换成整形的值
// b 要转换成数值的形式(二进制, 八进制, 十进制)

one by one

怎么说捏, 这个函数, 用在延迟计算上真的很有用,比如我需要一个值, 这个值在我下一步函数用到,但是这个值, 不能污染我下一个函数啊, 那么我就可以使用组合的方式, 使用这个identity函数去组合.

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
function identity (v) {
return v;
}
// arrow function
const identity = v => v;


// explames
// 1\. 空值检测
var words = " Now is the time for all... ".split( /\s|\b/ );

words.filter( identity );

// 2\. 参数为函数时, 作为该参数的默认值, 去掉原逻辑中,判断该参数是否函数的默认值
function output(msg,formatFn = identity) {
msg = formatFn( msg );
console.log( msg );
}

function upper(txt) {
return txt.toUpperCase();
}

output( "Hello World", upper ); // HELLO WORLD
output( "Hello World" ); // Hello World

unchanging

某些API不让你直接传入promise, 而是要通过一个返回一个函数来调用的方式, 比如在promise链, then函数中使用另一个promise函数, 这个时候就用到了

1
2
3
4
5
6
7
8
// don't work:
p1.then(foo).then(p2).then(bar)

// instead:
p1.then(foo).then(function () {return p2}).then(bar)

// or use arrow function
p1.then(foo).then(() => p2).then(bar)

FP中有个很好的工具解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
// function 
function constant (v) {
return function value () {
return v;
}
}

// arrow funtion
const constant = v => () => v;

// so you can use constant function
p1.then(foo).then(constant(p2)).then(bar)

Adapting Arguments to Parameters

先看一个例子

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

fucntion bar (fn) {
fn([3, 9])
}

我知道, 我们可以在fn([3, 9])使用解构去处理数组, 但是对于FPer来说, 我们需要更强的可读性, 所以有了下面的小玩具

1
2
3
4
5
6
7
function spreadArgs(fn) {
return function speadFn(argsArr) {
return fn(...argsArr)
}
}

const spreadArgs = fn => argsArr => fn(...argsArr)

但是有时候 , 我们又需要相反的操作.所以出现了下面的另一个小操作

1
2
3
4
5
6
7
fucntion gatherArgs(fn) {
return function gatheredFn(...argsArr) {
return fn(argsArr)
}
}

cosnt gatherArgs = fn => (...argsArr) => fn(argsArr)

Some Now, Some Later —- 偏函数

先看个例子

1
2
3
4
5
6
7
8
9
10
11
function getPerson(data, cb) {
Ajax('http://api/person', data, cb)
}
function getPerson(data, cb) {
Ajax('http://api/person', data, cb)
}

// 这个时候, 我又需要一个定制的接口, 用于获取特定用户的信息
function getCurrentUser (cb) {
getPerson({user: CURRENT_USER_ID}, cb)
}

以上的操作大家都不陌生, 就对于基本函数ajax的一个增强, 比如写死他的url, 写死, 请求方式形成get/post特定的请求方式.但是对于函数式编程来说, 这些都归为偏函数, 让我们来看偏函数这个玩具.

1
2
3
4
5
6
7
function partial (fn, ...presetArgs) {
return function partiallyApplied(...lastArgs) {
return fn(...presetArgs, ...lastArgs)
}
}

const partial = (fn, ...presetArgs) => (...lastArgs) => fn(...presetArgs, ...lastArgs)

好啦, 本质上就是一个闭包的使用,真的超好用. 但是, 对于js本身自带的API里其实就有类似偏函数的功能, 比如bind, 但是呢, bind存在问题,就是硬绑定this, 如果你不需要this, 那么就要传一个null作为一个占位符.你不觉得很很多余吗?

Reversing Arguments

属于部分应用函数的增强.偏函数的传入顺序是左到右, 而reversing的意思就是把传入顺序改为右到左.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function reverseArgs (fn) {
return function argsReversed(...args) {
fn(args.reverse())
}
}

const reverseArgs = (fn) => (...args) => fn(args.reverse())

// 另一种partialRight的技巧
function partialRight (fn, presetArgs) {
return function partiallyApplied (...laterArgs) {
return fn(...laterArgs, ...presetArgs)
}
}

const partialRight = (fn, ...presetArgs) => (...laterArgs) => fn(...later, ...presetArgs)

One at a time

额, 柯里化函数, 很像偏函数, 把需要多个参数的函数变成一系列连续调用的单参数函数.也就类似于, 每个函数都接受一个参数, 并且返回一个函数来接受下一个参数。

来看个场景

1
2
3
4
5
6
curriedAjax('http://some.api/person')({user: CURRENT_USER_ID})(function() { console.log('成功后的回调')})
// 或许以上的代码有点难以理解, 让我们把它拆分成
const personFetcher = curriedAjax('http://some.api/person');
var getCurrentUser = personFetcher({user: CURRENT_USER_ID});
getCurrentUser(function foundUser(user){// 成功后的回调})
// 这两种代码其实都是一个意思, 但是对于FP来说, 第一种风格就是我们提倡使用的

所以让我们来看一下curry玩具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function curry(fn, arity = fn.length) {
return (function nextCurried(prevArgs) {
return function curried(nextArg) {
var args = [...prevArgs, nextArg];

if(args.length >= arity) {
return fn(...args);
}else {
return nextCurried(args);
}
}
})([])
}
const curry = (fn, arity = fn.length, nextCurried) => (nextCurried = prevArgs => nextArg => {
var args = [...prevArgs, nextArg]

if(args.length >= arity) {
return fn(...args)
}else {
return nextCurried(args)
}
})([])

回到一开始的例子, 我们用curry来解决我们遇到的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
const curriedAjax = curry(ajax)
const personFetcher = curriedAjax('http:some/api/person')

const getCurrentUser = personFetcher({user: CURRENT_USER_ID})

getCurrentUser(function callback () {
// do something
})

// 再看一下之前的例子
const add = (a, b) => a + b

[1, 2, 3].map(curry(add)(3)) // 此时add函数中的a为3

柯里化和偏函数的区别, 偏函数是固定某些参数, 而达到增强。而偏函数的目的是适应变化.

Visualizing Curried Functions

来让我们手动的去定义一个curried函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
funtion curriedSum (v1) {
return function(v2) {
return function (v3) {
return function (v4) {
return function (v5) {
return sum (v1, v2, v3, v4, v5)
}
}
}
}
}
// 用箭头函数
const curriedSum = v1 => v2 => v3 => v4 => v5 => sum(v1, v2, v3, v4, v5)

Why Currying and Partial Application?

类比于普通函数, sum(1, 2, 3, 4, 5), 为什么要选择, 柯里化sum(1)(2)(3)这种形式, 或者使用偏函数partial(sum, 1, 2)这种形式呢?

  1. 你可以在某一时刻去指定部分参数, 然后在另一时刻再去指定剩余参数. 而传统的函数则要求所有参数都必须同时存在(传入).
  2. 当只有一个参数时, 函数组合将会更高效, 更容易.一元函数将会更易使用.
  3. 其实柯里化和偏函数增加了代码的可读性,同时使代码更为简洁.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ajax(
'http://some.api/person',
{user: CURRENT_USER_ID},
function foundUser(data) {

}
)
// 让我们使用偏函数去优化它
const getCurrentUser = partial(
ajax,
'http://some.api/person',
{user: CURRENT_USER_ID}
);
getCurrentUser(function foundUser() {
// do something
})

LooseCurry

宽松的curry指的是, 我可以一次传入多个参数,而不是像之前curry每次只传入一个, 来看个实例

1
2
3
4
5
6
// curry
sum(1)(2)(3)


// looseCurry
sum(1, 2)(3)

下面让我们看看looseCurry的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function looseCurry(fn, arity = fn.length) {
return (function nextCurried(pervArgs){
return function curried(...nextArgs) {
var args = [...pervArgs, ...nextArgs]

if(args.length >= arity) {
return fn(...args)
}else {
return nextCurried(args)
}
}
})([])
}

const looseCurry = (fn, arity = fn.length, nextCurried = pervArgs => (...nextArgs) => {
var args = [...pervArgs, ...nextArgs]

if(args.length > arity) {
return fn(...args)
}else {
return nextCurried(args)
}
})([])

No curry

如果你现在有个curry化函数, 你想把它转回正常函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function uncurry(fn) {
return function uncurried (...args) {
var ret = fn

for(let arg in args) {
ret = ret(arg)
}

return ret
}
}

// =>
const uncurry = fn => (...args) => {
var ret = fn

for(let arg in args) {
ret = ret(arg)
}

return ret
}

用命名空间来优化偏函数和柯里化函数, 不受顺序的影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function partialProps(fn, presetArgsObj) {
return function partiallyApplied(laterArgsObj) {
return fn(Object.assign({}, presetArgsObj, laterArgsObj))
}
}

function curryProps(fn,arity = 1) {
return (function nextCurried(prevArgsObj){
return function curried(nextArgObj = {}){
var [key] = Object.keys( nextArgObj );
var allArgsObj = Object.assign(
{}, prevArgsObj, { [key]: nextArgObj[key] }
);

if (Object.keys( allArgsObj ).length >= arity) {
return fn( allArgsObj );
}
else {
return nextCurried( allArgsObj );
}
};
})( {} );
}

来看个使用例子

1
2
3
4
5
6
7
8
9
10
11
12
function foo({ x, y, z } = {}) {
console.log( `x:${x} y:${y} z:${z}` );
}

var f1 = curryProps( foo, 3 );
var f2 = partialProps( foo, { y: 2 } );

f1( {y: 2} )( {x: 1} )( {z: 3} );
// x:1 y:2 z:3

f2( { z: 3, x: 1 } );
// x:1 y:2 z:3

1.function-purity.md

Posted on 2020-04-12 | In funcitonal-js-v3

funciton-purity

function and procedures

  1. 函数: 有输入, 有输出
  2. 程序: 没有输出, 只是在代码段中做了一些事

语义化函数命名

因为函数式编程倾向于声明式编程, 那么对于函数名就要简明易懂.

函数名称的重要性不亚于, 数学中, 公式对应的图像.

副作用

副作用就是, 在函数内部改变了外部变量(包括dom).
常见副作用:

  1. I/O操作(console/file)
  2. 数据库操作
  3. 网络请求调用
  4. DOM操作
  5. Timestamps
  6. 随机数
  7. Cup heat
  8. Cup time Delay

原则: 我们尽量避免副作用,但是,如果无法避免, 那么就让副作用明显一点, 让阅读代码的人看得到.

纯函数和常量

  1. 没有副作用
  2. 没有外部依赖

reducing surface Area

减少可以产生副作用的行为.不要对参数有赋值的念头.

纯函数的确定性: 同样输入,固定输出.

绝对不纯函数

3 Practical Uses of Object Destructuring in JavaScript.md

Posted on 2020-04-12 | In translation

3 Practical Uses of Object Destructuring in JavaScript(翻译)

现在你可能非常熟悉js中解构的概念了!解构这个概念来自2015年ES6草案, 但如果你需要更深一步的了解它, Mozilla有一篇更深入的文章说明它是如何工作的。(文章底部)

然而, 了解解构如何工作并不等于我们了解了如何使用它。使用这三个解构模式可以让你的代码更加清晰, 更强大, 更具可读性。

Named Function Arguments (命名函数参数)

相较于我们通过单个传参, 通过传入参数的位置来控制传参,解构模式用于形式参数是一个很好的代替方式.你只需按名称指定参数,而不是按照与函数签名相同的顺序排序参数。例如,在Python中:

1
2
3
4
def sum(a=1, b=2, c=3):
return a + b + c

sum(b=5, c=10)

就像你所看到的一样, 参数的顺序并不是问题, 你通过名字指定了他们。命名参数相较于基于位置的参数命名有以下的好处:

  1. 调用函数时, 可以省略一个或者多个参数
  2. 当传参的时候,顺序并不是问题
  3. 调用可能存在于其他地方的函数时,代码更具可读性

虽然JavaScript中不存在真正的命名参数,但我们可以使用解构模式来实现所有3个相同的好处。这是和上面python相同功能的代码, 但是在js中我们可以这样:

1
2
3
4
5
const sum = ({a=1, b=2, c=3}) => {
return a + b + c;
}

sum({b: 5, a: 1}); // 9

这种模式符合我们命名参数的所有目标。我们能够省去参数c,顺序无关紧要,我们通过名称引用它们来分配我们的参数。这一切都可以通过对象解构来实现。

Cleanly Parse a Server Response (清晰解析服务响应)

通常我们只关注服务响应内容里data块的东西,或者只关注在data中的一个特定值。在这个例子中,你可以使用解构来仅获取该值,同时忽略服务器通常发回的许多其他内容。这是一个代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const mockServer = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
'status': 200,
'content-type': 'application/json',
'data': {
dataInfo: 42
}
})
}, 1000);
})
}

mockServer().then(({data: {dataInfo = 100 }}) => {
console.log(dataInfo);
})

此模式允许您在解析参数时从对象中提取值。您还可以自由的设置默认值!

Setting Default Values During Assignment (在赋值的时候给默认值)

分配变量或常量时的常见情况是,如果范围中当前不存在某些其他值,则给他一个默认值。

在没有解构之前, 你可能通过以下代码来实现这个功能:

1
var nightMode = userSettings.nightMode || false;

但这需要为每个赋值写一行代码。通过解构,您可以同时处理所有的赋值及提供默认值。

1
2
3
4
5
6
7
8
const userSettings = { fontSize: 'large', nightMode: true };
const {
nightMode = false,
language = 'en',
fontSize = 'normal'
} = userSettings;

console.log(nightMode, language, fontSize);

解构模式能应用于react组件中的state.

我希望你能够将一些这些模式应用到你的代码中!查看下面的链接,了解有关解构的更多信息。
ES6 In Depth: Destructuring - Mozilla Hacks - the Web developer blog

Learn the basics of destructuring props in React

(完)

原文链接

How to deal with nested callbacks and avoid “callback hell”.md

Posted on 2020-04-12 | In translation

How to deal with nested callbacks and avoid “callback hell” (翻译)

(渣渣小怪兽翻译,如果看翻译不开心可以看->原文)

js是一门强大语言。有时候,你必须处理另一个回调中的回调,而这个回调这是另一个回调中的回调。

人们形象的描述这种模式为回调地狱。

它有点像这样:

1
2
3
4
5
6
7
firstFunction(args, function() {
secondFunction(args, function() {
thirdFunction(args, function() {
// And so on…
});
});
});

这是js中的回调。当你看到这样的嵌套回调, 它可能令你的大脑难以置信,但我不认为这是“地狱”.这个“地狱”可以更好的管理,如果你知道怎么处理它的话。

关于回调

我假设当你在阅读这篇文章的时候你知道回调的概念.如果你不知道, 请阅读这篇文章, 在我们继续往下走之前这篇文章会介绍什么是回调。在那里,我们讨论回调是什么以及为什么在JavaScript中使用它们。

回调的处理方案

这是四个处理回调地狱的方案

  1. 写注释
  2. 拆分函数成为更小的函数
  3. 使用Promise
  4. 使用Async/await

在我们拆分讲解这个解决方案之前, 让我们一起构造一个回调地狱. 为什么?因为它真的太抽象了,当我们看到上面的例子: firstFunction, secondFunciton 和 thirdFunction.我们让构造一个真是的回调, 让我们的例子更具体。

构造一个回调地狱

让我们想象, 现在我们要做一个汉堡.为了制作一个汉堡, 我们需要执行以下步骤来达到目的:

  1. 获取配料
  2. 烹饪牛肉
  3. 获取汉堡的面包
  4. 把煮好的牛肉放在面包之间
  5. 供应汉堡

如果这些步骤都是同步的, 你将看到一个函数像下面这样:

1
2
3
4
5
6
7
8
9
const makeBurger = () => {
const beef = getBeef();
const patty = cookBeef(beef);
const buns = getBuns();
const burger = putBeefBetweenBuns(buns, beef);
return burger;
};
const burger = makeBurger();
serve(burger);

然而, 在我们的步骤中,我们不能自己制作汉堡。我们必须指导助手制作汉堡的步骤。在我们指示助手之后,我们必须等待助手完成步骤,然后才开始下一步。

如果我们想在js中等待一会再执行, 我们就需要使用回调。为了制作汉堡, 我们不得不先获取牛肉。我们只能在获取牛肉后再煮牛肉。

1
2
3
4
5
const makeBurger = () => {
getBeef(function(beef) {
// We can only cook beef after we get it.
});
};

为了煮牛肉, 我们需要传递牛肉进入cookBeef函数内.否则我们将没有东西煮。因此我们不得不等待牛肉煮熟.

一旦牛肉煮好了,我们就要获取面包

1
2
3
4
5
6
7
8
9
const makeBurger = () => {
getBeef(function(beef) {
cookBeef(beef, function(cookedBeef) {
getBuns(function(buns) {
// Put patty in bun
});
});
});
};

在我们获取面包后,我们需要放肉饼在两个面包之间。这就是汉堡形成的地方.

1
2
3
4
5
6
7
8
9
10
11
const makeBurger = () => {
getBeef(function(beef) {
cookBeef(beef, function(cookedBeef) {
getBuns(function(buns) {
putBeefBetweenBuns(buns, beef, function(burger) {
// Serve the burger
});
});
});
});
};

最终, 我们可以提供汉堡了。但是我们不能从makeBurger中返回burger, 因为这是异步的。我们需要用一个回调去接收汉堡.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const makeBurger = nextStep => {
getBeef(function (beef) {
cookBeef(beef, function (cookedBeef) {
getBuns(function (buns) {
putBeefBetweenBuns(buns, beef, function(burger) {
nextStep(burger)
})
})
})
})
}
// Make and serve the burger
makeBurger(function (burger) => {
serve(burger)
})

制作这个回调的例子很有趣啊

解决方案1: 写注释

这个makeBurger回调是简单易于理解。我们可以读懂。它只是有一些不好看。

如果你第一次读到makeBurger这个函数, 你可能在想“为什么我们需要这么多回调才能制作汉堡呢?这没有意义!”

在这种情况下,您需要留下注释来解释您的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Makes a burger
// makeBurger contains four steps:
// 1. Get beef
// 2. Cook the beef
// 3. Get buns for the burger
// 4. Put the cooked beef between the buns
// 5. Serve the burger (from the callback)
// We use callbacks here because each step is asynchronous.
// We have to wait for the helper to complete the one step
// before we can start the next step
const makeBurger = nextStep => {
getBeef(function(beef) {
cookBeef(beef, function(cookedBeef) {
getBuns(function(buns) {
putBeefBetweenBuns(buns, beef, function(burger) {
nextStep(burger);
});
});
});
});
};

现在,不会是在想“WTF”, 当你看到这个回调地狱, 你可以理解为什么要用这个方式去写它了。

解决方案2:将回调拆分为不同的函数

我们的回调地狱的例子已经是一个很棒拆分的例子。让我给你一步步的拆分代码, 就会明白为什么我这样说了。

拿getBeef, 我们的第一个回调来说, 我们为了拿到牛肉不得不先去冰箱。厨房里有两个冰箱,我们需要去右手边的冰箱拿牛肉。

1
2
3
4
5
const getBeef = nextStep => {
const fridge = leftFright;
const beef = getBeefFromFridge(fridge);
nextStep(beef);
};

为了烹饪牛肉,我们需要把牛肉放进烤箱里, 把烤箱开到200度, 并且等20分钟

1
2
3
4
5
6
const cookBeef = (beef, nextStep) => {
const workInProgress = putBeefinOven(beef);
setTimeout(function() {
nextStep(workInProgress);
}, 1000 * 60 * 20);
};

现在想象一下, 如果你不得不写每个步骤在makeBurger ,那么这个函数就会非常的庞大。

有关将回调拆分为较小函数的具体示例,您可以在我的回调文章中阅读这一小部分。

解决方案3: 使用Promise

我猜你应该知道什么是Promise.如果你不知道, 请先阅读这一篇文章

Promise能让回调地狱更易于管理.而不是像上面的嵌套回调.你将看到像这样:

1
2
3
4
5
6
7
8
const makeBurger = () => {
return getBeef()
.then(beef => cookBeef(beef))
.then(cookedBeef => getBuns(beef))
.then(bunsAndBeef => putBeefBetweenBuns(bunsAndBeef));
};
// Make and serve burger
makeBurger().then(burger => serve(burger));

如果你更倾向于单参数风格的promise, 你能把上面的例子转换成这样

1
2
3
4
5
6
7
8
const makeBurger = () => {
return getBeef()
.then(cookBeef)
.then(getBuns)
.then(putBeefBetweenBuns);
};
// Make and serve burger
makeBurger().then(serve);

更易读和管理。

但是问题是, 如何把回调地狱转换成Promise?

把回调转成Promise

为了转成Promise, 我们需要为每个回调先new一个Promise.当这个回调成功执行,那么给这个Promise使用resolve返回。或当这个回调失败或者抛出错误的时候,我们就要使用reject。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const getBeefPromise = _ => {
const fridge = leftFright;
const beef = getBeefFromFridge(fridge);
return new Promise((resolve, reject) => {
if (beef) {
resolve(beef);
} else {
reject(new Error(“No more beef!”));
}
});
};
const cookBeefPromise = beef => {
const workInProgress = putBeefinOven(beef);
return new Promise((resolve, reject) => {
setTimeout(function() {
resolve(workInProgress);
}, 1000 * 60 * 20);
});
};

在我们的练习中,可能已经为您编写了回调函数。如果使用Node,则包含回调的每个函数将具有相同的语法:

  1. 回调函数将是最后一个参数
  2. 这个回调函数将一直有两个参数。这些参数都有相同的顺序(error是第一个, 随后是你感兴趣的任何东西)
1
2
3
4
5
6
7
8
9
10
11
12
// The function that’s defined for you
const functionName = (arg1, arg2, callback) => {
// Do stuff here
callback(err, stuff);
};
// How you use the function
functionName(arg1, arg2, (err, stuff) => {
if (err) {
console.error(err);
}
// Do stuff
});

如果你的回调已经有了相同的语法, 你可以使用像ES6 Promisify or Denodeify 把回调转换成Promise.如果你使用Node v8.0或者之上, 你可以使用util.promisify.

他们三个都可以使用。你可以选择任意一个库来使用。不过,每种方法之间都有细微的差别。我建议你查看它的文档,了解方法。

解决方案4: 使用异步函数(async/await)

为了使用异步函数, 你首先需要知道两件事。

  1. 如何把callback转换为Promise(就是我们解决方案三中说的内容)
  2. 怎么使用异步函数(如果你需要帮助,你可以读一下这篇文章)

使用异步函数,您可以编写makeBurger,就像写同步一样!

1
2
3
4
5
6
7
8
9
const makeBurger = async () => {
const beef = await getBeef();
const cookedBeef = await cookBeef(beef);
const buns = await getBuns();
const burger = await putBeefBetweenBuns(cookedBeef, buns);
return burger;
};
// Make and serve burger
makeBurger().then(serve);

我们可以在这里对makeBurger进行一项改进。 你可以同时获得两个getBuns和getBeef的助手。 这意味着您可以使用Promise.all语法, 然后使用await。

1
2
3
4
5
6
7
8
const makeBurger = async () => {
const [beef, buns] = await Promise.all(getBeef, getBuns);
const cookedBeef = await cookBeef(beef);
const burger = await putBeefBetweenBuns(cookedBeef, buns);
return burger;
};
// Make and serve burger
makeBurger().then(serve);

(注意: 你可以在Promise做到相同的效果, 但是它的语法并不是很棒, 不像async/await那么清晰。)

总结

回调地狱并不是像你想象中的那么可怕.这里有四种方式管理回调地狱。

  1. 写注释
  2. 切分函数为更小的函数
  3. 使用Promise
  4. 使用async/await

正则.md

Posted on 2020-04-12 | In fcc

正则:

  1. /a|b|c|d/ 匹配a或者b, c, d

  2. 用通配符.匹配任何内容

  3. 匹配a, b 或 c

  4. 匹配集合 [0-9] [a-z0-9]

  5. 匹配差集 [^0-9] // 注意, 这个^ 在字符集里面是否定的意思, 而在字符集外面, 是开始的意思

  6. 匹配1个或者多个a : /a+/

  7. 匹配零个或者多个ao : /ao*/ 会匹配出 a

  8. 惰性匹配: 匹配长度为最小字符串 /<.*?>/ (注意了, js中的正则本身是贪婪匹配)

  9. 前置断言 (<=[xxx]?)

  10. 以xxx开始的字符串: /^xxx/

  11. 以xxx结尾的字符串: /xxx$/

  12. 元字符:
    \w // 所有的数字和字母
    \W // w的差集
    \d // 所有数字
    \D // 所有非数字
    \s // 此匹配模式不仅匹配空格,还匹配回车符、制表符、换页符和换行符,你可以将其视为与[\r\t\f\n\v]类似。所有非数字字母
    \S // 匹配所有非空白字符
    /a{3,5}h/ // 指定匹配模式的上界下界
    /a{3,}h/ // 指定匹配模式的下界
    /a{3}h/ // 指定数量匹配
    /colou?r/ // 模糊查询可选项: u就是可选项, 可以有, 也可以没有, 但是不可以是其他字符

  13. 前置断言 (?=…) 以…开头
    正向先行断言会查看并确保搜索匹配模式中的元素存在,但实际上并不匹配。

  14. 后置断言 (?!…) 不以…开头
    负向先行断言会查看并确保搜索匹配模式中的元素不存在。

  15. 使用捕获组重用模式

    1
    2
    3
    let repeatStr = "regex regex";
    let repeatRegex = /(\w+)\s\1/;
    repeatStr.match(repeatRegex); // Returns ["regex regex", "regex"]
  16. 使用捕获组搜索和替换

    1
    "Code Camp".replace(/(\w+)\s(\w+)/, '$1'); // 此时, 使用括号括起来的元素就是一个组, 这个组会命名为$1 - $n在替换的时候可以使用到
  17. 用正则表达式写一个trim

    1
    2
    3
    let hello = "   Hello, World!  ";
    let wsRegex = /(\s+)(\w+.+\w+\S)(\s+)/;
    let result = hello.replace(wsRegex, '$2');
  18. 注意 reg.test()中的reg正则对象存在一个值, 这个值表示我使用test()方法匹配的起点index, 如果稍不注意,就很容易翻车

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    (function(){
    const reg = /o/g;
    function isHasO(str){
    // reg.lastIndex = 0; 这样就可以避免这种情况
    return reg.test(str)
    }
    var str = 'google';
    console.log(isHasO(str))
    console.log(isHasO(str))
    console.log(isHasO(str))
    }())

Here are some practical JavaScript objects that have encapsulation.md

Posted on 2020-04-12 | In translation

Here are some practical JavaScript objects that have encapsulation (翻译)

(渣渣翻译,如果看翻译不开心可以看->原文)

封装意味着隐藏信息.意思是尽可能的隐藏对象内部的部分, 同时暴露出最小公共接口。

最简单和最优雅的方式创建一个封装是使用闭包.可以将闭包创建为具有私有状态的函数.当创建了许多的闭包共享相同的私有状态, 我们就会创建一个Object.

我将开始创建一些在我们开发应用中比较实用的Object: Stack, Queue, Event Emitter, and Timer.以上Object的都使用工厂函数创建。

让我们开始吧。

Stack

栈,是一种数据结构, 它有两个主要操作: push, 往这个集合里增加一个元素; pop, 移除最近添加的元素.其中,push和pop元素依据先进后出原则。

让我们看下一个例子:

1
2
3
4
5
6
let stack = Stack();
stack.push(1);
stack.push(2);
stack.push(3);
stack.pop(); //3
stack.pop(); //2

让我们使用工厂函数实现一个stack:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Stack(){  
let list = [];

function push(value){
list.push(value);

}
function pop(){
return list.pop();
}

return Object.freeze({
push,
pop
});
}

这个栈对象有两个公共方法push()和pop().内部的状态只能通过这些方法去改变.

1
stack.list; // undefined

我不能直接修改内部状态:

1
stack.list = 0; //Cannot add property list, object is not extensible

使用class实现Stack结构

如果我使用类完成相同的实现,则没有实现封装

1
2
3
4
5
let stack = new Stack();
stack.push(1);
stack.push(2);
stack.list = 0; //corrupt the private state
console.log(stack.pop()); //this.list.pop is not a function

这是我使用class实现的stack

1
2
3
4
5
6
7
8
9
10
11
12
13
class Stack {  
constructor(){
this.list = [];
}

push(value) {
this.list.push(value);
}

pop() {
return this.list.pop();
}
}

如果要看更深的对比(class和工厂函数), 可以看一下这篇class vs Factory function: exploring the way forward

Queue

队列是一种数据结构, 有两个主要操作:入队和出队。

入队: 往我们的集合里增加元素。

出队: 移除在集合里最早加入的元素。

出队和入队操作遵循, 先进先出的原则。

这是使用队列的一个例子

1
2
3
4
5
6
let queue = Queue();
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
queue.dequeue(); //1
queue.dequeue(); //2

以下是队列的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Queue(){  
let list = [];
function enqueue(value){
list.push(value);
}
function dequeue(){
return list.shift();
}
return Object.freeze({
enqueue,
dequeue
});
}

就像我们之前看到的, 这个对象的内部不能从外部直接访问。

1
queue.list; //undefined

Event emitter

一个发布订阅机制是一个有发布和订阅的API的一个对象.它常用于一个应用里两个不同的部分的通信.

来看一个使用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 初始化事件对象
let eventEmitter = EventEmitter();
// 订阅事件
eventEmitter.subscribe("update", doSomething);
eventEmitter.subscribe("update", doSomethingElse);
eventEmitter.subscribe("add", doSomethingOnAdd);

// 发布(触发之前的订阅)
eventEmitter.publish("update", {});

function doSomething(value) { };
function doSomethingElse(value) { };
function doSomethingOnAdd(value) { };

首先, 我为update事件订阅了两个函数,并为add事件添加了一个函数.当事件触发发布“updata”事件的时候, doSomething和doSomethingElse都会被调用.

这是事件对象的一个简单实现:

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
function EventEmitter(){  
let subscribers = [];
function subscribe(type, callback){
subscribers[type] = subscribers[type] || [];
subscribers[type].push(callback);
}
function notify(value, fn){
try {
fn(value);
} catch(e) {
console.error(e);
}
}
function publish(type, value){
if(subscribers[type]){
let notifySubscriber = notify.bind(null, value);
subscribers[type].forEach(notifySubscriber);
}
}

return Object.freeze({
subscribe,
publish
});
}

订阅者的状态和通知方法是私有的。

Timer

众所周知, js中有两个计时器函数:setTimeout 和 setInterval.我想以面向对象的方式与计时器一起工作,最终将以如下的方式调用:

1
2
3
let timer = Timer(doSomething, 6000);
timer.start();
function doSomething(){}

但是setInterval函数有一些限制,在进行新的回调之前,它不会等待之前的回调执行完成。即使前一个尚未完成,也会进行新的回调。更糟糕的是,在AJAX调用的情况下,响应回调可能会出现故障。

递归setTimeout模式可以解决这个问题.使用这个模式, 一个新的回调形成只能等前一个回到完成之后。

让我们创建一个TimerObject:

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
function Timer(fn, interval){
let timerId;
function startRecursiveTimer(){
fn().then(function makeNewCall(){
timerId = setTimeout(startRecursiveTimer, interval);
});
}

function stop(){
if(timerId){
clearTimeout(timerId);
timerId = 0;
}
}

function start(){
if(!timerId){
startRecursiveTimer();
}
}

return Object.freeze({ start, stop});
}

let timer = Timer(getTodos, 2000);timer.start();

只有start和stop方法是公共,除此之外, 所有的方法和变量都是私有的.调用setTimeout(startRecursiveTimer,interval)时,没有this(指向问题)丢失的上下文问题,因为工厂函数不使用this。

计时器使用返回promise的回调。

现在, 我们可以很简单的实现,当浏览器的tab隐藏的时候, 计时器停止, 当浏览器的tab显示的时候, 计时器继续计时.

1
2
3
4
5
6
7
8
9
10
11
document.addEventListener("visibilitychange", toggleTimer);

function toggleTimer(){
if(document.visibilityState === "hidden"){
timer.stop();
}

if(document.visibilityState === "visible"){
timer.start();
}
}

#总结
JavaScript提供了一种使用工厂函数创建封装对象的独特方法。对象封装状态。

Stack和Queue可以创建为基本数组功能的包装器。

事件对象是在应用程序中的不同部分之间进行通信的对象。

计时器对象易于使用。它有一个清晰的接口start()和stop()。您不必处理管理计时器标识(timerId)的内部部分。

您可以在我的Discover Functional JavaScript一书中找到有关JavaScript中的功能和面向对象编程的更多内容。

3. advanced_scoped.md

Posted on 2020-04-12 | In deep-js-foundations-v2-noteBook

Lexical & Dynamic Scoped

lexical scoped: 指的是编译器, 解析器, 处理器执行之前,进行了一此预编译, 这就是词法作用域。词法作用域不是在运行时决定的, 是在编译的时候决定的。

Dynamic Scoped: 像bash是一种解释型语言, 他的作用域是动态作用域。因此它没有词法作用域, 不是编译型语言。

Lexical Scoped

  1. 词法作用域,其实映射的概念就是预编译,js在运行之前进行了预编译,把作用域, 和作用域内的变量以及函数信息都保存起来, 供引擎运行时使用。
  2. 词法作用域定下来就确定了, 一次编译,运行时就可以对此进行优化。
  3. 作用域之间是层级关系。

Dynamic Scoped

  1. 动态作用域指的是, 黑箱, 我只有打开它(运行的时候)才知道是怎么回事。
  2. 动态作用域下, 你对一个变量引用的结果取决于你的上下文环境。
  3. 所以, 动态作用域是在运行时候确定的, 而词法作用域是在编写代码时确定的。但是js有一种机制, 提供了类似的动态作用域的效果。

function scoped

  1. 函数作用域给了我们隔离外部作用域的机会(提供了命名空间), 我们应该尽量的保持函数内操作的变量私有。
  2. 并且持续遵循最小变量原则。函数内的变量函数内自己去创建和消化(object记得要深拷贝)。

IIFE Pattern(立即执行函数表达式模式)

  1. 使用函数表达式,创建一个作用域, 立刻调用它, 产生了作用域后, 执行函数体内容, 作用域消失。只运行一次,就消失了, 不会留下副作用。比如变量污染。
  2. IIFE是表达式, 而不是函数定义式。
  3. 如果你需要一个函数表达式,那么你完全可以使用IIFE去做这件事。比如try-catch语句
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    let a = (() => {
    try {
    someFn() // return value.
    }catch() {
    return 'defaultValue'
    }
    }())

    let value = "some";
    ((value)=> {
    console.log(value);
    value = "big";
    })(value);
    console.log(value); // some

Block Scoping

块作用域的特点: 只有在使用let/const定义时的作用域有效, 出了定义的作用域就无效.

块作用域的产生:

  1. 使用IIFE
    1
    2
    3
    4
    5
    6
    let value = "some";
    ((value)=> {
    console.log(value);
    value = "big";
    })(value);
    console.log(value); // some
  2. 在花括号内使用let/const定义
    1
    let a = 2; {let a = 1; console.log(a)};console.log(a)

let vs var (个人认为: const > let > var)

有作用域好过没有作用域。不用考虑边界情况.

当然,除非你想利用hosting(提升), 那么可以使用var.

一个有趣的冷知识:

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

// a在内存中中是同一个地址, 因为在第一次定义的时候在内存中已经有a这个变量了, 第二次定义做了一次查询而已.

explicit let block

1
2
3
4
5
6
7
8
9
(sum) => {
{ // 大胡子推荐这种写法,显示的去告诉大家我需要let的块级作用域, 明确变量的范围
let a = 1,
b = 2,
c = 3;
sum = a + b + c;
}
return sum;
}

const

你应该优先使用const, 之后再是let.

当时,const也存在一定的问题, 使用const, 不可以二次赋值。但是如果const定义的不是基础类型(string/boolea/number), 而是数组的话, 那么const的行为就有些怪异了。历史因素导致的, 当我们使用object类型(数组)的时候, 变量内存的是一个地址,而,这个地址中存的是对象属性中的地址。而我们修改属性的地址时, 实际上, 变量的地址是没有改变的。来看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const arr = [1, 2, 3]
arr = [2, 3, 4] // Throw Error
arr[0] = 2;
arr[1] = 3;
arr[2] = 4;

const obj = {
a: 1
}

obj = {} // Throw Error
obj.a = 2;

// 解决方案: freeze 但是注意, 分浅冻结和深冻结
Object.freeze();

从语义上理解, const是常量,意味着我不会再给他重新赋值。

遵循两个使用原则: 1. 原语类型 2. 只赋值一次, 不会再次赋值

Hoisting

在es的规范中,没有提及提升,我们所说的提升其实是一个比喻。就像我们说的词法作用域,都是一种比喻,一种理解这个概念的介质, 来看个例子:

1
2
3
4
a; // undefined
b; // undefined
var a = 1;
var b = 2;

实际上,js引擎把以上的代码转换成了这样: 把变量定义,函数定义自动的放在函数执行的顶部。

1
2
3
4
5
6
7
8
var a;
var b;


a; // undefined
b; // undefined
a = 1;
b = 2;

提升的特性: 变量定义提升,函数定义提升, 函数表达式不提升。

1
2
3
4
5
6
7
8
a(); // a
function a () {
console.log("a");
}

b(); // b is not a function
var b = () => {console.log("b");};
b(); // b

关于函数提升这一点,我们能理解到, 函数提升, 其实本质上就是做了C语言中,提前把要用的函数暴露出来,这一点他们是一致的。

同时,我们也可以用预编译来解释这件事, 预编译只对LSH做处理,也就是只处理收集了var b这个信息,而没有获取到RSH的信息,所以在使用b()的时候,没能获取到函数体本身.获取到函数体本身要到执行阶段才能获取到.

Hoisting explames

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 'a';
function b () {
console.log(a); // undefined
var a = 'b';
}

// 实际上,js引擎做了以下处理
var a = 'a';
function b () {
var a; // 初始化未赋值, 那么所有的初始值都是 undefined
console.log(a); // undefined
a = 'b';
}

其实得出这个结果一点也不怪异, 想想之前说的, 预编译阶段,在预编译阶段,function b 中就存有自己的变量a, 那么在执行阶段,console.log语句拿a变量其实是从,b函数的作用域中拿a。

let doesn’t hoist? No.

1
2
3
4
{
a = 'a'; // TDZ error
let a;
}

这有个概念叫临时死区, 在用let定义之前的区域都为死区,不可以在该区域使用a变量, 否则会报错。

大胡子说, 其实var和let/const都进行了提升,只是提升的行为不一样, var提升了变量定义,并且初始化了这个变量, 赋予初始值undefined.但是let/const也进行了提升, 只是没有进行初始, 这里没有进行初始化等价于不可以去使用它(是不是很像临时死区)

Understanding Prototypes in JavaScript.md

Posted on 2020-04-12 | In translation

Understanding Prototypes in JavaScript

Prototype是一个怪异并且我们极力避免的一个话题,但是对于顶级的js开发者来说尤为重要.虽然类更容易理解,并且在学习面向对象编程时通常是许多人的起点,但原型是构建JavaScript的基础.

它看起来像类, 闻起来也像, 但是它不是类。因此,忘记你知道的关于class和面向对象语言的概念, 如果像真正的理解prototype的概念。

不, 我是个骗子。来, 让我们快速的回顾一下传统的类以及面向对象编程是如何工作的。

Classes-the master blueprint

我的想法是,所有东西都是某种对象,所有你需要做的就是为那个对象创建“蓝图”,并在每次需要它的唯一实例时实例化新对象。

类往往是举例。然而, 一个类是仅仅是一个对象创建的模式。其他语言也存在原型,但是在js中使用的原型和java等语言的原型不同。这种语言只是在名称上类似, 但是本质上是完全不同的语言。

类(不是特定于语言的)通常具有构造函数,其中的方法被调用以执行某种操作。它们是以这种方式构造的,因为它为对象提供了清晰的界限和环形围栏 - 这个想法是所有对象都是’事物’而所有’事物’都具有属性和功能。

然而,现实并不总是那么明确,定义对象往往很困难。你能共享一个方法, 创建一个超类或者子类但这是另一个不同的故事了。有时, 但你创建一个class, 它能复制和别的class类似的特性, 但并不是相同的class.

Prototype-Do you have a pen?

别误会我的意思, class是一套很好的编程范式。然而js建立于Prototype之上。

一个prototype像一个结构的class, 它被加总在一起通过引用的方式。如果一个class像一个蓝图, 那么prototype就像是在问你-do you have a pen? 如果答案是不。那么你会去问你朋友这个问题。如果他的回答也是不, 那么你的朋友回去问你朋友的朋友。这个过程会持续到没朋友的那个人。

当你在js创造了一个函数, js引擎会自动创建一个空的容器并且命名为prototype, 为了挂载你的方法。

因此, 如果你写了一个函数funciton x() {}, 并且把它打印出来你可能会看到下面的内容:

这意味着, 你能增加一些东西到prototype, 通过一些像这样的方式:

1
2
3
4
function x () {};
x.prototype = {
sayHello: 'Hello'
};

这些将会展示在prototype下并且让我们调用,当我们通过这个函数实例化一个对象的时候。

原型模式的工作原理是函数/方法只编写一次,并且原型对象可以通过原型链引用和使用。

有些困惑是吗?来看个图表。

连接函数时,原型中设置的方法从左到右可用。x和y中的任何内容都可用于z,但不是相反。希望之前的那笔比喻现在更有意义了。

New Initialization Pattern

这有四种方式初始化并且创建一个prototype链,但是在这篇文章里, 我做一个例子就是新初始化模式。我发现这个模式更简单对于演示原型继承以及原型链,特别是那些仍在努力吸收和理解这个想法的人。

让我们开始,从一个空的car函数。你可以在里面有东西但是为了这个演示的目的,我们将把它留空。

1
2
3
function Car() {

}

我们现在往Car的prototype里增加以下属性.

1
2
3
4
Car.prototype = {
washCar: 'wash',
doors: true
}

现在,当我们用console.dir打印出Car的时候, 我们可以看到这两个属性出现在prototype里。

让我们创建另一个函数以便可以链接他们并且获得继承的效果。

1
2
3
function Honda () {

}

我们有Honda函数.现在我们去创建一个引用并且链接到Car, 通过实例化这个Car函数(使用new关键字)

1
let HondaProto = new Car;

你也可以设置你自己的prototype方法在HondaProto上, 像这样

1
2
3
HondaProto.limit = function() {
console.log('limit');
}

为了链接你的Honda函数与我们定制的prototype蓝图, 只需要把Honda.prototype设置为HondaProto

1
Honda.prototype = HondaProto;

OK, 现在, 如果你使用console.dir(Honda), 你将可以看见这个函数Honda()现在已经有了limit函数, 同时,当你访问prototype的时候你可以看到Car的两个属性washCar/door在proto下.

让我们增加更多的函数在这条继承链上, 因此你可以看到这条链在console.log打印的内容里。

1
2
3
4
5
6
7
8
9
10
11
function Electric () {

}

let ElectricProto = new Honda;

ElectricProto.electric = function () {
console.log('electric');
}

Electric.prototype = ElectricProto;

相同的方式实例化Electric对象, 得到的console信息如下。这将增加一个新的proto在原有的proto下方嵌套。

现在, 如果你使用Electric初始化一个新对象并且调用wash方法, 该对象将可以访问所有Car对象的方法和内部设置。子级域链也会继承任何父级链进一步发生变化的变化。

1
2
let LX4Honda = new Electric();
console.log(LX4Honda.washCar);

console.log将打印出 “wash”.

尽管LX4没有叫washCar的方法, 它会沿着原型链往下直到找到这个方法.如果没有匹配, 将返回undefined.

您还可以将一个原型函数附加到另一个链,从而共享该函数。 创建这种关系的方法与上面说明的完全相同。 从逻辑上讲,如果我们继续使用到目前为止的示例,它应该看起来像这样。

Final words

原型以及原型链的继承最有效的地方是,你没必要创建一个全新的桶来容纳里面的所有方法。相反,你只需要创建一个指向这个函数的引用仅当你需要的时候去调用就好。对于类,这些方法坐在桶中等待被调用。

希望上面这个例子对你有些意义, 并且可以帮助你更好的理解原型。这还有一些其他的方式初始化并且创建一个原型继承链, 但是要控制好链的长度,防止他们混淆你。我已经从这里省略了它们,并向您展示了如何使用新的初始化模式创建继承链。但是,你应该去看看Object.create,Object.setPrototypeOf和proto。

这三个列出的方法同样有价值,是创建原型继承链的绝佳选择。

让我们保持联系,加入我每周很棒的网页摘要通讯列表。

谢谢你的阅读

(完)

2. scope.md

Posted on 2020-04-12 | In deep-js-foundations-v2-noteBook

Scope 作用域

overview

  1. Nested Scope
  2. Hosting
  3. Closure
  4. Modules

当你遇到作用域相关的问题,回答两个问题

  1. What position is it in? (该变量在哪个位置)
  2. What scope does it belong to? (该变量在那个作用域)

js是一种编译型(解析型)语言: 当在逐行执行代码之前,js引擎会先扫描一次代码, 如果你有任何的语法错误,js会抛出错误。(所以它在执行语句的时候就编译了一次?)

在编译理论中: 编译是高级语言编写转换成机器可执行代码的处理过程

  1. lexing and tokenization.(就是把代码转成抽象语法树AST: abstract syntax tree)

  2. code generation(把AST转换成计算机可执行的代码)

    很多观点认为, js是即时编译的, 就像薛定谔的猫,如果你在第十行有个语法错误,那么你在运行到第十行才发现。这个观点是错误的, 因为,如果我们第十行有错误,那么1-9行都不会执行。并且抛出错误.

所以,在js执行之前,会先走一次预编译, 这次编译会生成AST.其实这和那些静态语言类似, 比如java.js并不是运行时编译的即时编译语言.

函数和块作用域一起,组成了js的scope.

compilation & Scope

在编译阶段,我们有个scope管理器和一个编译器。
编译过程中,scope管理器中会维护一个作用域, 每个作用域里存放变量,以及变量的初始值undefined。在处理完所有的作用域时, 我们处理后的信息移交给js引擎, 用于执行。

所以,js所获得关于scope的信息,是在第一次编译的时候获得的,然后在运行时使用这些信息, 但是这些信息是在编译的时候确定的.

这样有一个好处,就是我在编译阶段获取到的信息可以有很多优化的空间。

excute code

编译阶段与执行阶段是两个不同的过程。

在编译阶段,我们更多的去处理的是定义语句(LHS): var/let/const/funcion

除此之外,剩下的都是执行阶段要做的事.

let a = 1;

let a is LHS
= 1 IS RHS

在执行阶段,js引擎获得了变量的作用域信息(这些引用信息其实就是每个变量现在的内存地址, 代码执行的时候其实也就是不断的往内存里放东西),就开始执行, 关于变量,还有定义的信息,都从编译阶段获取的scope信息中获取.(我们写完代码后,作用域是不会变的)

查找顺序, 本级作用域-> 父级作用域->父级的父级作用域-> …… -> 全局作用域 -> undefined

所以在js运行的时候分两个阶段,1. 获取变量(函数也是一种特殊的变量)信息, 也就是预编译的过程 2. 根据这些信息执行运算(执行代码内容)

lexical scope review

js不是interpreted(解释型)语言和逐行执行的语言。逐行执行只是在他运行的第二阶段的执行顺序。js是编译型语言,编译过程分为两个阶段: 1. 在LHS切分作用域, 获取变量信息(对应的东西放进对应的篮子里), 在这个阶段我们可以提前发现错误, 完成信息收集后,把收集到的信息传递给js引擎(由于我们提前获取到了信息,在我们的执行阶段可以进行进一步的优化) 2. 拿到变量信息,开始执行RHS, 执行阶段使用阶段1获取的信息,进行运算

基于它的编译模型, 也就不奇怪为啥所有变量一开始的初始值都是undefied了。

现在我认为,我们的代码就是往内存中放东西。

Dynamic global variables

1
2
3
4
5
6
7
8
function a() {
c = 0;
}
c // throw error

a()

c // 0

Js的历史原因导致了这种动态全局变量的产生.尽可能的避免产生这种全局变量.

nested scope

嵌套scope的分析和单层scope分析一样,但是多层嵌套还是会减小代码的可读性,让代码不清晰.

error type

  1. reference error: 函数调用产生
  2. undefined error: 变量没有定义产生

undefined and undeclared

undefined: 你定义了一个变量而为赋值, 那么这个变量就是undefined

undeclared: 你使用了一个变量(访问了也算),但是这个变量在内存中找不到(也就是在编译的第一阶段,收集变量信息的时候就报错了)

5.closure.md

Posted on 2020-04-12 | In deep-js-foundations-v2-noteBook

Closure

闭包已经是计算机编程领域中很流行的概念。

Brendan Eich: js的作者
一开始是想把scheme放在浏览器跑的。(scheme: 一种旧的函数式编程语言)
但是网景公司想要一种类似java的语言。所以….结合两者, 出现了javascript。

其实我们都在使用闭包的概念进行编程, 像异步ajax.

What’s Closure?

Closure是函数有能力记住和访问变量的词法作用域, 在函数本身已经执行完成后。(数据持久性)

这是因为,js引擎的垃圾回收机制, 执行我们的代码的时候,js维护着一个调用栈。在函数执行完成的时候,由函数的垃圾回收机制去处理这个调用栈(调用栈内包含函数的词法作用域), 要销毁的时候,发现还存在引用。那么垃圾回机制就不处理它。这就导致这个词法作用域保留了下来,也让该函数具有了数据持久性。

注意了,学院派倾向于把闭包的概念应用在单个的变量上,以变量为单位。而在js引擎里,闭包是基于词法作用域的。因此,如果你有个变量, 存在大量的数据, 那么该变量是不会被垃圾收集机制收集的。

1
2
3
4
5
6
const timer = (text) => {
setTimeout(() => {
console.log(test)
}, 1000)
}
timer('Hello World.')
1
const printer = x => () => console.log(x);

Closure var

1
2
3
4
5
6
7
8
9
10
var teacher = 'Kyle';

var myTeacher = () => {
console.log(teacher);
// 这里的变量引用是软链接,不是自己创建一个封闭的作用域
// 它并没有把这个变量抓住, 生成一个副本
}

teacher = 'Suzy';
myTeacher(); // Suzy
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
for(var i = 0; i < 4; i++) {
setTimout(() => {
console.log(i)
}, i * 1000);
}

// 解决方案1: let
for(var i = 0; i < 4; i++) {
let j = i;
setTimout(() => {
console.log(j);
}, j * 1000);
}
// 解决方案2: 匿名函数
var main = () => {
for(var i = 0; i < 4; i++) {
((i)=> { // 在setTimeout外面写一个匿名函数,新建作用域, 形成闭包变量
setTimeout(() => {
console.log(i)
}, i * 1000);
})(i);

}
}
main();
// 解决方案3: 匿名函数
var main = () => {
for(var i = 0; i < 4; i++) {
setTimeout(((i) => {
return () => {
console.log(i)
}
})(i), i * 1000);
}
}
main();

Module Pattern

要了解什么是Module, 让我们先看什么不是Module.

1
2
3
4
5
6
7
var workspace = {
name: 'Kyle',
ask (quetion) {
console.log(quetion);
}
}
// 这是命名空间,并不是模块化

模块的化的概念中有一个封装的概念, 其实指的是隐藏数据和行为, 只暴露出单一的接口供使用者使用.而封装内部的东西都是私有的,对于外部来说是不可见和不可操作了。唯一可以交流的只有暴露出来的接口。

Modules encapsulate data and behavior(Method) together.The state(data) of a module is held by its methods via closure.

1
2
3
4
5
6
7
8
9
10
11
var workspace = (function Module(teacher) {
var publicAPI = { ask };
return publicAPI;

function ask (quetion) {
console.log(teacher, quetion);
}

})('Kyle');

workspace.ask('what?');
1
2
3
4
5
6
7
8
9
10
11
12
13
 
function Module(teacher) {
var publicAPI = { ask };
return publicAPI;

function ask (quetion) {
console.log(teacher, quetion);
}

};

var workspace = Module('Kyle');
workspace.ask('what?');

在早期, js中很多框架和工具中,都使用这种方式去模块化。

module and node.js

ES6 module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// file is workspace.mjs
var teacher = 'Kyle';

// 暴露单个ask
export default function ask (quetion) {
console.log(teacher, quetion);
}

// 或者使用多个
export default {
ask,
}

// main.js
import ask from 'workspace.mjs';

ask('what?');

// 或者你可以给你模块赋予别名

import * as workspace from 'workspace.mjs';

workspace.ask('what?');
<1234…7>

Hawei

Fullstack Engineer

61 posts
16 categories
3 tags
RSS
© 2022 Hawei
Powered by Hexo
|
Theme — NexT.Muse v5.1.4