ZB-056-05JVM字节码

字节码(IR 中间表示)

字节码——平台无关的基石

  • 基于栈(操作数栈)的、与平台无关的计算模型
  • 可以由多种方式生成
    • JVM上的编程语言
    • 直接生成字节码

JVM如何实现平台无关性?

  • JVM中所有代码,都是自己定义的一套字节码流程
  • 它的字节码中你没有看到任何和操作系统有关的指令

    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
    // class version 49.0 (49)
    // access flags 0x21
    public class Main {

    // compiled from: Main.java

    // access flags 0x1
    public <init>()V
    L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
    L1
    LOCALVARIABLE this LMain; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

    // access flags 0x9
    public static main([Ljava/lang/String;)V
    L0
    LINENUMBER 3 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ICONST_5
    INVOKESTATIC Main.f (I)I
    INVOKEVIRTUAL java/io/PrintStream.println (I)V
    L1
    LINENUMBER 4 L1
    RETURN
    L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

    // access flags 0x9
    public static f(I)I
    L0
    LINENUMBER 7 L0
    ILOAD 0
    ICONST_2
    IF_ICMPGE L1
    L2
    LINENUMBER 8 L2
    ICONST_1
    IRETURN
    L1
    LINENUMBER 10 L1
    ILOAD 0
    ILOAD 0
    ICONST_1
    ISUB
    INVOKESTATIC Main.f (I)I
    IADD
    IRETURN
    L3
    LOCALVARIABLE n I L0 L3 0
    MAXSTACK = 3
    MAXLOCALS = 1
    }
  • 它在运行时和任何东西都没关。这也是它能跨平台的基石

    • 它的基石就是 字节码,一种中间表示
  • 它的所有操作都是在栈上完成的
  • JVM满脑子都是 上面那样的字节码,它不管什么 java / ruby / groovy ,只执行字节码

如何生成java识别的字节码呢?

  • java包含两种规范 JLS 和 JVMS
  • JVM只执行 字节码那一坨东西,而java语言和他没任何关系。它们之间的联系是 java编译器
    • 也就是说从 源代码(人类能看懂) 到 JVM能识别的字节码的过程 需要经过编译器

为什么Java设计的时候要把 JLS / JVMS 分开

  • 为了未来允许其他语言生成 字节码,因为JVM只认识 字节码
  • java有意的将它们分开使得你可以在 JVM上执行不同的语言 ruby/ groovy

还有一种直接生成字节码

  • 如果你有兴趣,请打开你的 Main.class文件,你需要一个特殊的工具去查看字节码的东西。如16进制编辑器。
  • 打开后你会发现,它会以字节的方式定义了一组非常紧凑的模型,我们称之为字节码。它的基本单元是一个一个的字节。那它和源代码的区别呢?
    • 人类能读懂的我们称之为 字符文件
    • 机器能读懂的我们称之为 字节码
    • 其中最重要的字节码标识是 Magic number) ,你看到java的 magic number 是 CAFEBABE。几乎所有的文件都有 magic number,用来标识是什么文件

java自带的工具

  • javap Main查看类信息 或者 javap -v Main 查看更多信息
  • 如果你喜欢图形化工具可以搜 classpy
  • idea插件 bytecode

如果你不嫌烦,当然可以手工生成一个一个的字节,只要它符合java字节码的规定,它就能被java所识别

  • 但一般没人这么无聊。。。

一个项目里可以同时存在Java和 Groovy代码吗,并且可以互相调用?

  • 可以,参考gradle代码 如 gradle仓库里
    • main里是 java代码
    • test里是 groovy代码
    • 并且它们可以互相调用。所谓的互相调用就是JVM上的互相调用

ZB-055-04JVM运行时内存区域

JVM运行时内存区域

方法区

  • 被整个虚拟机所共享的Class信息等
  • 运行时常量池
  • Java7之前:永久代(PermGen)
  • Java8之后:元空间(Metaspace)

我们知道每当你 new Object() 或者 new Cat()的时候,都会在堆内开辟空间,而在你分配空间之前

  • 你首先得知道 Object 长什么样子,也就是 Class,Class就是那个模子,那个说明书,它指明了这个对象如何被创建
  • 因此 Object.class 和 Cat.class 就是你创建 Object / Cat 对象时候参照的说明书
  • 这个说明书放在哪里呢? 我们称之为 方法区

方法区中存的一般都是这些类型信息,被JVM所有人所共享,一般来说是只读的

  • 意外情况就是 运行时常量池

看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = new String("abc");
String s3 = s1.intern();
String s4 = s2.intern();

System.out.println( s1 == s2); // false
System.out.println( s3 == s4); // true
System.out.println( s3.equals(s4)); // 进入 equals if 分支
System.out.println( s1.equals(s2)); // 进入 equals else 分支
}
}
/*
s1 == s2 返回 false可以理解 因为它比较的是引用 ——-》 地址
s3 == s4 为什么 true ? 点击 intern() 进入发现 它是一个 native 方法 意思是它没有java代码的由 java虚拟机帮你实现
intern() 意思是 在一个你看不到的地方 一个池子 Pool 存了很多字符串 ,如 "abc" ,保证你每次调用的时候返回的都是同一个字符串实例
所以 intern() 你可以理解为 内联化,内部表示
*/

intern()的注释非常好

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
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java&trade; Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();

/**
*返回字符串对象的规范表示形式。
*<p>
*一个字符串池,最初是空的,由
*类{@code字符串}。
*<p>
*如果池中已经包含了
* string等于这个{@code string}对象
* {@link #equals(Object)}方法,则池中的字符串为
*返回。否则,将此{@code字符串}对象添加到
*池和这个{@code String}对象的引用被返回。
*<p>
*对于任意两个字符串{@code s}和{@code t},
* {@code s.intern () = = t.intern()}是{@code真的}
*当且仅当{@code s.equals(t)}为{@code true}时。
*<p>
*所有字符串和字符串值常量表达式都是
*实习。的第3.10.5节定义了字符串字面量
* <引用> Java™语言规范< /引用>。
*
* @返回一个与该字符串内容相同的字符串,但是
*保证来自唯一字符串池。
* /
public native String intern();

为什么要 intern() 答案就在 String.equals()

equals 方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}

// 如果你要比较两个字符串 它就会跑一个循环,比较每个字符。假如你这个字符串有 几百个几千个字符,意味着你运行 equals 就要比较那么多的次数,效率非常低

假如碰巧这两个对象是同一个对象的话,它的地址是一样的话,只需要一次就可以完成
所以Java的 Collection 体系非常依赖 equals
如你把 String 声明的东西 当作 map的 key 。做成 Set / List 它们比较的时候都会使用 equals
有这样字符串常量池的意义就在于,如果有个常量池,可以对于每个字符串都返回一摸一样对象的时候,用来比较它们相等性的时候就可以通过一个 指令 this == anObject 就完成,效率高很多

子类对象创建的时候同时创建父类对象吗? 是不是有n个Object对象在 jvm中?

是也不是

假如你有以下类,请分别创建 Base.java 和 Sub.java

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
public class Base{
int i;
}

// Base对应字节码
// class version 49.0 (49)
// access flags 0x21
public class Base {

// compiled from: Base.java

// access flags 0x0
I i

// access flags 0x1
public <init>()V
L0
LINENUMBER 1 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this LBase; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
}

------------------分割线------------------------
public class Sub extends Base{
int j;
}

// Sub对应字节码
// class version 49.0 (49)
// access flags 0x21
public class Sub extends Base {

// compiled from: Sub.java

// access flags 0x0
I j

// access flags 0x1
public <init>()V
L0
LINENUMBER 1 L0
ALOAD 0
INVOKESPECIAL Base.<init> ()V
RETURN
L1
LOCALVARIABLE this LSub; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
}

每当你 new Sub()的时候实际上它会先偷偷帮你调用父类的构造器 也就是偷偷帮你调用new Base(),而你去看 Base 发现它会偷偷调用 new Object()

  • 这样对象就是一层一层的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    _____________________
    object| |
    ______| |
    |
    Base 的数据 i |
    --------------------|
    Sub 的数据 j |
    --------------------|

    同时也解释了 为什么面向对象里你总是可以把一个 子类转换成 父类

基本数据类型的值是放在堆常量池还是栈的常量池?

  • 基本数据类型不放常量池
  • 放在堆还是栈取决于你这个东西是什么东西

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Main {
    public static void main(String[] args) {
    int i = 0;
    // 这个 i 放在 main 的栈帧里

    Base base = new Base();
    // 这个 base会在 堆上开辟一块内存
    // base 里有个 i 初始化是 0
    // base 指向这块空间 base = 一个地址(指向堆上开辟的base对象)
    }
    static class Base{
    int i;
    }
    }
    • 如果是局部变量放在 栈里
    • 如果是一个类的成员它就放在堆里面

永久代(PermGen):Java7之前

  • OutOfMemory
    • 有的时候看到的是 heap space
    • 有的时候是 perm gen (java8之后把它移出来了,移到元空间里了)

元空间(Metaspace):Java8之后

  • 元空间和Native是共享内存

为什么 Java8之后把 perm gen 放在元空间里

  • 永久代在之前的过程中,是和堆放在一起的,会和堆抢内存。很容易碰到 OutOfMemory

Node-JS专精07手写深拷贝03

完整代码

src/idnex.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
let cache = [];
function deepClone(source){
if(source instanceof Object){
let cacheDist = findCache(source)
if(cacheDist){
// console.log("有缓存")
return cacheDist;
}else{
// console.log("没缓存")
let dist;
if(source instanceof Array){
dist = new Array();
} else if(source instanceof Function){
dist = function(){
return source.apply(this,arguments);
}

} else if(source instanceof RegExp){
dist = new RegExp(source.source,source.flags);
} else if(source instanceof Date){
dist = new Date(source);
} else {
dist = new Object();
}
// 数据源 和 副本 都存入缓存 ,注意一定要 在 dist创建成功之后就把它 存入,防止重复的生成
cache.push([source,dist])
for(let key in source){
if(source.hasOwnProperty(key)){
dist[key] = deepClone(source[key]);
}
}
return dist;
}
}
return source;
}

function findCache(source){
for(let i=0;i<cache.length;i++){
if(cache[i][0] === source){
return cache[i][1];
}
}
return undefined
}

module.exports = deepClone;

test/index.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
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
const chai = require("chai");
const sinon = require("sinon");
const sinonChai = require("sinon-chai");
chai.use(sinonChai);
const assert = chai.assert;
const deepClone = require("../src/index");

describe('deepClone',()=>{
it("是一个函数",()=>{
assert.isFunction(deepClone)
})

it("能够复制基本类型",()=>{
const n = 123;
const n2 = deepClone(n);
assert(n === n2);

const s = "123456";
const s2 = deepClone(s);
assert(s === s2);

const b = true;
const b2 = deepClone(b);
assert(b === b2);

const u = undefined;
const u2 = deepClone(u);
assert(u === u2);

const empty = null;
const empty2 = deepClone(empty);
assert(empty === empty2);

const sym = Symbol();
const sym2 = deepClone(sym);
assert(sym === sym2);

})

describe("对象",()=>{
it("能够复制普通对象",()=>{
const a = {name: "方方", chlid: {name: "小方方"}};
const a2 = deepClone(a);
assert(a !== a2);
assert(a.name === a2.name);
assert(a.chai !== a2.chlid);
assert(a.chlid.name === a2.chlid.name);
})

it("能够复制数组对象",()=>{
const a = [[11,12],[21,22],[31,32]]
const a2 = deepClone(a);
assert(a !== a2);
assert(a[0] !== a2[0]);
assert(a[1] !== a2[1]);
assert(a[2] !== a2[2]);
assert.deepEqual(a,a2);
})

it("能够复制函数",()=>{
const a = function(x,y){
return x + y;
}
a.xxx = { yyy: { zzz: 1 } }
const a2 = deepClone(a);
assert(a !== a2);
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz);
assert(a.xxx.yyy !== a2.xxx.yyy);
assert(a.xxx !== a2.xxx);
assert(a(1,2) === a2(1,2));

})

it("环也能复制",()=>{
const a = { name: "方方" }
a.self = a;
const a2 = deepClone(a);
assert(a !== a2);
assert(a.name === a2.name);
assert(a.self !== a2.self);

})

xit("不会爆栈",()=>{
const a = {child:null};
let b = a
for(let i=0;i<20000;i++){
b.child = {
child:null
}
b = b.child
}
const a2 = deepClone(a);
assert(a !== a2);
assert(a.child !== a2.child);
})

it("可以复制正则表达式",()=>{
const a = new RegExp("/hi\d+/","gi");
a.xxx = { yyy: { zzz: 1 } }
const a2 = deepClone(a);
assert(a.source === a2.source);
assert(a.flags === a2.flags);
assert(a !== a2);
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz);
assert(a.xxx.yyy !== a2.xxx.yyy);
assert(a.xxx !== a2.xxx);
})

it("可以复制日期",()=>{
const a = new Date();
a.xxx = { yyy: { zzz: 1 } }
const a2 = deepClone(a);
assert(a.source === a2.source);
assert(a !== a2);
assert(a.getTime() === a2.getTime());
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz);
assert(a.xxx.yyy !== a2.xxx.yyy);
assert(a.xxx !== a2.xxx);
})

it("自动跳过原型属性",()=>{
const a = Object.create({name:"a"})
a.xxx = { yyy: { zzz: 1 } }
const a2 = deepClone(a);
assert(a !== a2);
assert.isFalse("name" in a2);
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz);
assert(a.xxx.yyy !== a2.xxx.yyy);
assert(a.xxx !== a2.xxx);
})

it("很复杂的对象",()=>{
const a = {
n: NaN,
n2:Infinity,
s: '',
bool: false,
null: null,
u: undefined,
sym: Symbol(),
o: {
n: NaN,
n2:Infinity,
s: '',
bool: false,
null: null,
u: undefined,
sym: Symbol()
},
array: [
{
n: NaN,
n2:Infinity,
s: '',
bool: false,
null: null,
u: undefined,
sym: Symbol()
}
],
fn: function(){return "fn"},
date: new Date(),
reg: /test/gi,
}

const a2 = deepClone(a);
assert(a !== a2);
assert.isNaN(a2.n);
assert(a.n2 === a2.n2);
assert(a.s === a2.s);
assert(a.bool === a2.bool);
assert(a.null === a2.null);
assert(a.u === a2.u);
assert(a.sym === a2.sym);
assert(a.o !== a2.o);
assert.isNaN(a2.o.n);
assert(a.o.n2 === a2.o.n2);
assert(a.o.s === a2.o.s);
assert(a.o.bool === a2.o.bool);
assert(a.o.null === a2.o.null);
assert(a.o.u === a2.o.u);
assert(a.o.sym === a2.o.sym);
assert(a.array !== a2.array);
assert(a.array[0] !== a2.array[0]);
assert.isNaN(a2.array[0].n);
assert(a.array[0].n2 === a2.array[0].n2);
assert(a.array[0].s === a2.array[0].s);
assert(a.array[0].bool === a2.array[0].bool);
assert(a.array[0].null === a2.array[0].null);
assert(a.array[0].u === a2.array[0].u);
assert(a.array[0].sym === a2.array[0].sym);
assert(a.fn !== a2.fn);
assert(a.fn() === a2.fn());
assert(a.date !== a2.date);
assert(a.date.getTime() === a2.date.getTime());
assert(a.reg !== a2.reg);
assert(a.reg.source === a2.reg.source);
assert(a.reg.flags === a2.reg.flags);
})

})
})

代码存在缺陷

  • cache它是一个全局对象,每次调用 deepClone 都会往里面 push东西
  • 所以你下次 deepClone的时候 就会相互影响

面向对象改造我们的代码

src/index_oop.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
class DeepCloner{
cache = [];
clone(source){
if(source instanceof Object){
let cacheDist = this.findCache(source)
if(cacheDist){
// console.log("有缓存")
return cacheDist;
}else{
// console.log("没缓存")
let dist;
if(source instanceof Array){
dist = new Array();
} else if(source instanceof Function){
dist = function(){
return source.apply(this,arguments);
}

} else if(source instanceof RegExp){
dist = new RegExp(source.source,source.flags);
} else if(source instanceof Date){
dist = new Date(source);
} else {
dist = new Object();
}
// 数据源 和 副本 都存入缓存 ,注意一定要 在 dist创建成功之后就把它 存入,防止重复的生成
this.cache.push([source,dist])
for(let key in source){
if(source.hasOwnProperty(key)){
dist[key] = this.clone(source[key]);
}
}
return dist;
}
}
return source;
}
findCache(source){
for(let i=0;i<this.cache.length;i++){
if(this.cache[i][0] === source){
return this.cache[i][1];
}
}
return undefined
}
}


module.exports = DeepCloner;

test/index_oop.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
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
const chai = require("chai");
const sinon = require("sinon");
const sinonChai = require("sinon-chai");
chai.use(sinonChai);
const assert = chai.assert;
const DeepCloner = require("../src/index_oop");

describe('DeepCloner',()=>{
it("是一个类",()=>{
assert.isFunction(DeepCloner)
})

it("能够复制基本类型",()=>{
const n = 123;
const n2 = new DeepCloner().clone(n);
assert(n === n2);

const s = "123456";
const s2 = new DeepCloner().clone(s);
assert(s === s2);

const b = true;
const b2 = new DeepCloner().clone(b);
assert(b === b2);

const u = undefined;
const u2 = new DeepCloner().clone(u);
assert(u === u2);

const empty = null;
const empty2 = new DeepCloner().clone(empty);
assert(empty === empty2);

const sym = Symbol();
const sym2 = new DeepCloner().clone(sym);
assert(sym === sym2);

})

describe("对象",()=>{
it("能够复制普通对象",()=>{
const a = {name: "方方", chlid: {name: "小方方"}};
const a2 = new DeepCloner().clone(a);
assert(a !== a2);
assert(a.name === a2.name);
assert(a.chai !== a2.chlid);
assert(a.chlid.name === a2.chlid.name);
})

it("能够复制数组对象",()=>{
const a = [[11,12],[21,22],[31,32]]
const a2 = new DeepCloner().clone(a);
assert(a !== a2);
assert(a[0] !== a2[0]);
assert(a[1] !== a2[1]);
assert(a[2] !== a2[2]);
assert.deepEqual(a,a2);
})

it("能够复制函数",()=>{
const a = function(x,y){
return x + y;
}
a.xxx = { yyy: { zzz: 1 } }
const a2 = new DeepCloner().clone(a);
assert(a !== a2);
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz);
assert(a.xxx.yyy !== a2.xxx.yyy);
assert(a.xxx !== a2.xxx);
assert(a(1,2) === a2(1,2));

})

it("环也能复制",()=>{
const a = { name: "方方" }
a.self = a;
const a2 = new DeepCloner().clone(a);
assert(a !== a2);
assert(a.name === a2.name);
assert(a.self !== a2.self);

})

xit("不会爆栈",()=>{
const a = {child:null};
let b = a
for(let i=0;i<20000;i++){
b.child = {
child:null
}
b = b.child
}
const a2 = new DeepCloner().clone(a);
assert(a !== a2);
assert(a.child !== a2.child);
})

it("可以复制正则表达式",()=>{
const a = new RegExp("/hi\d+/","gi");
a.xxx = { yyy: { zzz: 1 } }
const a2 = new DeepCloner().clone(a);
assert(a.source === a2.source);
assert(a.flags === a2.flags);
assert(a !== a2);
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz);
assert(a.xxx.yyy !== a2.xxx.yyy);
assert(a.xxx !== a2.xxx);
})

it("可以复制日期",()=>{
const a = new Date();
a.xxx = { yyy: { zzz: 1 } }
const a2 = new DeepCloner().clone(a);
assert(a.source === a2.source);
assert(a !== a2);
assert(a.getTime() === a2.getTime());
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz);
assert(a.xxx.yyy !== a2.xxx.yyy);
assert(a.xxx !== a2.xxx);
})

it("自动跳过原型属性",()=>{
const a = Object.create({name:"a"})
a.xxx = { yyy: { zzz: 1 } }
const a2 = new DeepCloner().clone(a);
assert(a !== a2);
assert.isFalse("name" in a2);
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz);
assert(a.xxx.yyy !== a2.xxx.yyy);
assert(a.xxx !== a2.xxx);
})

it("很复杂的对象",()=>{
const a = {
n: NaN,
n2:Infinity,
s: '',
bool: false,
null: null,
u: undefined,
sym: Symbol(),
o: {
n: NaN,
n2:Infinity,
s: '',
bool: false,
null: null,
u: undefined,
sym: Symbol()
},
array: [
{
n: NaN,
n2:Infinity,
s: '',
bool: false,
null: null,
u: undefined,
sym: Symbol()
}
],
fn: function(){return "fn"},
date: new Date(),
reg: /test/gi,
}

const a2 = new DeepCloner().clone(a);
assert(a !== a2);
assert.isNaN(a2.n);
assert(a.n2 === a2.n2);
assert(a.s === a2.s);
assert(a.bool === a2.bool);
assert(a.null === a2.null);
assert(a.u === a2.u);
assert(a.sym === a2.sym);
assert(a.o !== a2.o);
assert.isNaN(a2.o.n);
assert(a.o.n2 === a2.o.n2);
assert(a.o.s === a2.o.s);
assert(a.o.bool === a2.o.bool);
assert(a.o.null === a2.o.null);
assert(a.o.u === a2.o.u);
assert(a.o.sym === a2.o.sym);
assert(a.array !== a2.array);
assert(a.array[0] !== a2.array[0]);
assert.isNaN(a2.array[0].n);
assert(a.array[0].n2 === a2.array[0].n2);
assert(a.array[0].s === a2.array[0].s);
assert(a.array[0].bool === a2.array[0].bool);
assert(a.array[0].null === a2.array[0].null);
assert(a.array[0].u === a2.array[0].u);
assert(a.array[0].sym === a2.array[0].sym);
assert(a.fn !== a2.fn);
assert(a.fn() === a2.fn());
assert(a.date !== a2.date);
assert(a.date.getTime() === a2.date.getTime());
assert(a.reg !== a2.reg);
assert(a.reg.source === a2.reg.source);
assert(a.reg.flags === a2.reg.flags);
})

})

})

这样一个我们就解决了 cache被全局共享造成影响的问题

面试小技巧

  • cache是我们预留的坑,你打王者肯定不会上来先放大招,那样打不死,肯定是小技能磨血,然后一套技能接大招带走
  • 这样面试官就会惊讶于怎么我一说你就能很快的作出反应。就很加分。

常见回答

  • Object.assign({},a) 这是一个浅拷贝
  • Lodash.cloneDeep
  • 扩展操作符{...a} 它是无法深度克隆的

回到答题思路

五步

  • why 要解决什么问题
  • how 如何解决的
  • pros 优点:只要是你自己实现的基本没什么优点,唯一能证明就是 js基础掌握的较好,因为一般都是用第三方的库,因为第三方的库测试比你完善
  • cons 缺点
    • 如果你的类型超过了我的 if else 我就搞不定了,比如 set / map
  • more 解决缺点的方案
    • 直接用第三方库 比如 lodash / immutable.js

面试亮点

  • 除了写代码,你可以在上面任何一点出彩

代码仓库

Node-JS专精07手写深拷贝02

思路

递归

  • 看节点的类型 7种(number bool string undefined null symbol 和 object)
    • array function regexp date 是 object的子类型
  • 如果是简单数据类型就直接拷贝
  • 如果是 object 就情况讨论

Object

  • 普通 object 的 for in ?
    • for in 有bug
  • 数组 array - Array初始化
    • 它里面可能还是对象
  • 函数 function - 怎么拷贝? 闭包?
  • 日期 Date - 怎么拷贝?

开始干

  • 创建目录

    1
    2
    3
    4
    5
    根目录
    deep-clone

    src/index.js
    test/index.js
  • 引入 chai 和 sinon

    • 参考代码引入库
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      npm init -y 

      加入如下内容
      "scripts": {
      "test": "mocha test/**/*.js"
      },
      "devDependencies": {
      "chai": "^4.2.0",
      "mocha": "^6.2.0",
      "sinon": "^7.4.1",
      "sinon-chai": "^3.3.0"
      }

      yarn install

      运行 yarn test
      如果显示 0 passing (1ms) 代表环境搭建成功
  • 开始驱动测试开发

    • src/index.js

      1
      2
      function deepClone(){}
      module.exports = deepClone;
    • 注意! 由于是js

      • 引入模块:js是不支持 import的 所以只能用 require
      • 导出模块:js是不支持 export default 所以只能用 module.exports = xxx
    • test/index.js
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      // 注意!!!
      // js是不支持 import的 所以只能用 require
      const chai = require("chai");
      const sinon = require("sinon");
      const sinonChai = require("sinon-chai");
      chai.use(sinonChai);
      const assert = chai.assert;
      const deepClone = require("../src/index");

      describe('deepClone',()=>{
      it("是一个函数",()=>{
      assert.isFunction(deepClone)
      })
      })
  • 测试失败 =》 改代码 =》 测试成功 =》 加测试 =》 测试失败

  • 这就是永动机

鸡贼的完成基本数据类型深拷贝

src/index.js

1
2
3
4
5
function deepClone(source){
return source;
}

module.exports = deepClone;

test/index.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
const chai = require("chai");
const sinon = require("sinon");
const sinonChai = require("sinon-chai");
chai.use(sinonChai);
const assert = chai.assert;
const deepClone = require("../src/index");

describe('deepClone',()=>{
it("是一个函数",()=>{
assert.isFunction(deepClone)
})

it("能够复制基本类型",()=>{
const n = 123;
const n2 = deepClone(n);
assert(n === n2);

const s = "123456";
const s2 = deepClone(s);
assert(s === s2);

const b = true;
const b2 = deepClone(b);
assert(b === b2);

const u = undefined;
const u2 = deepClone(u);
assert(u === u2);

const empty = null;
const empty2 = deepClone(empty);
assert(empty === empty2);

const sym = Symbol();
const sym2 = deepClone(sym);
assert(sym === sym2);

})
})

Symbol的疑问? 为什么会相等

  • 不是说不会有两个相同的 Symbol吗?它到底是引用呢?还是没有引用啊?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = Symbol()
var b = a;

// 此时 a 和 b 是同一个 Symbol吗? 是不是不确定啊

// 但是如果这样
var a2 = 1;
var b2 = a2;
// a2 和 b2 是同一个 1吗 在内存里? 你会回答 不是

// 同样的问题回到 Symbol

// a === b 吗? 答案是 true
// 所以 symbol 可能是个 不可变的引用类型。 此时你已经陷入知识盲区了,就不要在深入了

复制简单对象

test/index.js

1
2
3
4
5
6
7
8
9
10
describe("对象",()=>{
it("能够复制普通对象",()=>{
const a = {name: "方方", chlid: {name: "小方方"}};
const a2 = deepClone(a);
assert(a !== a2);
assert(a.name === a2.name);
assert(a.chai !== a2.chlid);
assert(a.chlid.name === a2.chlid.name);
})
})

通常你会这样想

  • for in的坑 你怎么知道 key是 source的属性 还是它原型上的属性
1
2
3
4
5
6
7
8
9
10
11
12
function deepClone(source){
if(source instanceof Object){
const dist = new Object();
for(let key in source){
dist[key] = deepClone(source[key]);
}
return dist;
}
return source;
}

module.exports = deepClone;

试试数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
it("能够复制数组对象",()=>{
const a = [[11,12],[21,22],[31,32]]
const a2 = deepClone(a);
assert(a !== a2);
assert(a[0] !== a2[0]);
assert(a[1] !== a2[1]);
assert(a[2] !== a2[2]);
assert.deepEqual(a,a2);
})

// yarn test报错
AssertionError: expected [ [ 11, 12 ], [ 21, 22 ], [ 31, 32 ] ] to deeply equal { Object (0, 1, ...) }
// 你可以把代码拷到浏览器试一下, a2 实际是一个 包含 0,1,2 key的伪数组

// 要分支处理判断 Object的子类型

修改src/index.js

  • 注意此时不要优化代码
  • 注意此时不要优化代码
  • 注意此时不要优化代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function deepClone(source){
if(source instanceof Object){
if(source instanceof Array){
const dist = new Array();
for(let key in source){
dist[key] = deepClone(source[key]);
}
return dist;
}else{
const dist = new Object();
for(let key in source){
dist[key] = deepClone(source[key]);
}
return dist;
}
}
return source;
}

module.exports = deepClone;

复制函数

test/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
it("能够复制函数",()=>{
const a = function(x,y){
return x + y;
}
a.xxx = { yyy: { zzz: 1 } }
const a2 = deepClone(a);
assert(a !== a2);
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz);
assert(a.xxx.yyy !== a2.xxx.yyy);
assert(a.xxx !== a2.xxx);
assert(a(1,2) === a2(1,2));

})

网上的一个思路 eval

1
2
3
4
5
6
7
8
9
10
11
12
// 浏览器控制台里
function x(a,b){return a+b;}
// 直接f 显示是个函数
// 你要这样
console.dir(x);
// 你会发现他的 __proto__ 里有个 toString
x.toString() // "function x(a,b){return a+b;}"


// 假设 source是一个函数 而且是具名函数
eval(source.toString())
// 但是 eval 是一个很难控制的东西

我们的思路是 call / apply

  • 唯一的缺陷就是比原来的函数多嵌套了一层
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
function deepClone(source){
if(source instanceof Object){
if(source instanceof Array){
const dist = new Array();
for(let key in source){
dist[key] = deepClone(source[key]);
}
return dist;
} else if(source instanceof Function){
const dist = function(){
return source.apply(this,arguments);
}
for(let key in source){
dist[key] = deepClone(source[key]);
}
return dist;
}else{
const dist = new Object();
for(let key in source){
dist[key] = deepClone(source[key]);
}
return dist;
}
}
return source;
}

module.exports = deepClone;

环检测

  • 我们完成的递归都是对象有结尾的,而如果对象是个环状的就有问题了
1
2
3
4
5
var a = {name:"方方"}
a.self = a

// 此时 a 就是一个环状结构
a.self.self.self.self ..... .self = a // true

思路

  • 优化代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function deepClone(source){
    if(source instanceof Object){
    let dist;
    if(source instanceof Array){
    dist = new Array();
    } else if(source instanceof Function){
    dist = function(){
    return source.apply(this,arguments);
    }
    }else{
    dist = new Object();
    }
    for(let key in source){
    dist[key] = deepClone(source[key]);
    }
    return dist;
    }
    return source;
    }

    module.exports = deepClone;
  • 你要把出现过的 节点 和 克隆后的节点 都保存起来

src/index.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
let cache = [];
function deepClone(source){
if(source instanceof Object){
let cacheDist = findCache(source)
if(cacheDist){
console.log("有缓存")
return cacheDist;
}else{
console.log("没缓存")
let dist;
if(source instanceof Array){
dist = new Array();
} else if(source instanceof Function){
dist = function(){
return source.apply(this,arguments);
}
}else{
dist = new Object();
}
// 数据源 和 副本 都存入缓存 ,注意一定要 在 dist创建成功之后就把它 存入,防止重复的生成
cache.push([source,dist])
for(let key in source){
dist[key] = deepClone(source[key]);
}
return dist;
}
}
return source;
}

function findCache(source){
for(let i=0;i<cache.length;i++){
if(cache[i][0] === source){
return cache[i][1];
}
}
return undefined
}

module.exports = deepClone;

如果这个对象不是环,就是特别深呢?

  • 由于你用的是递归它会使用调用栈,所以在一定层数后它会爆栈
1
a.x.x.x.x.x.x.x.x.x.x  大概2w层呢?

test/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xit("不会爆栈",()=>{
const a = {child:null};
let b = a
for(let i=0;i<20000;i++){
b.child = {
child:null
}
b = b.child
}
const a2 = deepClone(a);
assert(a !== a2);
assert(a.child !== a2.child);

})
// 测试通不过
// 而层数改成 5000 又没问题了

如何解决递归爆栈问题?

  • 就是把递归变成循环
    • 每进入一个对象,不要去复制他,先把它放到数组里,然后把它的子元素放在数组后面
    • 就是把竖着的拍平
  • 本次不实现这个功能!!!
    • 面试一般不会考到这一步,因为要把上面所有的思路改掉

如果面试问到

  • 可能会遇到爆栈
  • 解决办法是对它的结构进行一个改造,用循环的方式把它放在数组里

复制正则表达式和Date

  • 首先要去看文档,起码要知道 正则有那些属性

测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 it("可以复制正则表达式",()=>{
const a = new RegExp("/hi\d+/","gi");
a.xxx = { yyy: { zzz: 1 } }
const a2 = deepClone(a);
assert(a.source === a2.source);
assert(a.flags === a2.flags);
assert(a !== a2);
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz);
assert(a.xxx.yyy !== a2.xxx.yyy);
assert(a.xxx !== a2.xxx);
})

it("可以复制日期",()=>{
const a = new Date();
a.xxx = { yyy: { zzz: 1 } }
const a2 = deepClone(a);
assert(a.source === a2.source);
assert(a !== a2);
assert(a.getTime() === a2.getTime());
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz);
assert(a.xxx.yyy !== a2.xxx.yyy);
assert(a.xxx !== a2.xxx);
})

src/index.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
let cache = [];
function deepClone(source){
if(source instanceof Object){
let cacheDist = findCache(source)
if(cacheDist){
// console.log("有缓存")
return cacheDist;
}else{
// console.log("没缓存")
let dist;
if(source instanceof Array){
dist = new Array();
} else if(source instanceof Function){
dist = function(){
return source.apply(this,arguments);
}

} else if(source instanceof RegExp){
dist = new RegExp(source.source,source.flags);
} else if(source instanceof Date){
dist = new Date(source);
} else {
dist = new Object();
}
// 数据源 和 副本 都存入缓存 ,注意一定要 在 dist创建成功之后就把它 存入,防止重复的生成
cache.push([source,dist])
for(let key in source){
dist[key] = deepClone(source[key]);
}
return dist;
}
}
return source;
}

function findCache(source){
for(let i=0;i<cache.length;i++){
if(cache[i][0] === source){
return cache[i][1];
}
}
return undefined
}

module.exports = deepClone;

是否拷贝原型属性?

1
2
3
4
var a = {name:'hi'}

var a2 = Object.create({say:function(){}})
// a2的原型上有个 say函数,如果深拷贝需要复制这个 say吗?
  • 一般来说不拷贝,因为如果拷贝这个对象内存占用就太大了
    • 因为一旦你拷贝这一层,那它的原型链的所有原型都要拷贝,这个原型上的每个函数也有原型那岂不是都要拷贝
    • 所以你开了一个头,那整个 JS对象身上的东西都要拷贝,这样占用的内存就太大了

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
it("自动跳过原型属性",()=>{
const a = Object.create({name:"a"})
a.xxx = { yyy: { zzz: 1 } }
const a2 = deepClone(a);
assert(a !== a2);
assert.isFalse("name" in a2);
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz);
assert(a.xxx.yyy !== a2.xxx.yyy);
assert(a.xxx !== a2.xxx);
})

// 测试没有通过,因为你用的是 for in
// 需要加一个判断
// if(source.hasOwnProperty(key)){ ... }

尽量少用for in 复制属性

代码仓库

Node-JS专精08手写bind02

手写bind02

上次实现的代码

但它有个问题

  • 不支持 new

自带的bind 演示

  • 理解 new fn() 后的四个关键步骤
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 fn = function(a){
this.a = a;
}


new fn('x'); // 返回一个对象
// 当你这样之后
// 会有四个步骤

// step01 产生临时对象 temp
var temp = {};

// step02 挂载 fn.prototype 到 temp对象的 prototype上
temp.__proto__ = fn.prototype

// step03 调用 call 绑定this 把生成的临时对象 temp作为 this
fn.call(temp,'x')

// step04 new fn('a')后的返回值 this 返回 this 就是 temp
return this;


var fn2 = fn.bind(undefined,'y')
new fn2(); // 此时bind通过参数帮我们 绑定身上的 a = 'y'

这就是原版bind支持 new操作的

我们先测试原版bind

  • 测试后可以通过
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function test5(message){
console.log(message)
Function.prototype.bind2 = bind;
const fn = function(p1, p2){
this.p1 = p1;
this.p2 = p2;
}
const fn2 = fn.bind(undefined,'x','y');
const object = new fn2();
console.log(object) // 此时 object = {p1:'x',p2:'y'}
console.assert(object.p1 === 'x','p1');
console.assert(object.p2 === 'y','p2');
}

// 这是可以通过的
  • 测试我们的bind
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 slice = Array.prototype.slice;
function bind(asThis){
var args = slice.call(arguments,1);
const fn = this
if(typeof fn !== 'function'){
throw new Error("bind 必须调用在函数身上");
}
return function(){
var args2 = slice.call(arguments,0);
return fn.apply(asThis, args.concat(args2))
}
}

// 测试不通过
Function.prototype.bind2 = bind;
const fn = function(p1, p2){
this.p1 = p1;
this.p2 = p2;
}
const fn2 = fn.bind2(undefined,'x','y');
const object = new fn2();
console.log(object) // 此时 object = {}
console.assert(object.p1 === 'x','p1');
console.assert(object.p2 === 'y','p2');

分析原因

  • new fn2()的时候
    • 产生临时对象 temp
      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
      function bind(asThis){
      var args = slice.call(arguments,1);
      const fn = this
      return function(){
      /*
      // step01
      产生临时对象 temp
      // step02
      temp.p1 = 'x';
      temp.p2 = 'y'
      // step03
      return this
      // 问题来了 ? 此时你主动写了 return 就不会把 temp正常返回
      // 返回的是什么? 就是 global 对象
      // 实际效果是 global.p1 = 'x' ; global.p2= 'y';
      */

      var args2 = slice.call(arguments,0);
      return fn.apply(asThis, args.concat(args2))
      }
      }

      // 所以我们最后不能使用 return fn.apply(asThis, args.concat(args2) 里的 asThis
      而是使用 new 产生的临时对象 this

      // 意思就是 可能是 asThis 可能是 temp临时对象 (this)
      // 那什么时候用这个 this呢?

如何知道一个函数调用的时候用没用 new

1
2
3
4
5
6
7
8
9
10
11
12
13
var fn = function(){console.log(this); console.log(this.__proto__ === fn.prototype);}

fn()
// window
// false

fn.call({a:11})
// {a:11}
// false

new fn();
// fn{}
// true

所以答案非常明显我们只要这样判断 this就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var slice = Array.prototype.slice;
function bind(asThis){
var args = slice.call(arguments,1);
const fn = this
if(typeof fn !== 'function'){
throw new Error("bind 必须调用在函数身上");
}
return function resultFn(){
var args2 = slice.call(arguments,0);
return fn.apply(
this.__proto__ === resultFn.prototype ? this : asThis,
args.concat(args2)
)
}
}

// 而且测试也能通过

其实此时代码是有bug的

  • 继续添加测试用例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function test6(message){
console.log(message)
Function.prototype.bind2 = bind;
const fn = function(p1, p2){
this.p1 = p1;
this.p2 = p2;
}
fn.prototype.sayHi = function(){};
const fn2 = fn.bind2(undefined,'x','y');
const object = new fn2();
console.assert(object.p1 === 'x','p1');
console.assert(object.p2 === 'y','p2');
console.assert(typeof object.sayHi === 'function');
}

// 测试没有通过
// 而用原版 bind 是可以通过的
// 证明我们代码有问题
  • 重新指向它的 prototype
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var slice = Array.prototype.slice;
// ES5版
function bind(asThis){
var args = slice.call(arguments,1);
const fn = this
if(typeof fn !== 'function'){
throw new Error("bind 必须调用在函数身上");
}
function resultFn(){
var args2 = slice.call(arguments,0);
return fn.apply(
this.__proto__ === resultFn.prototype ? this : asThis,
args.concat(args2)
)
}
resultFn.prototype = fn.prototype;
return resultFn;
}

// 此时测试可以通过

// 但是代码还是有问题

解决代码的问题

  • bind传递的this 是 fn是 new fn() 就会误判断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 虽然没造成影响,但是有 误判的情况
    function test7(message){
    console.log(message)
    Function.prototype.bind2 = bind;
    const fn = function(p1, p2){
    this.p1 = p1;
    this.p2 = p2;
    }
    fn.prototype.sayHi = function(){};
    const object1 = new fn();
    const fn2 = fn.bind2(object1,'x','y');
    const object = fn2();
    console.assert(object === undefined, 'object为空');
    console.assert(object1.p1 === 'x','p1');
    console.assert(object1.p2 === 'y','p2');
    }
  • this.__proto__ === resultFn.prototype 中的 this.__proto__ 是不能在生产代码里用的

    • this instanceof resultFn
    • resultFn.prototype.isPrototypeOf(this)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      var slice = Array.prototype.slice;
      // ES5版
      function bind(asThis){
      var args = slice.call(arguments,1);
      const fn = this
      if(typeof fn !== 'function'){
      throw new Error("bind 必须调用在函数身上");
      }
      function resultFn(){
      var args2 = slice.call(arguments,0);
      return fn.apply(
      resultFn.prototype.isPrototypeOf(this) ? this : asThis,
      args.concat(args2)
      )
      }
      resultFn.prototype = fn.prototype;
      return resultFn;
      }

手写bind完整版

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
var slice = Array.prototype.slice;
// ES5版
function bind(asThis){
var args = slice.call(arguments,1);
const fn = this
if(typeof fn !== 'function'){
throw new Error("bind 必须调用在函数身上");
}
function resultFn(){
var args2 = slice.call(arguments,0);
return fn.apply(
resultFn.prototype.isPrototypeOf(this) ? this : asThis,
args.concat(args2)
)
}
resultFn.prototype = fn.prototype;
return resultFn;
}
module.exports = bind;

// ES6版
function _bind(asThis, ...args){
const fn = this
function resultFn(...args2){
return fn.call(
resultFn.prototype.isPrototypeOf(this) ? this : asThis,
...args,
...args2
);
}
resultFn.prototype = fn.prototype;
return resultFn;
}

if(!Function.prototype.bind){
Function.prototype.bind = bind;
}

MDN的bind实现是有缺陷的

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
// Yes, it does work with `new funcA.bind(thisArg, args)`
if (!Function.prototype.bind) (function(){
var ArrayPrototypeSlice = Array.prototype.slice;
Function.prototype.bind = function(otherThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}

var baseArgs= ArrayPrototypeSlice .call(arguments, 1),
baseArgsLength = baseArgs.length,
fToBind = this,
fNOP = function() {},
fBound = function() {
baseArgs.length = baseArgsLength; // reset to default base arguments
baseArgs.push.apply(baseArgs, arguments);
return fToBind.apply(
fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
);
};

if (this.prototype) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();

return fBound;
};
})();

它的代码和我们不同的地方在这

1
2
3
4
if (this.prototype) {
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();

先不管它代码什么意思 我们先测试一下

  • step01拷贝到浏览器控制台
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

var ArrayPrototypeSlice = Array.prototype.slice;
// 注意不能直接 Function.prototype.bind = xxx
// 因为不能改写不了 要 Function.prototype.bind2 = xxx
Function.prototype.bind2 = function(otherThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}

var baseArgs= ArrayPrototypeSlice .call(arguments, 1),
baseArgsLength = baseArgs.length,
fToBind = this,
fNOP = function() {},
fBound = function() {
baseArgs.length = baseArgsLength; // reset to default base arguments
baseArgs.push.apply(baseArgs, arguments);
return fToBind.apply(
fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
);
};

if (this.prototype) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();

return fBound;
};
  • step02继续拷贝代码
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
var fn = function(a){this.a = a;}
fn.prototype.sayHi = function(){};

// 此时来测试
new fn('x')
// 打印 fn{a:'x'}
// 点开它只有一层 fn.__proto__

// 继续测试bind
fn2 = fn.bind(undefined,'y');
new fn2();
// 打印 fn{a:'y'}
// 点开它也只有一层 fn.__proto__

// 测试 mdn 的 bind
fn3 = fn.bind2(undefined,'y')
new fn3();
// 打印 fn{a:'y'}
// 点开它会多了一层 fn.__proto__.__proto__
/*
fn
-__proto__
-__proto__
-sayHi
*/


// MDN的代码问题 它的 prototype和原始的是不一样的
const o1 = new fn('x')
o1.__proto__ === fn.prototype // true

const o2 = new fn2('y')
o2.__proto__ === fn.prototype // true

const o3 = new fn3('x')
o3.__proto__ === fn.prototype // false
o3.__proto__.__proto__ === fn.prototype // true
  • MDN bind支持 new操作的实现的__proto__会多了一层
  • 而且它的prorotype不是跟原始 bind的不一样

代码链接

  • https://github.com/slTrust/source-bind
  • 启发
  • 先实现一个最容易的
  • 找到自己的问题(需要经验)
  • 不停迭代
  • 直到找不到问题(需要经验)
  • 留一个问题给面试官
    • 先甩出简陋版 ES6版
    • TA说给我改成ES5的/支持分阶段绑定参数/支持new的
    • 不要一开始就放大 不然一下提出一个高出你最终版的你没碰到的问题你就惨了

Node-JS专精08手写bind01

手写bind

确定API

  • bind 位于 Function.prototype 上
  • 需要做 polyfill

什么是polyfill

  • polyfill为何物
  • 聚填充(生活中)
    • 抹平
    • 补全
    • 你的桌子一个腿短,于是底下垫了一个纸片
    • 你的墙面不够平整,有坑,于是用白浆填平它
  • 对于前端
    • 你的JS代码运行在坑人的IE6下
    • 我们知道方法一般挂在Function.prototype上 也就是Function.prototype.bind而在 IE6没有,此时你就需要 polyfill

API

  • fn.bind(asThis)
  • fn.bind(asThis, param1, param2)
  • fn.bind(asThis)()
  • fn.bind(asThis, param1, param2)()
  • fn.bind(asThis)(param1)
  • fn.bind(asThis, param1, param2)(p3, p4)

bind API例子

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
function aa(){
console.log(this);
console.log(arguments)
}

// fn.bind(asThis)
// fn.bind(asThis)()
var newFn = aa.bind({a:1})
newFn()
/*
{a:1}
arguments[]
*/

// fn.bind(asThis, param1, param2)
// fn.bind(asThis, param1, param2)()
var newFn2 = aa.bind({a:1},1,2)
newFn2()
/*
{a:1}
arguments[1,2]
*/


// fn.bind(asThis)(param1)
var newFn3 = aa.bind({a:1})
newFn3(1)
/*
{a:1}
arguments[1]
*/


// fn.bind(asThis,p1,p2)(p3,p4)
var newFn4 = aa.bind({a:1},1,2)
newFn4(3,4)
/*
{a:1}
arguments[1,2,3,4]
*/

001bind(asThis) 实现

  • 新建项目目录 bind_demo
  • 新建 bind_demo/src/index.js
1
2
3
4
5
6
7
8
9
10
11
function bind(asThis){
const fn = this
return function(){
return fn.call(asThis)
}
}
module.exports = bind;

if(!Function.prototype.bind){
Function.prototype.bind = bind;
}
  • 新建 bind_demo/test/index.js
1
2
3
4
5
6
7
8
9
10
const bind = require('../src/index');
Function.prototype.bind2 = bind;

console.assert(Function.prototype.bind2 !== undefined);

const fn1 = function(){
return this;
}
const newFn = fn1.bind2({name:'hjx'});
console.assert(newFn().name === 'hjx');

002bind(asThis,p1,p2) 实现

  • 通过扩展运算符

src/index.js

1
2
3
4
5
6
7
8
9
10
11
function bind(asThis, ...args){
const fn = this
return function(){
return fn.call(asThis, ...args)
}
}
module.exports = bind;

if(!Function.prototype.bind){
Function.prototype.bind = bind;
}

test/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const bind = require('../src/index');
Function.prototype.bind2 = bind;

console.assert(Function.prototype.bind2 !== undefined);

const fn1 = function(){
return this;
}
const newFn = fn1.bind2({name:'hjx'});
console.assert(newFn().name === 'hjx');

const fn2 = function(p1,p2){
return [this,p1,p2]
}

const newFn2 = fn2.bind2({name:'hjx'}, 123, 456);
console.assert(newFn2()[0].name === 'hjx');
console.assert(newFn2()[1] === 123 , "p1");
console.assert(newFn2()[2] === 456 , "p2");

003bind(asThis,p1)(p2) 实现

  • step1通过newFn = fn.bind({name:'hjx'},1) 绑定this 传递参数
  • step2newFn(2) 在此传递参数调用
1
2
3
4
5
6
7
8
9
10
11
function bind(asThis, ...args){
const fn = this
return function(...args2){
return fn.call(asThis, ...args, ...args2)
}
}
module.exports = bind;

if(!Function.prototype.bind){
Function.prototype.bind = bind;
}

test/index.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
const bind = require('../src/index');
Function.prototype.bind2 = bind;

console.assert(Function.prototype.bind2 !== undefined);

const fn1 = function(){
return this;
}
const newFn = fn1.bind2({name:'hjx'});
console.assert(newFn().name === 'hjx');

const fn2 = function(p1,p2){
return [this,p1,p2]
}

const newFn2 = fn2.bind2({name:'hjx'},123,456);
console.assert(newFn2()[0].name === 'hjx');
console.assert(newFn2()[1] === 123 , "p1");
console.assert(newFn2()[2] === 456 , "p2");


const anotherFn2 = fn2.bind2({name:'hjx'},123);
console.assert(anotherFn2(456)[0].name === 'hjx');
console.assert(anotherFn2(456)[1] === 123);
console.assert(anotherFn2(456)[2] === 456);

MDN bind的Polyfill实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (!Function.prototype.bind) (function(){
var slice = Array.prototype.slice;
Function.prototype.bind = function() {
var thatFunc = this, thatArg = arguments[0];
var args = slice.call(arguments, 1);
if (typeof thatFunc !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - ' +
'what is trying to be bound is not callable');
}
return function(){
var funcArgs = args.concat(slice.call(arguments))
return thatFunc.apply(thatArg, funcArgs);
};
};
})();

改写我们的 bind

  • 为什么MDN 不用扩展运算符,因为它不能用和 bind 同时期的API

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
var slice = Array.prototype.slice;
function bind(asThis){
var args = slice.call(arguments,1);
const fn = this
if(typeof fn !== 'function'){
throw new Error("bind 必须调用在函数身上");
}
return function(){
var args2 = slice.call(arguments,0);
return fn.apply(asThis, args.concat(args2))
}
}

ES5和ES6版 bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var slice = Array.prototype.slice;
// ES5版
function bind(asThis){
var args = slice.call(arguments,1);
const fn = this
if(typeof fn !== 'function'){
throw new Error("bind 必须调用在函数身上");
}
return function(){
var args2 = slice.call(arguments,0);
return fn.apply(asThis, args.concat(args2))
}
}

// ES6版
function _bind(asThis, ...args){
const fn = this
return function(...args2){
return fn.call(asThis, ...args, ...args2)
}
}

Node-JS专精07手写深拷贝

手写深拷贝

如何答题?

步骤

  • 询问数据类型
  • 询问数据规模
  • 询问性能要求
  • 询问运行环境
  • 询问其他要求
  • 开始写

JSON数据格式有哪些

JSON序列化和反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var obj = {
name:'abc',
age:18,
birthDate:new Date(2018,1,1), // 会变成日期字符串
exp:/hi/ , // 会被忽略
body:{
leg:4,
head:'one',
eye:['left','right'],
color:'red'
},
s:Symbol("a"), // 会被忽略
gender:undefined, // 会被忽略
sayHi:function(){console.log('hello')} // 会被忽略
}

// 序列化
var obj_str = JSON.stringify(obj)

// 反序列化
var obj2 = JSON.parse(obj_str)
console.log(obj2)

JSON序列化反序列化的缺点是什么

  • 不支持函数(会被忽略)
  • 不支持 undefined 只支持null
  • 不支持引用(循环引用)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var a = {name:'abc',age:18}
    a.self = a;
    JSON.stringify(a)

    报错
    Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    --- property 'self' closes the circle
    at JSON.stringify (<anonymous>)
    at <anonymous>:3:6
  • 不支持 Date ,JSON.stringify 之后会是 Date的字符串形式

  • 不支持正则表达式
  • 不支持 Symbol

递归深拷贝

Node-JS专精05高阶函数

高阶函数

把函数作为参数或者返回值的函数

JS内置高阶函数

  • Function.prototype.bind
  • Function.prototype.apply
  • Function.prototype.call
  • Array.prototype.sort
  • Array.prototype.map
  • Array.prototype.filter
  • Array.prototype.reduce

bind

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
var bind = Function.prototype.bind

const fn = function(){
console.log(this)
console.log(arguments)
}

var f1 = fn.bind({name:'hjx'},1,2,3)

fn()
// window
// Arguments[]

f1()
// {name:'hjx'}
// Arguments伪数组[1,2,3]


// 首先我们认同这句话
// obj.method(a,b,c,d)
// 等价于
// obj.method.call(obj,a,b,c,d)


// 代入值
// 设 obj = f1
// 设 method = bind

// 得到
// f1.bind(a,b,c,d)
// 等价于
// f1.bind.call(f1,a,b,c,d)

// 继续代入
// 设 a = {name:'hjx'}
// 设 b,c,d = 1,2,3
// f1.bind({name:'hjx'},1,2,3)
// 等价于
// f1.bind.call(f1,{name:'hjx'},1,2,3)

// f1.bind = Function.prototype.bind
// var bind = Function.prototype.bind
// 所以 f1.bind 就是 bind

// 继续代入 f1.bind.call(f1,{name:'hjx'},1,2,3)
bind.call(f1,{name:'hjx'},1,2,3) 这个式子就是结论


// 得到结论
bind.call 接受 一个函数fn,this,其他参数
返回一个新的函数,会调用 fn,并传入 this,和其他参数

apply

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
var apply = Function.prototype.apply

const f1 = function(){
console.log(this)
console.log(arguments)
}

f1.apply({name:'hjx'},[1,2,3]) // 直接调用
// {name:'hjx'}
// Arguments伪数组[1,2,3]


// 首先我们认同这句话
// obj.method(a,b,c,d)
// 等价于
// obj.method.call(obj,a,b,c,d)

// 设 obj = f1
// 设 method = apply

// 代入
// f1.apply({name:'hjx'},[1,2,3])
// 等价于
// f1.apply.call(f1,{name:'hjx'},[1,2,3])

// 继续代入
// 设 a = {name:'hjx'}
// 设 b,c,d = 1,2,3
// f1.apply({name:'hjx'},[1,2,3])
// 等价于
// f1.apply.call(f1,{name:'hjx'},[1,2,3])

// f1.apply = Function.prototype.apply
// f1.apply = apply

// 注意这里是直接调用,不会像 bind一样重新返回函数
apply.call(f1,{name:'hjx'},[1,2,3])

call

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
var call = Function.prototype.call

const f1 = function(){
console.log(this)
console.log(arguments)
}

f1.call({name:'hjx'},1,2,3) // 直接调用
// {name:'hjx'}
// Arguments伪数组[1,2,3]

// 设 obj = f1
// 设 method = call

// 代入
// f1.call({name:'hjx'},1,2,3)
// 等价于
// f1.call.call(f1,{name:'hjx'},1,2,3)

// 继续代入
// 设 a = {name:'hjx'}
// 设 b,c,d = 1,2,3
// f1.call({name:'hjx'},[1,2,3])
// 等价于
// f1.call.call(f1,{name:'hjx'},[1,2,3])

// f1.call = Function.prototype.call
// f1.call = call


// call的含义 也是直接调用 f1
call.call(f1,{name:'hjx'},1,2,3)

sort

map ,filter , reduce同理

1
2
3
4
5
var array = [1,5,2,3,4]
var sort = Array.property.sort

array.sort((a,b)=> b - a)
array.sort.call(array,(a,b=> b - a))

注意

理解JS的 call 用法才能真正掌握JS

adrian Mejia twitter

1
2
3
4
5
6
7
8
9
10
Reduce illustrated with Emojis:

map([🌽, 🐮, 🥚,🐓, 🥔], cook)
//=> [🍿, 🍔, 🍳,🍗, 🍟]

filter([🍿, 🍔, 🍳,🍗, 🍟], isVegetarian)
//=> [🍿, 🍳, 🍟]

reduce([🍿, 🍔, 🍳,🍗, 🍟], digest)
//=> 💩

函数的组合(用的比较少的)

1
2
3
4
5
6
7
8
9
10
11
12
function doubleSay (str) {
return str + ", " + str;
}
function capitalize (str) {
return str[0].toUpperCase() + str.substring(1);
}
function exclaim (str) {
return str + '!';
}

let result = exclaim(capitalize(doubleSay("hello")));
result //=> "Hello, hello!"

使用Pipe操作

1
2
3
4
5
6
let result = "hello"
|> doubleSay
|> capitalize
|> exclaim;

result //=> "Hello, hello!"

在不久的将来,这个语法就能用了。
如果你现在就想用,可以使用Ramda.js

1
2
3
// 组合方法,除了第一个函数,后面的函数必须接受一个参数
const say = R.compose(doubleSay, capitalize, exclaim);
let result = say("hello");

React高阶组件

特点

  • children是个函数
  • children返回React个数
  • 把需要暴露的API传给 children
  • 其实就是个回调

总结

  • 搞清函数所有特点
  • 理解递归
  • 理解迭代
  • 理解记忆化 memorize
  • 理解柯里化 curry
  • 组合函数

Node-JS专精06手写EventHub

EventHub

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
//订阅的事件
var eventMap = {
// 蒙牛:['张三','李四','王五'],
// 光明:['熊大','光头强']
};
var eventHub = {
trigger(eventName,data){
let fnList = eventMap[eventName];
if(!fnList) return
for(var i=0;i<fnList.length;i++){
fnList[i](data)
}
},
on(eventName,fn){
if(!eventMap[eventName]){
eventMap[eventName] = []
}
eventMap[eventName].push(fn)
}
}

// 订阅——事件
eventHub.on('我要用钱',function fn1(data){
console.log(data)
})

// 发布——事件
eventHub.trigger('我要用钱',100)

确定API

  • on
  • off
  • emit
  • 如果可能还有 once

环境配置先确保你有ts-node环境

1
2
3
yarn global add ts-node@8.3.0
ts-node -v
# 如果是 8.3.0 代表我们环境一致了

新建 index.ts

1
2
const a = 1;
console.log(a);

你就可以运行 ts代码了

1
ts-node index.ts

TS版 EventHub

001-JS版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class EventHub {
cache = {};
on(eventName,fn){
if(this.cache[eventName]=== undefined){
this.cache[eventName] = []
}
const array = this.cache[eventName];
array.push(fn)
}
emit(eventName){
let array = this.cache[eventName];
if(array === undefined){
array = [];
}
array.forEach(fn => fn());
}
}

002-JS优化版,支持传递data

1
2
3
4
5
6
7
8
9
10
class EventHub {
cache = {};
on(eventName,fn){
this.cache[eventName] = this.cache[eventName] || [];
this.cache[eventName].push(fn);
}
emit(eventName,data?){
(this.cache[eventName] || []).forEach(fn => fn(data));
}
}

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const eventHub  = new EventHub();
// eventHub 是个对象
console.assert(eventHub instanceof Object === true,"eventHub 是个对象");

// emit('xxx') 能被触发
let called = false;
eventHub.on('xxx',()=>{
called = true;
console.log("called:" + called);
})
eventHub.emit('xxx');


// 可以传递 abc
eventHub.on('xxx2',(y)=>{
console.assert(y === 'abc' === true,"传递emit('xxx2','abc')")
})
eventHub.emit('xxx2','abc');

003-JS-可以off

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
class EventHub {
private cache = {};
on(eventName, fn){
this.cache[eventName] = this.cache[eventName] || [];
this.cache[eventName].push(fn);
}
emit(eventName, data?){
(this.cache[eventName] || []).forEach(fn => fn(data));
}
off(eventName, fn) {
let index = indexOf(this.cache[eventName], fn);
if (index === -1) return;
this.cache[eventName].splice(index, 1);
}
}

export default EventHub;

/**
* 帮助函数 indexOf
* @param array
* @param item
*/
function indexOf(array, item) {
if (array === undefined) return -1;

let index = -1;
for (let i = 0; i < array.length; i++) {
if (array[i] === item) {
index = i;
break;
}
}
return index;
}

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
{
const eventHub = new EventHub();
let called = false;
const fn1 = (y)=>{
called = true;
};
eventHub.on('xxx',fn1)
eventHub.off('xxx',fn1);
eventHub.emit('xxx','aaa');
setTimeout(()=>{
console.log(called);
},1000)
}

004-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
import EventHub from "../src/index";

const test1 = message => {
const eventHub = new EventHub();
console.assert(eventHub instanceof Object === true,"eventHub 是个对象");
console.log(message);
}

const test2 = message => {
const eventHub = new EventHub();
let called = false;
eventHub.on('xxx',(y)=>{
called = true;

console.assert(y === 'abc' === true,"传递emit('xxx','abc')")
})
eventHub.emit('xxx','abc');
console.assert(called);
console.log(message);
}


const test3 = message => {
const eventHub = new EventHub();
let called = false;
const fn1 = (y)=>{
called = true;
};
eventHub.on('yyy',fn1)
eventHub.off('yyy',fn1);
eventHub.emit('yyy');
console.assert(called === false);
console.log(message);
}


test1("EventHub 可以创建对象");
test2(".on 了之后 .emit,会触发 .on 的函数");
test3(".off 有用");

一些疑问

  • emit('xxx',data) 能不能传递两个数据?
    • 面试容易问,你这个只能传一个参数,我想要两个怎么办?
    • 传一个数组不就好了
    • 这个没有规范的。面试技巧就是留一个坑给Ta踩
      1
      2
      3
      4
      5
      6
      7
      8
      9
      const eventHub = new EventHub();
      let called = false;
      eventHub.on("xxx", y => {
      called = true;
      console.assert(y[0] === "今天林志玲结婚了");
      console.assert(y[1] === "言承旭无话可说");
      });
      eventHub.emit("xxx", ["今天林志玲结婚了", "言承旭无话可说"]);
      console.assert(called);

代码仓库

Node-JS专精04柯里化

柯里化 Currying

让所有函数只接受一个参数

单一参数的意义?

基于单一参数,已经衍生出非常多的理论知识,如λ演算

那么如何支持两个参数?

用对象接受?

并不是,而是用闭包

还记得吗:对象是穷人的闭包

单参数函数接受两个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 这是用对象形式
const add = ({a,b})=> a + b
add({a:1,b:2})

// add接受一个数字,返回一个函数
const add = a => b => a + b
add(1)(2)
/*
// 等价形式
const add = function(a){
return function(b){
return a + b;
}
}
*/

感觉有点不方便是不是

没错,如果你没接受函数式的一整套理论,你就没必要使用柯里化。

柯里化一个函数

把多个参数函数,变成单参数函数

面试题

如何把三参数函数 add(1,2,3) 变成 curriedAdd(1)(2)(3)形式?

1
2
3
4
5
6
const curriedArr =
a =>
b =>
c =>
add(a,b,c)
// 完毕

面试题升级

1
2
3
4
5
6
7
8
9
假设
addTwo 接受2个参数
addThree 接受3个参数
addFore 接受4个参数
请写出一个 currify函数,使他分别接受 2 3 4 次参数,比如
currify(addTwo)(1)(2) // 3
currify(addThree)(1)(2)(3) // 6
currify(addFore)(1)(2)(3)(4) // 10
也就是说,currify 能将任意接受固定个参数的函数,变成单一参数的函数

答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
addTwo = (a,b)=>a+b
currify = (fn,params = [])=>{
return (arg)=>{
params.push(arg)
if(params.length === fn.length){
return fn(...params)
}else{
return currify(fn,params)
}
}
}
newAddTwo = currify(addTwo)
newAddTwo(1)(2) // 打印 3

代码是有问题的

1
2
3
4
5
// 如果这样就报错 
console.log(newAddTwo(1))
console.log(newAddTwo(1)(2))

原因是 params 是外面传递进来,会不会导致感染?

改进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
addTwo = (a,b)=>a+b
currify = (fn,params = [])=>{
return (arg)=>{
const newParams = params.concat(arg)
if(newParams.length === fn.length){
return fn(...newParams)
}else{
return currify(fn,newParams)
}
}
}
newAddTwo = currify(addTwo)
newAddTwo(1)(2) // 打印 3
console.log(newAddTwo(1)) // 打印是一个函数
console.log(newAddTwo(1)(2)) // 打印3

再次优化

  • 它支持一次绑定两个参数
    • newAddThree(1)(2)(3) 可以这样 newAddThree(1)(2,3)
1
2
3
4
5
6
7
8
9
10
11
12
13
currify = (fn, params = [])=>
(...args) =>
params.length + args.length === fn.length
? fn(...params, ...args)
: currify(fn, [...params, ...args])

addTwo = (a, b)=>a + b
addThree = (a, b, c)=>a + b + c
newAddTwo = currify(addTwo)
newAddThree = currify(addThree)
newAddTwo(1)(2)
newAddThree(1)(2)(3)
newAddThree(1)(2,3)