持续更新中!

浏览器的工作原理

首先 JavaScript 代码,在浏览器中是这样进行的:

  1. 从服务器中加载 index.html
  2. 解析 index.html 文件;
  3. 遇到 link 标签解析相应的 css 文件;
  4. 遇到 script 标签解析相应的 js 文件;

这个解析的过程是由 浏览器内核 来完成的。

浏览器内核

不同的浏览器是有不同的内核组成的,例如:

  • Gecko:早期被 Netscape 和 Mozilla Firefox 浏览器使用;
  • Trident:微软开发,被 IE4 ~ IE11 浏览器使用,但 Edge 浏览器已转向 Blink;
  • Webkit:苹果基于 KHTML 开发的,用于 Safari、Google Chrome 之前在使用;
  • Blink:是 Webkit 的一个分支,由 Google 开发,目前应用于 Google Chrome、Edge、Opera 等等;

上面说的浏览器内核指的是 浏览器的排版引擎(layout engine),也称为 浏览器引擎(browser engine)、页面渲染引擎(rendering engine)、样版引擎。


浏览器渲染过程

  1. 构建 DOM 树:渲染引擎解析 HTML 文档,首先将标签转换成 DOM 树中的 DOM node(包括JS生成的标签),构建 DOM 树;
  2. 构建 CSSOM 树:解析对应的 CSS 样式文件(包括 JS 生成的样式和外部 CSS 样式),构建 CSSOM 树;
  3. 构建渲染树(Render Tree):CSSOM 树构建结束后,和 DOM 树一起生成渲染树。渲染树中每个 node 都有自己的 style,而且渲染树不包含隐藏的节点(比如 display:none;),因为这些节点不会用于呈现;
  4. 布局渲染树(Layout/reflow):有了 Render Tree 后,从根节点递归调用,计算每一个元素的大小、位置等,给出每个节点所应该在屏幕上出现的精确坐标;
  5. 绘制渲染树(Painting/repaint):遍历渲染树,将各个节点绘制在屏幕上;

但在这个执行过程中,HTML 解析时遇到了 JavaScript 标签后会停止解析 HTML,而去加载和执行 JavaScript 代码。而这个 JavaScript 代码则是由 JavaScript 引擎来执行。


JS引擎

由于高级的编程语言都是需要转成最终的机器指令来执行的。而编写的 JavaScript 无论是交给浏览器还是 node 执行,最后都是需要被 CPU 执行的。所以我们需要 JavaScript 引擎帮助我们将 JavaScript 代码翻译成 CPU 指令来执行。

常见的 JavaScript 引擎如下:

  • SpiderMonkey:第一款 JS 引擎,由 Brendan Eick 开发(JavaScript作者);
  • Chakra:微软开发,用于 IE 浏览器;
  • JavaScriptCore:WebKit 中的 JavaScript 引擎,Apple 公司开发;
  • V8:由 Google 开发;

V8引擎

V8 是用 C++ 编写的 Google 开源高性能 JavaScript 和 WebAssembly 引擎,它用于 Chremo 和 Node.js 等。它实现 ECMAScript 和 WebAssembly,并在 window7 或更高版本,macOS10.12+ 和使用 x64,IA-32,ARM 或 MIPS 处理器的 Linux 系统上运行。V8 可以独立运行,也可以嵌入到任何 C++ 应用程序中;

架构:

  1. Parse 模块会将 JavaScript 代码转换成 AST(抽象语法树);
    • 如果函数没有被调用,是不会被转换成 AST 的;
    • V8 的 Parse 模块 官方文档
  2. Ignition 是一个解释器,会将 AST 转换成 ByteCode(字节码);
    • 同时会收集 TurboFan 优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
    • 如果函数只调用一次,Ignition 会执行解释执行 ByteCode;
    • V8 的 Ignition 官方文档
  3. TurboFan 是一个编译器,可以将字节码编译为 CPU 可以执行的机器码;
    • 如果一个函数被多次调用,那么就会标记为热点函数,就会经过 TurboFan 转换成优化的机器码,提高代码的执行性能;
    • 但是机器码实际上也会被还原为 ByteCode,这是因为如果后续执行函数过程中,类型发生了变化(比如函数原来执行的是 number 类型,后面执行变成了 string 类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换字节码;
    • V8 的 TurboFan 官方文档

那 JavaScript 源码是如何被解析的(Parse 过程):

  • Blink 将源码交给 V8 引擎,Stream 获取到源码并且进行编码转换;
  • Scanner 会进行词法分析,词法分析会将代码转换成 tokens;
  • tokens 会被转换成 AST 树,经过 Parser 和 PreParser:
    • Parser 就是直接将 tokens 转换成 AST 树结构;
    • PreParser 预解析:V8 引擎实现了 Lazy Parsing(延迟解析)方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行;因为并不是所有的 JavaScript 代码,在一开始时就会被执行,那么如果对所有的 JavaScript 代码进行解析,就会影响网页的运行效率;比如一个函数内部定义了另外一个函数,那么另外一个函数就会进行预解析;

JavaScript的执行过程

JS 引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object:

  • 该对象所有的作用域都可以访问;
  • 里面包含了 Date、Array、String、setTimeout等等;
  • 其中 window 属性指向自己;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var name = "lqzww";

console.log(result); // undefined

var num1 = 10;
var num2 = 20;
var result = num1 + num2;

console.log(result); // 30

function test() {
var name = "test";
console.log(name); // test
}

test();
1
2
3
4
5
6
7
8
9
10
11
12
var message = "hello message";

function foo() {
console.log(message); // hello message
}

function bar() {
var message = "hello bar";
foo();
}

bar();
1
2
3
4
5
6
7
var num = 100;
function foo() {
num = 200;
}
foo();

console.log(num); // 200
1
2
3
4
5
6
7
8
function foo() {
console.log(num); // undefined
var num = 200;
console.log(num); // 200
}

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

function foo1() {
console.log(num); // 100
}

function foo2() {
var num = 200;
console.log(num); // 200
foo1();
}

foo2();

console.log(num); // 100
1
2
3
4
5
6
7
8
9
10
var num = 100;

function foo() {
console.log(num); // undefined

return;
var num = 100;
}

foo();
1
2
3
4
5
6
7
8
function foo() {
var a = (b = 100);
}

foo();

// console.log(a); // ReferenceError: a is not defined
console.log(b); // 100

内存管理

概念

不管什么编程语言,在代码的执行过程中都是需要给它分配内存的,不同的是某些编程语言需要我们手动的来管理内存,某些编程语言可以自动帮助我们管理内存。

不管以什么方式来管理内存,它都有如下这些生命周期:

  1. 分配申请需要的内存;
  2. 使用分配的内存(存放一些东西,比如对象等等);
  3. 不需要使用的时候,对其进行释放;

不同的编程语言对于第一和第二步会有不同的实现:

  • 手动管理内存:比如 C、C++,都是需要手动来管理内存的申请和释放;
  • 自动管理内存:比如 Java、JavaScript、Python、Swift、Dart 等等,它们会自动帮助我们管理内存;

在 JavaScript 中会在定义变量的时候为我们分配内存。

但是由于 JavaScript 有基本数据类型和复杂数据类型,因此在内存的分配上会有不同:

  • 基本数据类型:直接在栈空间进行分配;
  • 复杂数据类型:在堆内存中开辟空间,并且将这块空间的指针返回值变量引用;

垃圾回收

因为内存的大小是有限的,所以当内存不再需要的时候,就需要对其进行释放。这就需要垃圾回收机制。

垃圾回收(Garbage Collection),简称 GC。对于那些不再使用的对象,我们称之为是垃圾,它们需要被回收,以释放更多的内存空间。

那么 GC 又怎么知道哪些对象是不再使用的呢,下面来看看常用的 GC 算法:

  1. 引用计数:当一个对象有一个引用指向它时,那这个对象的引用就 +1,当一个对象的引用为 0 时,这个对象就可以被销毁掉。但是它有一个弊端就是会产生循环引用;
  2. 标记清除:设置一个根对象,垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于那些没有引用到的对象就认为是不可用的对象;

闭包

我们来看下在计算机科学和 JavaScript 中都是如何定义的。

在计算机科学中:

  • 闭包(Closure),又称为词法闭包(lexical closure)或函数闭包(function closures);
  • 是在支持头等函数的编程语言中,实现词法绑定的一种技术;
  • 闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表);
  • 闭包与函数最大的区别是,当捕捉闭包时,它的自由变量会在捕捉时被确定,这样即使脱离了捕捉时的上下文,也能照常运行;

在 MDN 中对 JavaScript 闭包的定义:

  • 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说是函数被引用包围),这样的组合就是闭包;
  • 闭包让你可以在一个内层函数中访问到其外层函数的作用域;
  • 每当创建一个函数,闭包就会在函数创建的同时被创建出来;
1
2
3
4
5
6
7
8
9
10
function foo() {
var age = 18;
function bar() {
console.log(age);
}
return bar;
}

var fn = foo();
fn();

this及绑定规则

this全局作用域下的指向,这里有两种情况:

  1. 浏览器环境:window 对象;
  2. node 环境:{}

下面我们来看看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
console.log(this);
}

// 1. 直接调用函数
foo(); // Window

// 2. 创建对象,对象中函数指向 foo
let obj = {
name: "lqzww",
foo: foo,
};
obj.foo(); // {name: 'lqzww', foo: ƒ}

// 3. apply 调用
foo.apply("hhhh"); // String {'hhhh'}

我们会发现,上面三种打印的 this 值都不一样。

因此,this 的指向跟函数所处的位置是没有关系的,跟函数被调用的方式是有关系的。

this 有如下几种绑定规则:

  1. 默认绑定;
  2. 隐式绑定;
  3. 显式绑定;
  4. new 绑定;

默认绑定

在独立函数调用时使用默认绑定,独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用。

1
2
3
4
5
function foo() {
console.log(this); // Window
}

foo();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo1() {
console.log(this); // Window
}

function foo2() {
console.log(this); // Window
foo1();
}

function foo3() {
console.log(this); // Window
foo2();
}

foo3();
1
2
3
4
5
6
7
8
9
let obj = {
name: "lqzww",
foo: function () {
console.log(this); // Window
},
};

let bar = obj.foo;
bar();
1
2
3
4
5
6
7
8
9
10
11
function foo() {
console.log(this); // Window
}

let obj = {
name: "lqzww",
foo: foo,
};

let bar = obj.foo;
bar();
1
2
3
4
5
6
7
8
function foo() {
return function () {
console.log(this); // Window
};
}

let fn = foo();
fn();

隐式绑定

通过某个对象进行调用,也就是它的调用位置中,是通过某个对象发起的函数调用。

1
2
3
4
5
6
7
8
9
10
function foo() {
console.log(this); // {name: 'lqzww', foo: ƒ}
}

let obj = {
name: "lqzww",
foo: foo,
};

obj.foo();
1
2
3
4
5
6
7
8
9
10
11
12
13
let obj1 = {
name: "obj1",
foo: function () {
console.log(this); // {name: 'obj2', bar: ƒ}
},
};

let obj2 = {
name: "obj2",
bar: obj1.foo,
};

obj2.bar();

显式绑定

JavaScript 所有的函数都可以使用 callapply 方法。这两个函数第一个参数都是一个对象,在调用这个函数时,会将 this 绑定到这个对象上。

1
2
3
4
5
6
7
function foo() {
console.log(this);
}

foo(); // Window
foo.call(); // Window
foo.apply({ name: "lqzww" }); // {name: 'lqzww'}

callapply 区别:

  1. call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
  2. apply() 方法调用一个具有给定 this 值的函数,以及作为一个数组(或类似数组对象)提供的参数。当不确定参数的个数时,就可以使用 apply。
1
2
3
4
5
6
function sum(num1, num2) {
console.log(num1 + num2, this);
}

sum.call("call", 20, 30); // 50 String {'call'}
sum.apply("apply", [20, 30]); // 50 String {'apply'}

显式绑定除了使用 callapply 以外,还可以使用 bind

bind() 方法会返回一个新的函数,在 bind() 被调用时,这个新函数会 call 原来的函数,新函数的 this 被 bind 的第一个参数指定,其余的参数将作为新函数的参数供调用时使用。

1
2
3
4
5
6
function foo() {
console.log(this);
}

let newFoo = foo.bind("lqzww");
newFoo(); // String {'lqzww'}

new绑定

JavaScript 中的函数可以当做一个类的构造函数来使用,也就是使用 new 关键字。

使用 new 关键字来调用函数时,会进行如下几个操作:

  1. 创建一个全新的对象;
  2. 新对象会被执行 prototype 连接;
  3. 新对象会绑定到函数调用的 this 上;
  4. 如果函数没有返回其他对象,表达式会返回这个新对象;
1
2
3
4
5
6
7
8
function Person(name, age) {
console.log(this); // Person {}
this.name = name;
this.age = age;
}

let p = new Person("lqzww", 18);
console.log(p); // Person {name: 'lqzww', age: 18}

内置函数的绑定

下面来看一看一些内置函数的 this 是如何绑定的。

1
2
3
setTimeout(function () {
console.log(this); // Window
}, 1000);
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.box {
width: 100px;
height: 100px;
background-color: red;
}
</style>
</head>

<body>
<div class="box"></div>
<script>
const boxDiv = document.querySelector(".box");
boxDiv.onclick = function () {
console.log(this); // <div class="box"></div>
};

boxDiv.addEventListener("click", function () {
console.log(this); // <div class="box"></div>
});

</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
// forEach、map、filter、find
let arr = [1, 2, 3];
arr.forEach(function () {
console.log(this); // Window
});

// 第二个参数可以绑定 this
arr.forEach(function () {
console.log(this); // String {'lqzww'}
}, "lqzww");

绑定优先级

  • 默认绑定优先级最低;
  • 显式绑定 > 隐式绑定;
  • new 绑定 > 隐式绑定;
  • new 绑定 > 显式绑定(bind);

显式绑定 > 隐式绑定

1
2
3
4
5
6
7
8
9
10
11
12
let obj = {
name: "lqzww",
foo: function () {
console.log(this);
},
};

obj.foo.call("ggg"); // String {'ggg'}
obj.foo.apply("ggg"); // String {'ggg'}

let bar = obj.foo.bind("ggg");
bar(); // String {'ggg'}
1
2
3
4
5
6
7
8
9
10
function foo() {
console.log(this);
}

let obj = {
name: "lqzww",
foo: foo.bind("ggg"),
};

obj.foo(); // String {'ggg'}

new绑定 > 隐式绑定

1
2
3
4
5
6
7
8
let obj = {
name: "lqzww",
foo: function () {
console.log(this);
},
};

let o = new obj.foo(); // foo {}

new绑定 > bind绑定

1
2
3
4
5
6
function foo() {
console.log(this);
}

let bar = foo.bind("ggg");
let obj = new bar(); // foo {}

注意:new 关键字不能和 call、apply 一起使用。

总结:new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定。


特殊绑定

在开发中,总有一些语法会超出上面的规则之外,针对于这些情况,我们举例一些常见的案例。

忽略显式绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
console.log(this);
}

foo.call(null); // Window
foo.call(undefined); // Window

foo.apply(null); // Window
foo.apply(undefined); // Window

let bar1 = foo.bind(null);
bar1(); // Window

let bar2 = foo.bind(undefined);
bar2(); // Window

callapplybind 当传入的是 nullundefined 时,会自动将 this 绑定成全局对象。


间接函数引用

1
2
3
4
5
6
7
8
9
10
11
12
let obj1 = {
name: "obj1",
foo: function () {
console.log(this);
},
};

let obj2 = {
name: "obj2",
};

(obj2.bar = obj1.foo)(); // Window

箭头函数

箭头函数不使用前面的四种绑定规则,而是根据外层作用域来决定 this。

1
2
3
4
5
6
7
8
9
let foo = () => {
console.log(this);
};

let obj = { foo: foo };

foo(); // Window
foo.call("1"); // Window
obj.foo(); // Window
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let obj = {
data: [],
getData: function () {
// 箭头函数之前
// let _this = this;
// setTimeout(function () {
// let result = [1, 2, 3];
// _this.data = result;
// }, 3000);

// 使用箭头函数
setTimeout(() => {
let result = [1, 2, 3];
this.data = result;
}, 3000);
},
};

obj.getData();

相关题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let name = "lqzww";

let person = {
name: "person",
sayName: function () {
console.log(this.name);
},
};

function sayName() {
let s = person.sayName;
s();
person.sayName();
(a = person.sayName)();
}

sayName();
查看答案

依次打印:lqzww、person、lqzww。


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
let name = "lqzww";

let person1 = {
name: "person1",
foo1: function () {
console.log(this.name);
},
foo2: () => console.log(this.name),
foo3: function () {
return function () {
console.log(this.name);
};
},
foo4: function () {
return () => {
console.log(this.name);
};
},
};

let person2 = { name: "person2" };

person1.foo1();
person1.foo1.call(person2);

person1.foo2();
person1.foo2.call(person2);

person1.foo3()();
person1.foo3.call(person2)();
person1.foo3().call(person2);

person1.foo4()();
person1.foo4.call(person2)();
person1.foo4().call(person2);
查看答案

依次打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
person1
person2

lqzww
lqzww

lqzww
lqzww
person2

person1
person2
person1

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
let name = "lqzww";

function Person(name) {
this.name = name;
this.foo1 = function () {
console.log(this.name);
};
this.foo2 = () => console.log(this.name);
this.foo3 = function () {
return function () {
console.log(this.name);
};
};
this.foo4 = function () {
return () => {
console.log(this.name);
};
};
}

let person1 = new Person("person1");
let person2 = new Person("person2");

person1.foo1();
person1.foo1.call(person2);

person1.foo2();
person1.foo2.call(person2);

person1.foo3()();
person1.foo3.call(person2)();
person1.foo3().call(person2);

person1.foo4()();
person1.foo4.call(person2)();
person1.foo4().call(person2);
查看答案

依次打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
person1
person2

person1
person1

lqzww
lqzww
person2

person1
person2
person1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let name = "lqzww";

function Person(name) {
this.name = name;
this.obj = {
name: "obj",
foo1: function () {
return function () {
console.log(this.name);
};
},
foo2: function () {
return () => {
console.log(this.name);
};
},
};
}

let person1 = new Person("person1");
let person2 = new Person("person2");

person1.obj.foo1()();
person1.obj.foo1.call(person2)();
person1.obj.foo1().call(person2);

person1.obj.foo2()();
person1.obj.foo2.call(person2)();
person1.obj.foo2().call(person2);
查看答案

依次打印:

1
2
3
4
5
6
7
lqzww
lqzww
person2

obj
person2
obj

函数式编程

call、apply、bind的实现

call

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Function.prototype.myCall = function (thisArg, ...args) {
// 1. 获取需要被执行的函数
let fn = this;

// 2. 将 thisArg 转为对象类型
thisArg =
thisArg !== null && thisArg !== undefined ? Object(thisArg) : window;

// 3. 调用需要执行的函数
thisArg.fn = fn;
let result = thisArg.fn(...args);
delete thisArg.fn;

// 4. 将结果返回
return result;
};

function foo(num1, num2, num3) {
console.log("函数被执行", this);
console.log(num1, num2, num3);
}

foo.myCall({ name: "lqzww" }, 1, 2, 3);

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
Function.prototype.myApply = function (thisArg, argArray) {
// 1. 获取需要被执行的函数
let fn = this;

// 2. 将 thisArg 转为对象类型
thisArg =
thisArg !== null && thisArg !== undefined ? Object(thisArg) : window;

// 3. 调用需要执行的函数
thisArg.fn = fn;
argArray = argArray || [];
let result = thisArg.fn(...argArray);
delete thisArg.fn;

// 4. 将结果返回
return result;
};

function foo(num1, num2, num3) {
console.log("函数被执行", this);
console.log(num1, num2, num3);
}

foo.myApply({ name: "lqzww" }, [10, 20, 30]);

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
Function.prototype.myBind = function (thisArg, ...argArray) {
// 1. 获取到真实需要调用的函数
let fn = this;

// 2. 将 thisArg 转为对象类型
thisArg =
thisArg !== null && thisArg !== undefined ? Object(thisArg) : window;

function proxyFn(...args) {
// 3. 将函数放到 thisArg 中调用
thisArg.fn = fn;
let finalArgs = [...argArray, ...args];
let result = thisArg.fn(...finalArgs);
delete thisArg.fn;

// 4. 将结果返回
return result;
}

return proxyFn;
};

function foo(num1, num2, num3) {
console.log("函数被执行", this);
console.log(num1, num2, num3);
}

let bar = foo.myBind("lqzww", 10, 20, 30);
let result = bar();

arguments

使用

arguments 是一个对应于传递给函数的参数的类数组对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
// 获取参数长度
console.log(arguments.length);

// 根据索引获取某一个参数
console.log(arguments[1]);

// 获取当前 arguments 所在的函数
console.log(arguments.callee);

console.log(typeof arguments); // object
}

foo(1, 2, 3, 4, 5);

类数组意味着它并不是一个数组类型,而是一个对象类型:

  • 拥有数组的一些特性:获取 leng、通过索引访问;
  • 没有数组的一些方法:forEach、map、find 等等;

那如何将类数组转换成数组呢,有如下几种方法:

  1. 遍历;
  2. 利用 Array.prototype.slice.call
  3. 利用 Array.from()
  4. 利用展开运算符;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function foo() {
// 1. 遍历
let newArr1 = [];
for (let i = 0; i < arguments.length; i++) {
newArr1.push(arguments[i]);
}

// 2. Array.prototype.slice.call
let newArr2 = Array.prototype.slice.call(arguments);
let newArr3 = [].slice.call(arguments);

// 3. Array.from()
let newArr4 = Array.from(arguments);

// 4. 展开运算符
let newArr5 = [...arguments];
}

foo(1, 2, 3, 4, 5);

另外我们来了解下 Array.prototype.slice 的大概底层逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Array.prototype.mySlice = function (start, end) {
let arr = this;
start = start || 0;
end = end || arr.length;

let newArray = [];
for (let i = start; i < end; i++) {
newArray.push(arr[i]);
}
return newArray;
};

let newArr = Array.prototype.mySlice.call([1, 2, 3, 4, 5], 1, 4);
console.log(newArr); // [2, 3, 4]

箭头函数中没有arguments

  • 在浏览器环境里,它会往上层作用域里找,但是在全局作用域里是找不到 arguments 的;
  • 在 node 环境里,全局作用域里是有 arguments 的;
1
2
3
4
5
let foo = () => {
console.log(arguments); // 报错:Uncaught ReferenceError: arguments is not defined
};

foo();
1
2
3
4
5
6
7
8
9
function foo() {
let bar = () => {
console.log(arguments); // Arguments [1, callee: ƒ, Symbol(Symbol.iterator): ƒ]
};
return bar;
}

let fn = foo(1);
fn();

那该如何拿到很多参数呢,在箭头函数里可以使用剩余参数来获取:

1
2
3
4
5
let foo = (num1, num2, ...args) => {
console.log(num1, num2, args); // 1 2 (3) [3, 4, 5]
};

foo(1, 2, 3, 4, 5);

纯函数

维基百科中定义的。在程序设计中,若一个函数符合下面条件,那么这个函数称为纯函数:

  • 此函数在相同的输入值时,需产生相同的输出;
  • 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由 I/O 设备产生的外部输出无关;
  • 该函数不能有语义上可观察的函数副作用;

总的来说就是:确定的输入,一定会产生确定的输出。并且函数执行过程中不能产生副作用。

其中副作用的含义是:在执行一个函数时,除了返回函数值外,还对调用函数产生了附加的影响,比如修改全局变量、修改参数、改变外部存储。

下面来看看 slicesplice

1
2
3
4
5
6
7
8
let name = ["hh", "gg", "aa", "bb"];
let newName1 = name.slice(0, 2);
console.log(newName1); // ['hh', 'gg']
console.log(name); // ['hh', 'gg', 'aa', 'bb']

let newName2 = name.splice(1, 4);
console.log(newName2); // ['gg', 'aa', 'bb']
console.log(name); // ['hh']

从上面代码我们可以看出:

  • slice:它不会修改原来的数组,它本身就是一个纯函数;
  • splice:它会修改原数组,这个操作就是产生了副作用,因此它不是一个纯函数;

柯里化

先来看看维基百科中如何定义的:

  • 在计算机科学里,柯里化又译为卡瑞化或加里化;
  • 把接收多个参数的函数变成接受一个单一参数的函数,并且返回接受余下的参数,而且返回结果的新函数的技术;
  • 如果你固定某些参数,你将得到接受余下参数的一个函数;

简单的说就是:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数,这个过程就是柯里化。

1
2
3
4
5
6
7
8
9
10
11
12
13
function add(x, y, z) {
return x + y + z;
}
console.log(add(1, 2, 3)); // 6

function sum(x) {
return function (y) {
return function (z) {
return x + y + z;
};
};
}
console.log(sum(1)(2)(3)); // 6

上面代码中,我们可以把 add 函数转变成 sum 函数的过程,称之为柯里化。

下面来看看为什么需要有柯里化呢?

  • 单一职责原则:在函数式编程中,其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理;
  • 逻辑的复用;
1
2
3
4
5
6
7
8
9
10
function add(num1) {
return function (num2) {
return num1 + num2;
};
}

let add5 = add(5);
console.log(add5(10));
console.log(add5(20));
console.log(add5(30));

下面来看看柯里化函数的实现,就是传入一个普通函数,让它转换成柯里化函数并返回:

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 add(x, y, z) {
return x + y + z;
}

function myCurrying(fn) {
function curried(...args) {
// 判断已经传入的参数大于等于需要的参数时,就执行函数
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
// 否则返回一个新的函数,继续接收参数
function curried2(...args2) {
// 接收到参数后,递归调用来检查函数的个数是否达到
return curried.apply(this, args.concat(args2));
}
return curried2;
}
}
return curried;
}

let curryAdd = myCurrying(add);
console.log(curryAdd(1, 2, 3)); // 6
console.log(curryAdd(1, 2)(3)); // 6
console.log(curryAdd(1)(2)(3)); // 6

组合函数

组合函数是在 JavaScript 开发过程中一种对函数的使用技巧、模式。

我们在需要对某一个数据进行函数的调用,依次执行两个函数,如果每次我们都需要进行两个函数的调用就会显示重复,那么是否可以将这两个函数组合起来,自动依次调用。这个过程就是对函数的组合,称之为组合函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function add(num) {
return num + num;
}

function double(num) {
return num * num;
}

let count = 10;
let result = double(add(count));
console.log(result);

function composeFn(fn1, fn2) {
return function (count) {
return fn2(fn1(count));
};
}

let result2 = composeFn(add, double);
console.log(result2(10));

上面代码中,我们称 composeFn 函数就是一个组合函数。

下面来实现一下通用的组合函数 myCompose

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 myCompose(...fns) {
let length = fns.length;
// 类型判断
for (let i = 0; i < length; i++) {
if (typeof fns[i] !== "function") {
throw new TypeError("Expected arguments are functions");
}
}
return function (...args) {
let index = 0;
let result = length ? fns[index].apply(this, args) : args;
while (++index < length) {
result = fns[index].call(this, result);
}
return result;
};
}

function add(num) {
return num + num;
}

function double(num) {
return num * num;
}

let newFn = myCompose(add, double);
console.log(newFn(10));

严格模式

在 ES5 标准中,JavaScript 提出了严格模式的概念:

  • 它是一种具有限制性的 JavaScript 模式,从而使代码隐式的脱离了 懒散(sloppy)模式
  • 支持严格模式的浏览器在检测到代码中有严格模式时,会以更加严格的方式对代码进行检测和执行;

严格模式对正常的 JavaScript 语义进行了一些限制:

  • 严格模式通过抛出错误来消除一些原有的静默错误;
  • 严格模式让 JS 引擎在执行代码时可以进行更多的优化(不需要对一些特殊的语法进行处理);
  • 严格模式禁用了在 ES 未来版本中可能会定义的一些语法;

开启严格模式的方式:

  • 可以在 js 文件中开启;
  • 也可以对某一个函数开启;
1
2
3
4
"use strict";

message = "hello";
console.log(message);
1
2
3
4
5
6
7
8
function foo() {
"use strict";

message = "hello";
console.log(message);
}

foo();

下面来看看在严格模式下的几种语法限制:

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
// "use strict";

// 1. 禁止意外的创建全局变量
message = "hello";

function foo1() {
age = 18;
}
foo1();

// 2. 不允许函数有相同的参数名称
function foo2(x, y, x) {
console.log(x, y, x);
}
foo2(1, 2, 3);

// 3. 静默错误
true.name = "11";
NaN = 123;

// 4. 不允许使用原先0开头的八进制
let num1 = 0123; // 严格模式下:Uncaught SyntaxError: Octal literals are not allowed in strict mode.
let num2 = 0o123; // 可以
let num3 = 0x123; // 可以

// 5. with 语句不能使用
let obj = { name: "lqzww" };
with (obj) {
console.log(obj);
}

// 6. eval 函数不会向上引用变量
let s1 = 'var message1 = "hi"; console.log(message1);';
eval(s1);
console.log(message1);

// 7. 在严格模式下,this 的默认绑定会执行 undefined
function foo3() {
console.log(this);
}
let obj2 = {
name: "lqzww",
foo: foo3,
};
foo3();
obj2.foo();
let bar = obj2.foo;
bar();

// 8. setTimeout 中的 this,不管是否是严格模式都指向window
setTimeout(function () {
console.log(this);
}, 2000);

面向对象

概念

JavaScript 支持多种编程范式,包括函数式编程和面向对象编程:

  • JS 的对象被设计成一组属性的无序集合,是由 key 和 value 组成;
  • 其中 key 是一个标识符名称,value 可以是任意类型,也可以是其他对象或者函数类型;
  • 如果值是一个函数,可以称之为是对象的方法;

下面来看看创建对象的两种方式:

  1. 通过 new Object() 创建;
  2. 通过字面量形式创建;
1
2
3
4
5
6
7
8
9
10
11
12
// 1. new Object() 创建
let obj1 = new Object();
obj1.name = "lqzww";
obj1.age = 18;
obj1.like = function () {};

// 2. 字面量形式创建
let obj2 = {
name: "lqzww",
age: 18,
like: function () {},
};

Object.defineProperty

如果要对一个属性进行比较精准的操作控制,比如某个属性是否可以通过 delete 删除这些等等,我们就可以使用属性描述符。

属性描述符可以精准的添加和修改对象的属性,它需要使用 Object.defineProperty 来对属性进行添加或修改。

Object.defineProperty() 方法会直接在一个对象上定义一个新属性或修改现有属性,并返回此对象。

语法:

1
Object.defineProperty(obj, prop, descriptor)

它可以接收如下三个参数:

  1. obj:要定义属性的对象;
  2. prop:要定义或修改的属性的名称或 Symbol;
  3. descriptor:要定义或修改的属性描述符;

属性描述符的类型又分为两种:

  • 数据描述符;
  • 存取描述去;
类型 configurable enumerable value writable get set
数据描述符 可以 可以 可以 可以 不可以 不可以
存取描述符 可以 可以 不可以 不可以 可以 可以
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let obj = {
name: "lqzww",
age: 18,
};

Object.defineProperty(obj, "address", {
value: "hhh",
configurable: false,
enumerable: true,
writable: true,
});

delete obj.address;
console.log(obj.address);

for (const key in obj) {
console.log(key);
}

obj.address = "qqqq";
console.log(obj);

34