From 2a872d975d3865c7b091dc8d21ec718cd97f64e5 Mon Sep 17 00:00:00 2001 From: xiejie <745007854@qq.com> Date: Wed, 3 Nov 2021 10:05:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=9F=A5=E8=AF=86=E5=9B=BE?= =?UTF-8?q?=E8=B0=B1=E5=92=8C=E5=BA=8F=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../let、var、const的区别.html | 41 + .../let、var、const的区别.md | 336 + 02. 值和引用/值和引用.js | 36 + 02. 值和引用/值和引用.md | 320 + 03. 包装类型/包装类型.js | 45 + 03. 包装类型/包装类型.md | 158 + 04. 数据类型的转换/数据类型的转换.js | 149 + 04. 数据类型的转换/数据类型的转换.md | 530 ++ 05. 运算符/运算符.js | 181 + 05. 运算符/运算符.md | 1369 +++ 06. 原型和原型链/原型和原型链.js | 99 + 06. 原型和原型链/原型和原型链.md | 368 + 07. 执行栈和执行上下文/执行栈和执行上下文.js | 120 + 07. 执行栈和执行上下文/执行栈和执行上下文.md | 318 + 08. 作用域和作用域链/作用域和作用域链.html | 25 + 08. 作用域和作用域链/作用域和作用域链.js | 37 + 08. 作用域和作用域链/作用域和作用域链.md | 423 + 09. this指向/this指向.js | 264 + 09. this指向/this指向.md | 803 ++ 10. 闭包/闭包.js | 80 + 10. 闭包/闭包.md | 430 + .../DOM 事件的注册和移除.html | 40 + .../DOM 事件的注册和移除.md | 199 + 12. DOM事件的传播机制/DOM 事件的传播机制.html | 47 + 12. DOM事件的传播机制/DOM 事件的传播机制.md | 316 + 13. 阻止事件的默认行为/阻止事件默认行为.html | 23 + 13. 阻止事件的默认行为/阻止事件默认行为.md | 170 + 14. 递归/递归.js | 49 + 14. 递归/递归.md | 146 + 15. 属性描述符/属性描述符.js | 154 + 15. 属性描述符/属性描述符.md | 315 + .../class 和构造函数区别.js | 121 + .../class 和构造函数区别.md | 578 ++ 17. 浮点数精度问题/浮点数精度问题.js | 29 + 17. 浮点数精度问题/浮点数精度问题.md | 241 + 18. 严格模式/严格模式.js | 49 + 18. 严格模式/严格模式.md | 294 + 19. 函数防抖和节流/函数防抖和节流.html | 87 + 19. 函数防抖和节流/函数防抖和节流.md | 333 + 20. 垃圾回收与内存泄漏/垃圾回收与内存泄漏.md | 182 + 21. WeakSet和WeakMap/WeakSet 和 WeakMap.js | 101 + 21. WeakSet和WeakMap/WeakSet 和 WeakMap.md | 552 ++ 22. 深浅拷贝/深浅拷贝.html | 231 + 22. 深浅拷贝/深浅拷贝.md | 532 ++ 23. 函数柯里化/函数柯里化.js | 116 + 23. 函数柯里化/函数柯里化.md | 318 + 24. Node的事件循环/Node的事件循环.html | 61 + 24. Node的事件循环/Node的事件循环.js | 131 + 24. Node的事件循环/Node的事件循环.md | 549 ++ 25. eval/eval.md | 45 + javascript 面试题汇总.md | 8026 +++++++++++++++++ 知识图谱.dio | 127 + 52 files changed, 20294 insertions(+) create mode 100644 01. let、var、const的区别/let、var、const的区别.html create mode 100644 01. let、var、const的区别/let、var、const的区别.md create mode 100644 02. 值和引用/值和引用.js create mode 100644 02. 值和引用/值和引用.md create mode 100644 03. 包装类型/包装类型.js create mode 100644 03. 包装类型/包装类型.md create mode 100644 04. 数据类型的转换/数据类型的转换.js create mode 100644 04. 数据类型的转换/数据类型的转换.md create mode 100644 05. 运算符/运算符.js create mode 100644 05. 运算符/运算符.md create mode 100644 06. 原型和原型链/原型和原型链.js create mode 100644 06. 原型和原型链/原型和原型链.md create mode 100644 07. 执行栈和执行上下文/执行栈和执行上下文.js create mode 100644 07. 执行栈和执行上下文/执行栈和执行上下文.md create mode 100644 08. 作用域和作用域链/作用域和作用域链.html create mode 100644 08. 作用域和作用域链/作用域和作用域链.js create mode 100644 08. 作用域和作用域链/作用域和作用域链.md create mode 100644 09. this指向/this指向.js create mode 100644 09. this指向/this指向.md create mode 100644 10. 闭包/闭包.js create mode 100644 10. 闭包/闭包.md create mode 100644 11. DOM事件的注册和移除/DOM 事件的注册和移除.html create mode 100644 11. DOM事件的注册和移除/DOM 事件的注册和移除.md create mode 100644 12. DOM事件的传播机制/DOM 事件的传播机制.html create mode 100644 12. DOM事件的传播机制/DOM 事件的传播机制.md create mode 100644 13. 阻止事件的默认行为/阻止事件默认行为.html create mode 100644 13. 阻止事件的默认行为/阻止事件默认行为.md create mode 100644 14. 递归/递归.js create mode 100644 14. 递归/递归.md create mode 100644 15. 属性描述符/属性描述符.js create mode 100644 15. 属性描述符/属性描述符.md create mode 100644 16. Class和普通构造器的区别/class 和构造函数区别.js create mode 100644 16. Class和普通构造器的区别/class 和构造函数区别.md create mode 100644 17. 浮点数精度问题/浮点数精度问题.js create mode 100644 17. 浮点数精度问题/浮点数精度问题.md create mode 100644 18. 严格模式/严格模式.js create mode 100644 18. 严格模式/严格模式.md create mode 100644 19. 函数防抖和节流/函数防抖和节流.html create mode 100644 19. 函数防抖和节流/函数防抖和节流.md create mode 100644 20. 垃圾回收与内存泄漏/垃圾回收与内存泄漏.md create mode 100644 21. WeakSet和WeakMap/WeakSet 和 WeakMap.js create mode 100644 21. WeakSet和WeakMap/WeakSet 和 WeakMap.md create mode 100644 22. 深浅拷贝/深浅拷贝.html create mode 100644 22. 深浅拷贝/深浅拷贝.md create mode 100644 23. 函数柯里化/函数柯里化.js create mode 100644 23. 函数柯里化/函数柯里化.md create mode 100644 24. Node的事件循环/Node的事件循环.html create mode 100644 24. Node的事件循环/Node的事件循环.js create mode 100644 24. Node的事件循环/Node的事件循环.md create mode 100644 25. eval/eval.md create mode 100644 javascript 面试题汇总.md create mode 100644 知识图谱.dio diff --git a/01. let、var、const的区别/let、var、const的区别.html b/01. let、var、const的区别/let、var、const的区别.html new file mode 100644 index 0000000..ed609a6 --- /dev/null +++ b/01. let、var、const的区别/let、var、const的区别.html @@ -0,0 +1,41 @@ + + + + + + + + Document + + + + + + + \ No newline at end of file diff --git a/01. let、var、const的区别/let、var、const的区别.md b/01. let、var、const的区别/let、var、const的区别.md new file mode 100644 index 0000000..f1694ed --- /dev/null +++ b/01. let、var、const的区别/let、var、const的区别.md @@ -0,0 +1,336 @@ +# *let、var、const* 的区别 + + + +## 经典真题 + + + +- *let const var* 的区别?什么是块级作用域?如何用? + + + +## 声明变量关键字汇总 + + + +在 *JavaScript* 中,一共存在 *3* 种声明变量的方式: + +- *var* +- *let* +- *const* + +之所以有 *3* 种方式,这是由于历史原因造成的。最初声明变量的关键字就是 *var*,但是为了解决作用域的问题,所以后面新增了 *let* 和 *const* 的方式。 + + + +### 作用域 + + + +首先我们来了解一下作用域。 + +*ES5* 中的作用域有:全局作用域、函数作用域,*ES6* 中新增了块级作用域。块作用域由 { } 包括,*if* 语句和 *for* 语句里面的 { } 也属于块作用域。 + +关于作用域的更多内容,可以参阅《作用域和作用域链》章节。 + + + +### *var* 关键字 + + + +1. 没有块级作用域的概念 + +```js +//Global Scope +{ + var a = 10; +} +console.log(a); //10 +``` + +上面代码中,在 *Global Scope*(全局作用域)中,且在 *Block Scope*(块级作用域) { } 中,*a* 输出结果为 *10*,由此可以看出 *var* 声明的变量不存在 *Block Scope* 的概念 + + + +2. 有全局作用域、函数作用域的概念 + +```js +//Global Scope +var a = 10; +function checkscope(){ + //Local Scope + var b = 20; + console.log(a); //10 + console.log(b); //20 +} +checkscope(); +console.log(b); //ReferenceError: b is not defined +``` + +上面代码中,在 *Global Scope* 中用 *var* 声明了 *a*,在 *checkscope* 函数中的 *Local Scope*(本地作用域、函数作用域)中打印出了 *10*,但是在 *Global Scope* 中打印的变量 *b* 报错了。 + + + +3. 不初始化值默认为 *undefined* + +```js +//Global Scope +var a; +console.log(a); //undefined +``` + +上面代码中,在 *Global Scope* 中用 *var* 声明了 *a*,但没有初始化值,它的值默认为 *undefined*,这里是 *undefined* 是 *undefined* 类型,而不是字符串。 + + + +4. 存在变量提升 + +```js +//Global Scope +console.log(a); //undefined +var a = 10; + +checkscope(); +function checkscope(){ + //Local Scope + console.log(a); //undefined + var a; +} +``` + +上面代码中,先打印了 *a*,然后用 *var* 声明变量 *a*。变量提升是因为 *js* 需要经历编译和执行阶段。而 *js* 在编译阶段的时候,会搜集所有的变量声明并且提前声明变量。 + +可以将这个过程形象地想象成所有的声明(变量)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。 + +至于 *checkscope* 函数中的变量 *a* 为什么输出 *undefined*,可以参阅《作用域和作用域链》章节。 + + + +5. 全局作用域用 *var* 声明的变量会挂载到 *window* 对象下 + +```js +//Global Scope +var a = 10; +console.log(a); //10 +console.log(window.a); //10 +console.log(this.a); //10 +``` + +上面代码中,打印出了 *3* 个 *10*,访问 *a* 和 *window.a* 或是 *this.a* 都是等价的。 + +举个例子:比如我要访问 *location* 对象,使用 *location* 可以访问,使用 *window.location* 也可以访问,只不过 *window* 对象可以省略不写,就像 *new Array( )* 和 *new window.Array( )* 是等价的。 + + + +6. 同一作用域中允许重复声明 + +```js +//Global Scope +var a = 10; +var a = 20; +console.log(a); //20 + +checkscope(); +function checkscope(){ + //Local Scope + var b = 10; + var b = 20; + console.log(b); //20 +} +``` + +上面代码中,在 *Global Scope* 中声明了 *2* 次 *a*,以最后一次声明有效,打印为 *20*。同理,在 *Local Scope* 也是一样的。 + + + +### *let* 关键字 + + + +1. 有块级作用域的概念 + +```js +{ + //Block Scope + let a = 10; +} +console.log(a); //ReferenceError: a is not defined +``` + +上面代码中,打印 *a* 报错,说明存在 *Block Scope* 的概念。 + + + +2. 不存在变量提升 + +```js +{ + //Block Scope + console.log(a); //ReferenceError: Cannot access 'a' before initialization + let a = 10; +} +``` + +上面代码中,打印 *a* 报错:无法在初始化之前访问。说明不存在变量提升。 + + + +3. 暂时性死区 + +```js +{ + //Block Scope + console.log(a); //ReferenceError: Cannot access 'a' before initialization + let a = 20; +} + +if (true) { + //TDZ开始 + console.log(a); //ReferenceError: Cannot access 'a' before initialization + + let a; //TDZ结束 + console.log(a); //undefined + + a = 123; + console.log(a); //123 +} +``` + +上面代码中,使用 *let* 声明的变量 *a*,导致绑定这个块级作用域,所以在 *let* 声明变量前,打印的变量 *a* 报错。 + +这是因为使用 *let/const* 所声明的变量会存在暂时性死区。 + +什么叫做暂时性死区域呢? + +*ES6* 标准中对 *let/const* 声明中的解释 [第13章](https://link.segmentfault.com/?enc=K6pZVwgVNQb0IBQ9LTOuJg%3D%3D.p07UoPCGl5RslJ9ZnW9Nr36NFqs2pU%2FnSfWZUPIH3S1TUXzWdj22pH0lUMFVGVUwJkDpSHrYe8uKlYek%2FK4HBDYkJhc%2Fe2xiWo5V6teR%2BXY%3D),有如下一段文字: + +> *The variables are created when their containing Lexical Environment is instantiated but may not be accessed inany way until the variable’s LexicalBinding is evaluated.* + + + +翻译成人话就是: + +> 当程序的控制流程在新的作用域(*module、function* 或 *block* 作用域)进行实例化时,在此作用域中用 *let/const* 声明的变量会先在作用域中被创建出来,但因此时还未进行词法绑定,所以是不能被访问的,如果访问就会抛出错误。因此,在这运行流程进入作用域创建变量,到变量可以被访问之间的这一段时间,就称之为暂时死区。 + + + +再简单理解就是: + +>*ES6* 规定,*let/const* 命令会使区块形成封闭的作用域。若在声明之前使用变量,就会报错。 +>总之,在代码块内,使用 *let/const* 命令声明变量之前,该变量都是不可用的。 +>这在语法上,称为 **“暂时性死区”**( *temporal dead zone*,简称 ***TDZ***)。 + + + +其实上面不存在变量提升的例子中,其实也是暂时性死区,因为它有暂时性死区的概念,所以它压根就不存在变量提升了。 + + + +4. 同一块作用域中不允许重复声明 + +```js +{ + //Block Scope + let A; + var A; //SyntaxError: Identifier 'A' has already been declared +} +{ + //Block Scope + var A; + let A; //SyntaxError: Identifier 'A' has already been declared +} +{ + //Block Scope + let A; + let A; //SyntaxError: Identifier 'A' has already been declared +} +``` + + + +### *const* 关键字 + + + +1. 必须立即初始化,不能留到以后赋值 + +```js +// Block Scope +const a; // SyntaxError: Missing initializer in const declaration } +``` + +上面代码中,用 *const* 声明的变量 *a* 没有进行初始化,所以报错。 + + + +2. 常量的值不能改变 + +```js +//Block Scope +{ + const a = 10; + a = 20; // TypeError: Assignment to constant variable +} +``` + +上面代码中,用 *const* 声明了变量 *a* 且初始化为 *10*,然后试图修改 *a* 的值,报错。 + +*const* 实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。 + + + +### 特点总结 + + + +- *var* 关键字 + +1. 没有块级作用域的概念 +2. 有全局作用域、函数作用域的概念 +3. 不初始化值默认为 *undefined* +4. 存在变量提升 +5. 全局作用域用 *var* 声明的变量会挂载到 *window* 对象下 +6. 同一作用域中允许重复声明 + + + +- *let* 关键字 + +1. 有块级作用域的概念 +2. 不存在变量提升 +3. 暂时性死区 +5. 同一块作用域中不允许重复声明 + + + +- *const* 关键字 + +1. 与 *let* 特性一样,仅有 *2* 个差别 +2. 区别 1:必须立即初始化,不能留到以后赋值 +3. 区别 2:常量的值不能改变 + + + +## 真题解答 + + + +- *let const var* 的区别?什么是块级作用域?如何用? + +>参考答案: +> +>1. *var* 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问,有变量提升。 +>2. *let* 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,无变量提升,不可以重复声明。 +>3. *const* 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改,无变量提升,不可以重复声明。 +> +>最初在 *JS* 中作用域有:全局作用域、函数作用域。没有块作用域的概念。 +> +>*ES6* 中新增了块级作用域。块作用域由 { } 包括,*if* 语句和 *for* 语句里面的 { } 也属于块作用域。 +> +>在以前没有块作用域的时候,在 *if* 或者 *for* 循环中声明的变量会泄露成全局变量,其次就是 { } 中的内层变量可能会覆盖外层变量。块级作用域的出现解决了这些问题。 + + + +-*EOF*- \ No newline at end of file diff --git a/02. 值和引用/值和引用.js b/02. 值和引用/值和引用.js new file mode 100644 index 0000000..9b3d913 --- /dev/null +++ b/02. 值和引用/值和引用.js @@ -0,0 +1,36 @@ +// function a(){ +// function b(){ +// function c(){ + +// } +// c(); +// } +// b(); +// } +// a(); + +// var obj = { +// name : "xiejie", +// age : 18, +// score : 100, +// newStu : { +// name : "zhangsan", +// age : 19 +// } +// } + +// var a = 10; +// var b = 10; +// console.log(a === b); // true + +// var arr1 = [1,2,3]; +// var arr2 = [1,2,3]; +// console.log(arr1 === arr2); // false + +var obj = {}; +obj.name = "xiejie"; +console.log(obj.name); // xiejie + +var str = "Hello"; +str.name = "xiejie"; +console.log(str.name); // undefinded diff --git a/02. 值和引用/值和引用.md b/02. 值和引用/值和引用.md new file mode 100644 index 0000000..77100c4 --- /dev/null +++ b/02. 值和引用/值和引用.md @@ -0,0 +1,320 @@ +# 值和引用 + + + +## 经典真题 + + + +- *JS* 的基本数据类型有哪些?基本数据类型和引用数据类型的区别 + + + +## 值和引用相关内容 + + + +在 *JavaScript* 中,数据类型整体上来讲可以分为两大类:**基本类型**和**引用数据类型** + +基本数据类型,一共有 *6* 种: + +```text +string,symbol,number,boolean,undefined,null +``` + +其中 *symbol* 类型是在 *ES6* 里面新添加的基本数据类型。 + +引用数据类型,就只有 *1* 种: + +```js +object +``` + +基本数据类型的值又被称之为原始值或简单值,而引用数据类型的值又被称之为复杂值或引用值。 + + + +那么两者之间具体有什么区别呢?我们一点一点来看: + + + +#### 1. 简单值(原始值) + +**简单值是表示 *JavaScript* 中可用的数据或信息的最底层形式或最简单形式。**简单类型的值被称为简单值,是因为它们是**不可细化**的。 + +也就是说,数字是数字,字符串是字符串,布尔值是 *true* 或 *false*,*null* 和 *undefined* 就是 *null* 和 *undefined*。这些值本身很简单,不能够再进行拆分。 + +由于简单值的数据大小是固定的,所以**简单值的数据是存储于内存中的栈区里面的。** + + + +要简单理解栈的存取方式,我们可以通过类比乒乓球盒子来分析。如下图: + +![img](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-09-30-025405.png) + + + +下面是具体的代码示例: + +```js +var str = "Hello World"; +var num = 10; +var bol = true; +var myNull = null; +var undef = undefined; +console.log(typeof str); // string +console.log(typeof num); // number +console.log(typeof bol); // boolean +console.log(typeof myNull); // object +console.log(typeof undef); // undefined +``` + +这里面 *null* 比较特殊,打印出来是 *object*,这是由于历史原因所遗留下来的问题。 + +是来源于 *JavaScript* 从第一个版本开始时的一个 *bug*,并且这个 *bug* 无法被修复。因为修复会破坏现有的代码。 + +具体原因是因为不同的对象在底层都表现为二进制,在 *JavaScript* 中二进制前三位都为 *0* 的话会被判断为 *object* 类型,*null* 的二进制全部为 *0*,自然前三位也是 *0*,所以执行 *typeof* 值会返回 *object*。 + + + +例外,当我们打印 *null == undefined* 的时候,返回的是 *true*,这也是面试时经常会被问到的一个问题。 + +这两个值都表示“无”的意思。 + +通常情况下, 当我们试图访问某个不存在的或者没有赋值的变量时,就会得到一个 *undefined* 值。*Javascript* 会自动将声明是没有进行初始化的变量设为 *undifined*。 + +而 *null* 值表示空,*null* 不能通过 *Javascript* 来自动赋值,也就是说必须要我们自己手动来给某个变量赋值为 *null*。 + +那么为什么 *JavaScript* 要设置两个表示"无"的值呢? + +这其实也是因为历史原因。 + +*1995* 年 *JavaScript* 诞生时,最初像 *Java* 一样,只设置了 *null* 作为表示"无"的值。根据 *C* 语言的传统,*null* 被设计成可以自动转为 *0*。 + +但是,*JavaScript* 的设计者,觉得这样做还不够,主要有以下两个原因。 + +1. *null* 像在 *Java* 里一样,被当成一个对象。但是,*JavaScript* 的数据类型分成原始类型(*primitive*)和复合类型(*complex*)两大类,作者觉得表示“无”的值最好不是对象。 +2. *JavaScript* 的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败。作者觉得,如果 *null* 自动转为 *0*,很不容易发现错误。 + +因此,作者又设计了一个 *undefined*。**这里注意:先有 *null* 后有 *undefined* 出来,*undefined* 是为了填补之前的坑。** + +*JavaScript* 的最初版本是这样区分的: + + + +*null* 是一个表示“无”的对象(空对象指针),转为数值时为 *0*; + +典型用法是: + +- 作为函数的参数,表示该函数的参数不是对象。 + +- 作为对象原型链的终点。 + + + +*undefined* 是一个表示"无"的原始值,转为数值时为 *NaN*。 + +典型用法是: + +- 变量被声明了,但没有赋值时,就等于 *undefined*。 +- 调用函数时,应该提供的参数没有提供,该参数等于 *undefined*。 +- 对象没有赋值的属性,该属性的值为 *undefined*。 +- 函数没有返回值时,默认返回 *undefined*。 + + + +#### 2. 复杂值(引用值) + +在 *JavaScript* 中,对象就是一个复杂值。因为对象可以向下拆分,拆分成多个简单值或者复杂值。 + +**复杂值在内存中的大小是未知的,因为复杂值可以包含任何值,而不是一个特定的已知值,所以复杂值的数据都是存储于堆区里面。** + +如下图所示: + +![img](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-09-30-025509.png) + +下面是具体的代码示例: + +```js +// 简单值 +var a1 = 0; +var a2 = "this is str"; +var a3 = null + +// 复杂值 +var c = [1, 2, 3]; +var d = {m: 20}; +``` + + + +#### 3. 访问方式 + +**按值访问** + +简单值是作为不可细化的值进行存储和使用的,引用它们会转移其值。 + +```js +var str = "Hello"; +var str2 = str; +str = null; +console.log(str,str2); // null "Hello" +``` + +**引用访问** + +复杂值是通过引用进行存储和操作的,而不是实际的值。创建一个包含复杂对象的变量时,其值是内存中的一个引用地址。引用一个复杂对象时,使用它的名称(即变量或对象属性)通过内存中的引用地址获取该对象值。 + +```js +var obj = {}; +var obj2 = obj; +obj.name = "zhangsan"; +console.log(obj.name); // zhangsan +console.log(obj2.name); // zhangsan +``` + + + +#### 4. 比较方式 + +简单值采用值比较,而复杂值采用引用比较。复杂值只有在引用相同的对象(即有相同的地址)时才相等。即使是包含相同对象的两个变量也彼此不相等,因为它们并不指向同一个对象。 + +示例 1: + +```js +var a = 10; +var b = 10; +var c = new Number(10); +var d = c; +console.log(a === b); // true +console.log(a === c); // false +console.log(a === c); // false +console.log(a == c); // true +d = 10; +console.log(d == c); // true +console.log(d === c); // false +``` + +示例 2: + +```js +var obj = {name : 'zhangsan'}; +var obj2 = {name : 'zhangsan'}; +console.log(obj == obj2); // false +console.log(obj === obj2); // false +var obj3 = {name : 'zhangsan'}; +var obj4 = obj3; +console.log(obj3 == obj4); // true +console.log(obj3 === obj4); // ture +``` + + + +#### 5. 动态属性 + +对于复杂值,可以为其添加属性和方法,也可以改变和删除其属性和方法。但简单值不可以: + +```js +var str = 'test'; +str.abc = true; +console.log(str.abc); // undefined +var obj = {}; +obj.abc = true; +console.log(obj.abc); // true +``` + + + +复杂值支持动态对象属性,因为我们可以定义对象,然后创建引用,再更新对象,并且所有指向该对象的变量都会获得更新。 + +一个新变量指向现有的复杂对象,并没有复制该对象。这就是复杂值有时被称为引用值的原因。复杂值可以根据需求有任意多个引用,即使对象改变,它们也总是指向同一个对象 + +```js +var obj = {name : 'zhangsan'}; +var obj2 = obj; +var obj3 = obj2; +obj.name = 'abc'; +console.log(obj.name, obj2.name, obj3.name); +// abc abc abc +``` + + + +#### 6. 变量赋值 + +最后说一下关于变量的赋值,其实是可以分为直接赋值和引用赋值的。直接赋值,就是指将简单值赋值给变量,而引用赋值是指将一个复杂值的引用赋值给变量,这个引用指向堆区实际存在的数据。 + +**直接赋值** + +```js +var a = 3; +var b = a; +b = 5; +console.log(a); // 3 +``` + +**引用赋值** + +```js +var a = {value : 1}; +var b = a; +b.value = 10; +console.log(a.value); // 10 +``` + + + +## 真题解答 + + + +- *JS* 的基本数据类型有哪些?基本数据类型和引用数据类型的区别 + +> 参考答案: +> +> 在 *JavaScript* 中,数据类型整体上来讲可以分为两大类:**基本类型**和**引用数据类型** +> +> 基本数据类型,一共有 *6* 种: +> +> ```text +> string,symbol,number,boolean,undefined,null +> ``` +> +> 其中 *symbol* 类型是在 *ES6* 里面新添加的基本数据类型。 +> +> 引用数据类型,就只有 *1* 种: +> +> ```js +> object +> ``` +> +> 基本数据类型的值又被称之为原始值或简单值,而引用数据类型的值又被称之为复杂值或引用值。 +> +> 两者的区别在于: +> +> **原始值是表示 *JavaScript* 中可用的数据或信息的最底层形式或最简单形式。**简单类型的值被称为原始值,是因为它们是**不可细化**的。 +> +> 也就是说,数字是数字,字符是字符,布尔值是 *true* 或 *false*,*null* 和 *undefined* 就是 *null* 和 *undefined*。这些值本身很简单,不能够再进行拆分。由于原始值的数据大小是固定的,所以**原始值的数据是存储于内存中的栈区里面的。** +> +> 在 *JavaScript* 中,对象就是一个引用值。因为对象可以向下拆分,拆分成多个简单值或者复杂值。**引用值在内存中的大小是未知的,因为引用值可以包含任何值,而不是一个特定的已知值,所以引用值的数据都是存储于堆区里面。** +> +> 最后总结一下两者的区别: +> +> 1. 访问方式 +> - 原始值:访问到的是值 +> - 引用值:访问到的是引用地址 +> 2. 比较方式 +> - 原始值:比较的是值 +> - 引用值:比较的是地址 +> +> 3. 动态属性 +> - 原始值:无法添加动态属性 +> - 引用值:可以添加动态属性 +> 4. 变量赋值 +> - 原始值:赋值的是值 +> - 引用值:赋值的是地址 + + + +-*EOF*- + diff --git a/03. 包装类型/包装类型.js b/03. 包装类型/包装类型.js new file mode 100644 index 0000000..580079f --- /dev/null +++ b/03. 包装类型/包装类型.js @@ -0,0 +1,45 @@ +// var str = "Hello"; // 当使用属性方法时,内部调用 new String("Hello") 生成一个临时的包装对象 +// var str2 = new String("World"); + + +// var i = 1; // 当使用属性方法时,内部调用 new Number(1) 生成一个临时的包装对象 +// var j = new Number(3.1415926); + +// var isPass = true; +// var isPass2 = new Boolean(false); + +// console.log(j.toFixed(2)); +// console.log(str.charAt(0)); +// console.log((1).toFixed(2)); + +// 通过上面第 11、12、13 行代码,我们惊讶的发现普通数据类型也可以使用属性方法 +// 因为当我们使用属性和方法的时候,JS 内部会自动进行一个转换 +// 会自动生成一个包装对象 + + +// var test = {}; +// test.name = "xiejie"; +// test.sayHello = function(){ +// console.log("Hello"); +// } +// console.log(test.name); +// test.sayHello(); + +// var i = 1; +// i.test = "Hello"; +// console.log(i.test); + +// 当执行 29 行代码的时候,实际上后台执行了以下的操作: +// var _i = new Number(1); +// _i.test = "Hello"; +// _i.test = null; + +// 如果直接声明的时候就是包装对象类型 +// 那么是可以添加属性方法的,因为是一个对象 +// var i = new Number(1); +// i.test = "Hello"; +// console.log(i.test); + +var i = 1; +Number.prototype.test = "Hello"; +console.log(i.test); \ No newline at end of file diff --git a/03. 包装类型/包装类型.md b/03. 包装类型/包装类型.md new file mode 100644 index 0000000..ee30279 --- /dev/null +++ b/03. 包装类型/包装类型.md @@ -0,0 +1,158 @@ +# 包装类型 + + + +## 经典真题 + + + +- 是否了解 *JavaScript* 中的包装类型? + + + +## 包装类型 + + + +在 *ES* 中,数据的分类分为**基本数据类型**和**引用类型**。 + + + +按照最新 *ES* 标准定义,基本数据类型(*primitive value*)包括 *undefined、null、boolean、number、symbol、string*。 + +引用类型包括 *Object、Array、Date、RegExp* 等。 + +基本数据类型和引用类型这两个类型其中一个很明显的区别是,引用类型有自己内置的方法,也可以自定义其他方法用来操作数据,而基本数据类型不能像引用类型那样有自己的内置方法对数据进行更多的操作。 + + + +但基本数据类型真的就不能使用方法吗?对于部分基本类型来说确实是这样的。 + +但是有 *3* 个是 *ES* 提供了对应的特殊引用类型(包装类型)*Boolean、Number、String*。 + +基本包装类型,和其他引用类型一样,拥有内置的方法可以对数据进行额外操作。如下: + +```js +var str = 'hello'; // string 基本类型 +var s2 = str.charAt(0); +console.log(s2); // h +``` + +上面的 *string* 是一个基本类型,但是它却能调用 *charAt( )* 的方法。 + +其主要是因为在执行第二行代码时,后台会自动进行下面的步骤: + +1. 自动创建 *String* 类型的一个实例(和基本类型的值不同,这个实例就是一个基本包装类型的对象) +2. 调用实例(对象)上指定的方法 +3. 销毁这个实例 + +用代码的方式解释就是如下: + +```js +//我们平常写程序的过程: +var str = 'hello'; // string 基本类型 +var s2 = str.charAt(0); // 在执行到这一句的时候 后台会自动完成以下动作 : +( + var _str = new String('hello'); // 1 找到对应的包装对象类型,然后通过包装对象创建出一个和基本类型值相同的对象 + var s2 = _str.charAt(0); // 2 然后这个对象就可以调用包装对象下的方法,并且返回结给 s2. + _str = null; // 3 之后这个临时创建的对象就被销毁了, str =null; +) +console.log(s2); // h +console.log(str); // hello +``` + + + +基本类型的值虽然没有方法可以调用,但是后台临时创建的包装对象上有内置方法可以让我们调用方法,因此这样我们就可以对字符串、数值、布尔值这三种基本数据类型的数据进行更多操作。 + +而什么时候后台会自动创建一个对应的基本包装类型的对象,取决于当前执行的代码是否是为了获取他的值。 + +每当读取一个基本类型的值,也就是当我们需要从内存中获取到他的值时(这个访问过程称为读取模式),这时后台就会自动创建一个基本包装类型的对象。例如: + +```javascript +var test = 'hhh' +console.log(test) // 读取模式,后台自动创建基本包装类型对象 +var test2 = test // 赋值给变量 test2,也需要读取 test 的值,同上 +``` + +基本包装类型的对象和引用类型的对象最大的一个区别是,对象的生存期不同,导致的一个结果就是,基本包装类型无法自定义自己的方法。 + +对于引用类型的数据,在执行流离开当前作用域之前都会保存在内存中,而对于自动创建的基本包装类型的对象,只存在于一行代码的执行瞬间,执行完毕就会立即被销毁。 +如下: + +```javascript +var str = 'test' +str.test = 'hhh' +console.log(str.test) //undefined +``` + +上面第二行代码给自动创建的 *String* 实例对象添加了 *test* 属性,虽然此刻代码执行时他是生效的,但是在这行代码执行完毕后该 *String* 实例就会立刻被销毁,*String* 实例的 *test* 属性也就不存在了。 + +当执行第三行代码时,由于是读取模式,又重新创建了新的 *String* 实例,而这个新创建的 *String* 实例没有 *test* 属性,结果也就是 *undefined*。 + + + +用代码的方式解释就是如下: + +```js +var str = 'hello'; +str.number = 10; //假设我们想给字符串添加一个属性 number ,后台会有如下步骤 +( + var _str = new String('hello'); // 1 找到对应的包装对象类型,然后通过包装对象创建出一个和基本类型值相同的对象 + _str.number = 10; // 2 通过这个对象调用包装对象下的方法 但结果并没有被任何东西保存 + _str =null; // 3 这个对象又被销毁 +) +console.log(str.number); // undefined 当执行到这一句的时候,因为基本类型本来没有属性,后台又会重新重复上面的步骤 +( + var str = new String('hello');// 1 找到基本包装对象,然后又新开辟一个内存,创建一个值为 hello 对象 + str.number = undefined;// 2 因为包装对象下面没有 number 这个属性,所以又会重新添加,因为没有值,所以值是未定义;然后弹出结果 + str =null; // 3 这个对象又被销毁 +) +``` + + + +那么我们怎么才能给基本类型添加方法或者属性呢? + +答案是在基本包装对象的原型下面添加,每个对象都有原型。 + +```js +//给字符串添加方法 要写到对应的包装对象的原型下才行 +var str = 'hello'; +String.prototype.last= fuction(){ + return this.charAt(this.length); +}; +str.last(); // 5 执行到这一句,后台依然会偷偷的干这些事 +( + var _str = new String('hello');// 找到基本包装对象,new一个和字符串值相同的对象, + _str.last(); // 通过这个对象找到了包装对象下的方法并调用 + _str =null; // 这个对象被销毁 +) +``` + + + +## 真题解答 + + + +- 是否了解 *JavaScript* 中的包装类型? + +> 参考答案: +> +> 包装对象,就是当基本类型以对象的方式去使用时,*JavaScript* 会转换成对应的包装类型,相当于 *new* 一个对象,内容和基本类型的内容一样,然后当操作完成再去访问的时候,这个临时对象会被销毁,然后再访问时候就是 *undefined*。 +> +> *number、string、boolean* 都有对应的包装类型。 +> +> 因为有了基本包装类型,所以 *JavaScript* 中的基本类型值可以被当作对象来访问。 +> +> 基本类型特征: +> +> 1. 每个包装类型都映射到同名的基本类型 +> 2. 在读取模式下访问基本类型值时,就会创建对应的基本包装类型的一个对象,从而方便了数据操作 +> 3. 操作基本类型值的语句一经执行完毕,就会立即销毁新创建的包装对象 + + + +-*EOF*- + diff --git a/04. 数据类型的转换/数据类型的转换.js b/04. 数据类型的转换/数据类型的转换.js new file mode 100644 index 0000000..497e7dd --- /dev/null +++ b/04. 数据类型的转换/数据类型的转换.js @@ -0,0 +1,149 @@ +// 1. js 是动态语言,变量可以是任意类型 +// var i = 1; +// i = "xiejie"; +// console.log(i); + +// 2. 在 js 中存在数据类型的转换 +// var result = '4' - '3'; +// console.log(result, typeof result); + + +// 3. 强制转换 + +// 3-1 Number() 可以将任意类型的值都转为 number 类型 +// (1)简单值 +// console.log(Number('')); // 0 +// console.log(Number('123')); // 123 +// console.log(Number('xiejie')); // NaN +// console.log(Number('123?')); // NaN +// // Number() 和 parseInt 就不一样 +// // parseInt 是会尽可能的去多转换 +// console.log('parseInt:::',parseInt('123xiejie')); // NaN +// console.log('parseInt:::',parseInt('xiejie123')); // NaN +// console.log(Number(true)); // 1 +// console.log(Number(false)); // 0 + +// console.log(Number(undefined)); // NaN +// console.log(Number(null)); // 0 +// console.log("parseInt::",parseInt(undefined)); // NaN +// console.log("parseInt::",parseInt(null)); // NaN + +// (2) 对象的情况 +// 第一步 valueOf 能得到简单值,调用 Number(),如果是对象,进入第二步 +// 第二步 toString 能得到简单值,调用 Number(),如果仍然是对象,进入第三步 +// 第三步 如果还是对象,就报错 + +// valueOf 和 toString 方法是任何对象或者值都有的,因为这两个方法是挂在 Object.prototype 上面的 +// var obj= { +// name : 'xiejie' +// }; +// console.log(obj.valueOf()); +// console.log(obj.toString()); + +// console.log(Number(obj)); + +// 1. valueOf ----> { name: 'xiejie' } +// 2. toString ----> [object Object] ----> Number('[object Object]') +// 3. 最终得到 NaN + +// var arr = [1,2,3]; +// console.log(arr.valueOf()); +// console.log(arr.toString()); +// console.log(Number(arr)); + +// var arr2 = [5]; +// console.log(arr2.valueOf()); +// console.log(arr2.toString()); + +// 关于 valueOf 和 toString 其实是可以自己定义的 +// console.log(Number({ +// valueOf:function(){ +// return 2; +// } +// })); + +// var obj = { +// toString:function(){ +// return 5; +// } +// } +// console.log(obj.valueOf()); + +// console.log(Number({ +// toString:function(){ +// return 5; +// } +// })); + +// console.log(Number({ +// valueOf: function () { +// return 2; +// }, +// toString: function () { +// return 5; +// } +// })); + +// console.log(Number({ +// valueOf: function () { +// return {}; +// }, +// toString: function () { +// return {}; +// } +// })); + +// 3-2 String() 转换对象 + +// 1. toString() ---> 原始类型值 ---> String() +// 2. valueOf() ---> 原始类型的值 ----> String() +// 3. 报错 + +// var obj = {a : 1}; +// console.log(obj.toString()); +// console.log(obj.valueOf()); +// console.log(String(obj)); // [object Object] + +// var obj = { +// a : 1, +// toString(){ +// return {} +// }, +// } +// console.log(String(obj)); + +// 3-3 Boolean + +// console.log(Boolean('')); +// console.log(Boolean("")); +// console.log(Boolean(``)); + +// 上面介绍的是强制转换,也就是我们开发人员手动进行转换 +// 接下来要介绍的是自动转换,程序内部自动发生 + +// console.log('4' - '3'); +// 这里等价于 Boolean('abc') +// if('abc'){ +// console.log('Hello'); +// } + +// 快速转换为布尔值 +// console.log(!!'abc'); // true + + +// console.log('5' + {}); // 5[object Object] +// 这里在做字符串的加法,那就变成了字符串的拼接 +// 左边是字符串 OK +// 右边不是字符串,那么就需要调用 String() 转为字符串,所以 {} 就转为了 [object Object] +// 最后两个字符串拼接起来 + +console.log('5' * []); + +// 这里在进行乘法运行,所以两边要转为 number 类型 + +// 左边:Number('5') ---> 5 +// 右边:因为 [] 的 valueOf 已经得到 0 了,所以最后转换出来就是 0 +console.log([].valueOf()); // 0 +console.log([].toString()); // [] + +console.log(+true); \ No newline at end of file diff --git a/04. 数据类型的转换/数据类型的转换.md b/04. 数据类型的转换/数据类型的转换.md new file mode 100644 index 0000000..bfda436 --- /dev/null +++ b/04. 数据类型的转换/数据类型的转换.md @@ -0,0 +1,530 @@ +# 数据类型的转换 + + + +## 经典真题 + + + +- *JavaScript* 中如何进行数据类型的转换? + + + +## 数据类型转换介绍 + + + +*JavaScript* 是一种动态类型语言,变量没有类型限制,可以随时赋予任意值。 + +```js +var x = y ? 1 : 'a'; +``` + +上面代码中,变量`x`到底是数值还是字符串,取决于另一个变量`y`的值。`y`为`true`时,`x`是一个数值;`y`为`false`时,`x`是一个字符串。这意味着,`x`的类型没法在编译阶段就知道,必须等到运行时才能知道。 + +虽然变量的数据类型是不确定的,但是各种运算符对数据类型是有要求的。如果运算符发现,运算子的类型与预期不符,就会自动转换类型。比如,减法运算符预期左右两侧的运算子应该是数值,如果不是,就会自动将它们转为数值。 + +```javascript +'4' - '3' // 1 +``` + +上面代码中,虽然是两个字符串相减,但是依然得到数值 `1`,原因就在于 *JavaScript* 将运算子自动转为了数值。 + + + +所以接下来我们就来看一下 *JavaScript* 中如何进行数据类型转换。 + + + +## 强制转换(显式转换) + + + +强制转换主要指使用`Number()`、`String()`和`Boolean()`三个函数,手动将各种类型的值,分别转换成数字、字符串或者布尔值。 + + + +#### *Number( )* + + + +使用`Number`函数,可以将任意类型的值转化成数值。 + +下面分成两种情况讨论,一种是参数是原始类型的值,另一种是参数是对象。 + + + +**(1)原始类型值** + +原始类型值的转换规则如下。 + +```js +// 数值:转换后还是原来的值 +Number(324) // 324 + +// 字符串:如果可以被解析为数值,则转换为相应的数值 +Number('324') // 324 + +// 字符串:如果不可以被解析为数值,返回 NaN +Number('324abc') // NaN + +// 空字符串转为0 +Number('') // 0 + +// 布尔值:true 转成 1,false 转成 0 +Number(true) // 1 +Number(false) // 0 + +// undefined:转成 NaN +Number(undefined) // NaN + +// null:转成0 +Number(null) // 0 +``` + +`Number`函数将字符串转为数值,要比`parseInt`函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为`NaN`。 + +```js +parseInt('42 cats') // 42 +Number('42 cats') // NaN +``` + +上面代码中,`parseInt`逐个解析字符,而`Number`函数整体转换字符串的类型。 + +另外,`parseInt`和`Number`函数都会自动过滤一个字符串前导和后缀的空格。 + +```js +parseInt('\t\v\r12.34\n') // 12 +Number('\t\v\r12.34\n') // 12.34 +``` + + + +**(2)对象** + +简单的规则是,`Number`方法的参数是对象时,将返回`NaN`,除非是包含单个数值的数组。 + +```js +Number({a: 1}) // NaN +Number([1, 2, 3]) // NaN +Number([5]) // 5 +``` + +之所以会这样,是因为`Number`背后的转换规则比较复杂。 + +第一步,调用对象自身的`valueOf`方法。如果返回原始类型的值,则直接对该值使用`Number`函数,不再进行后续步骤。 + +第二步,如果`valueOf`方法返回的还是对象,则改为调用对象自身的`toString`方法。如果`toString`方法返回原始类型的值,则对该值使用`Number`函数,不再进行后续步骤。 + +第三步,如果`toString`方法返回的是对象,就报错。 + +请看下面的例子。 + +```js +var obj = {x: 1}; +Number(obj) // NaN + +// 等同于 +if (typeof obj.valueOf() === 'object') { + Number(obj.toString()); +} else { + Number(obj.valueOf()); +} +``` + +上面代码中,`Number`函数将`obj`对象转为数值。背后发生了一连串的操作,首先调用`obj.valueOf`方法, 结果返回对象本身;于是,继续调用`obj.toString`方法,这时返回字符串`[object Object]`,对这个字符串使用`Number`函数,得到`NaN`。 + +默认情况下,对象的`valueOf`方法返回对象本身,所以一般总是会调用`toString`方法,而`toString`方法返回对象的类型字符串(比如`[object Object]`)。所以,会有下面的结果。 + +```js +Number({}) // NaN +``` + +如果`toString`方法返回的不是原始类型的值,结果就会报错。 + +```js +var obj = { + valueOf: function () { + return {}; + }, + toString: function () { + return {}; + } +}; + +Number(obj) +// TypeError: Cannot convert object to primitive value +``` + +上面代码的`valueOf`和`toString`方法,返回的都是对象,所以转成数值时会报错。 + +从上例还可以看到,`valueOf`和`toString`方法,都是可以自定义的。 + +```js +Number({ + valueOf: function () { + return 2; + } +}) +// 2 + +Number({ + toString: function () { + return 3; + } +}) +// 3 + +Number({ + valueOf: function () { + return 2; + }, + toString: function () { + return 3; + } +}) +// 2 +``` + +上面代码对三个对象使用`Number`函数。第一个对象返回`valueOf`方法的值,第二个对象返回`toString`方法的值,第三个对象表示`valueOf`方法先于`toString`方法执行。 + + + +#### *String( )* + + + +`String`函数可以将任意类型的值转化成字符串,转换规则如下。 + + + +**(1)原始类型值** + +- **数值**:转为相应的字符串。 +- **字符串**:转换后还是原来的值。 +- **布尔值**:`true`转为字符串`"true"`,`false`转为字符串`"false"`。 +- **undefined**:转为字符串`"undefined"`。 +- **null**:转为字符串`"null"`。 + +```js +String(123) // "123" +String('abc') // "abc" +String(true) // "true" +String(undefined) // "undefined" +String(null) // "null" +``` + + + +**(2)对象** + +`String`方法的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式。 + +```js +String({a: 1}) // "[object Object]" +String([1, 2, 3]) // "1,2,3" +``` + +`String`方法背后的转换规则,与`Number`方法基本相同,只是互换了`valueOf`方法和`toString`方法的执行顺序。 + +1. 先调用对象自身的`toString`方法。如果返回原始类型的值,则对该值使用`String`函数,不再进行以下步骤。 +2. 如果`toString`方法返回的是对象,再调用原对象的`valueOf`方法。如果`valueOf`方法返回原始类型的值,则对该值使用`String`函数,不再进行以下步骤。 +3. 如果`valueOf`方法返回的是对象,就报错。 + +下面是一个例子。 + +```js +String({a: 1}) +// "[object Object]" + +// 等同于 +String({a: 1}.toString()) +// "[object Object]" +``` + +上面代码先调用对象的`toString`方法,发现返回的是字符串`[object Object]`,就不再调用`valueOf`方法了。 + +如果`toString`法和`valueOf`方法,返回的都是对象,就会报错。 + +```js +var obj = { + valueOf: function () { + return {}; + }, + toString: function () { + return {}; + } +}; + +String(obj) +// TypeError: Cannot convert object to primitive value +``` + +下面是通过自定义`toString`方法,改变返回值的例子。 + +```js +String({ + toString: function () { + return 3; + } +}) +// "3" + +String({ + valueOf: function () { + return 2; + } +}) +// "[object Object]" + +String({ + valueOf: function () { + return 2; + }, + toString: function () { + return 3; + } +}) +// "3" +``` + +上面代码对三个对象使用`String`函数。第一个对象返回`toString`方法的值(数值3),第二个对象返回的还是`toString`方法的值(`[object Object]`),第三个对象表示`toString`方法先于`valueOf`方法执行。 + + + +#### *Boolean( )* + + + +`Boolean()`函数可以将任意类型的值转为布尔值。 + +它的转换规则相对简单:除了以下五个值的转换结果为`false`,其他的值全部为`true`。 + +- `undefined` +- `null` +- `0`(包含`-0`和`+0`) +- `NaN` +- `''`(空字符串) + +```js +Boolean(undefined) // false +Boolean(null) // false +Boolean(0) // false +Boolean(NaN) // false +Boolean('') // false +``` + +当然,`true`和`false`这两个布尔值不会发生变化。 + +```js +Boolean(true) // true +Boolean(false) // false +``` + +注意,所有对象(包括空对象)的转换结果都是`true`,甚至连`false`对应的布尔对象`new Boolean(false)`也是`true`(详见《原始类型值的包装对象》一章)。 + +```js +Boolean({}) // true +Boolean([]) // true +Boolean(new Boolean(false)) // true +``` + +所有对象的布尔值都是`true`,这是因为 JavaScript 语言设计的时候,出于性能的考虑,如果对象需要计算才能得到布尔值,对于`obj1 && obj2`这样的场景,可能会需要较多的计算。为了保证性能,就统一规定,对象的布尔值为`true`。 + + + +## 自动转换(隐式转换) + + + +下面介绍自动转换,它是以强制转换为基础的。 + +遇到以下三种情况时,JavaScript 会自动转换数据类型,即转换是自动完成的,用户不可见。 + +第一种情况,不同类型的数据互相运算。 + +```javascript +123 + 'abc' // "123abc" +``` + +第二种情况,对非布尔值类型的数据求布尔值。 + +```javascript +if ('abc') { + console.log('hello') +} // "hello" +``` + +第三种情况,对非数值类型的值使用一元运算符(即`+`和`-`)。 + +```javascript ++ {foo: 'bar'} // NaN +- [1, 2, 3] // NaN +``` + +自动转换的规则是这样的:预期什么类型的值,就调用该类型的转换函数。比如,某个位置预期为字符串,就调用`String()`函数进行转换。如果该位置既可以是字符串,也可能是数值,那么默认转为数值。 + +由于自动转换具有不确定性,而且不易除错,建议在预期为布尔值、数值、字符串的地方,全部使用`Boolean()`、`Number()`和`String()`函数进行显式转换。 + + + +#### 自动转换为布尔值 + + + +JavaScript 遇到预期为布尔值的地方(比如`if`语句的条件部分),就会将非布尔值的参数自动转换为布尔值。系统内部会自动调用`Boolean()`函数。 + +因此除了以下五个值,其他都是自动转为`true`。 + +- `undefined` +- `null` +- `+0`或`-0` +- `NaN` +- `''`(空字符串) + +下面这个例子中,条件部分的每个值都相当于`false`,使用否定运算符后,就变成了`true`。 + +```javascript +if ( !undefined + && !null + && !0 + && !NaN + && !'' +) { + console.log('true'); +} // true +``` + +下面两种写法,有时也用于将一个表达式转为布尔值。它们内部调用的也是`Boolean()`函数。 + +```javascript +// 写法一 +expression ? true : false + +// 写法二 +!! expression +``` + + + +#### 自动转换为字符串 + + + +JavaScript 遇到预期为字符串的地方,就会将非字符串的值自动转为字符串。具体规则是,先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串。 + +字符串的自动转换,主要发生在字符串的加法运算时。当一个值为字符串,另一个值为非字符串,则后者转为字符串。 + +```javascript +'5' + 1 // '51' +'5' + true // "5true" +'5' + false // "5false" +'5' + {} // "5[object Object]" +'5' + [] // "5" +'5' + function (){} // "5function (){}" +'5' + undefined // "5undefined" +'5' + null // "5null" +``` + +这种自动转换很容易出错。 + +```javascript +var obj = { + width: '100' +}; + +obj.width + 20 // "10020" +``` + +上面代码中,开发者可能期望返回`120`,但是由于自动转换,实际上返回了一个字符`10020`。 + + + +#### 自动转换为数值 + + + +JavaScript 遇到预期为数值的地方,就会将参数值自动转换为数值。系统内部会自动调用`Number()`函数。 + +除了加法运算符(`+`)有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值。 + +```javascript +'5' - '2' // 3 +'5' * '2' // 10 +true - 1 // 0 +false - 1 // -1 +'1' - 1 // 0 +'5' * [] // 0 +false / '5' // 0 +'abc' - 1 // NaN +null + 1 // 1 +undefined + 1 // NaN +``` + +上面代码中,运算符两侧的运算子,都被转成了数值。 + +> 注意:`null`转为数值时为`0`,而`undefined`转为数值时为`NaN`。 + +一元运算符也会把运算子转成数值。 + +```javascript ++'abc' // NaN +-'abc' // NaN ++true // 1 +-false // 0 +``` + + + +## 真题解答 + + + +- *JavaScript* 中如何进行数据类型的转换? + +> 参考答案: +> +> 类型转换可以分为两种,**隐性转换**和**显性转换**。 +> +> **1. 隐性转换** +> +> 当不同数据类型之间进行相互运算,或者当对非布尔类型的数据求布尔值的时候,会发生隐性转换。 +> +> 预期为数字的时候:算术运算的时候,我们的结果和运算的数都是数字,数据会转换为数字来进行计算。 +> +> | 类型 | 转换前 | 转换后 | +> | --------- | --------- | ------ | +> | number | 4 | 4 | +> | string | "1" | 1 | +> | string | "abc" | NaN | +> | string | "" | 0 | +> | boolean | true | 1 | +> | boolean | false | 0 | +> | undefined | undefined | NaN | +> | null | null | 0 | +> +> 预期为字符串的时候:如果有一个操作数为字符串时,使用`+`符号做相加运算时,会自动转换为字符串。 +> +> 预期为布尔的时候:前面在介绍布尔类型时所提到的 9 个值会转为 false,其余转为 true +> +> **2. 显性转换** +> +> 所谓显性转换,就是只程序员强制将一种类型转换为另外一种类型。显性转换往往会使用到一些转换方法。常见的转换方法如下: +> +> - 转换为数值类型:`Number()`,`parseInt()`,`parseFloat()` +> +> - 转换为布尔类型:`Boolean()` +> +> - 转换为字符串类型:`toString()`,`String()` +> +> 当然,除了使用上面的转换方法,我们也可以通过一些快捷方式来进行数据类型的显性转换,如下: +> +> - 转换字符串:直接和一个空字符串拼接,例如:`a = "" + 数据` +> +> - 转换布尔:!!数据类型,例如:`!!"Hello"` +> +> - 转换数值:数据*1 或 /1,例如:`"Hello * 1"` + + + +-*EOF*- \ No newline at end of file diff --git a/05. 运算符/运算符.js b/05. 运算符/运算符.js new file mode 100644 index 0000000..ee451c3 --- /dev/null +++ b/05. 运算符/运算符.js @@ -0,0 +1,181 @@ +// 加法运算符 + +// console.log(1 + 2); // 3 + +// 非数值(非字符串)会被转换为数值 +// console.log(true + true); // 2 + +// 如果有一个操作数是字符串,那么就是进行字符串的拼接 +// console.log('3' + 4 + 5); // '345' + +// 如果是对象,会先将对象转为数值类型(Number) +// 对象转 Number 会先调用 valueOf,如果得到的还是对象 +// 那么就会调用 toString,如果 toString 都还是对象,那就报错 +// console.log([] + 1); // '1' + +// var obj = { +// name : "xiejie", +// valueOf(){ +// return 2; +// }, +// toString(){ +// return 3; +// } +// } + +// console.log(obj + 1); // '[object Object]1' + +// console.log([].valueOf()); +// console.log([].toString()); + +// console.log({}.valueOf()); +// console.log({}.toString()); + +// var d = new Date(); +// d.toString = function(){ +// return 2; +// } +// d.valueOf = function(){ +// return 3; +// } +// console.log(d + 1); + +// 四则运算中的 -、*、/ 都是转成数值进行运算即可 +// 如果无法转为数值,那么最终得到的就是 NaN +// console.log('5' - '2'); +// console.log('5' - true); +// console.log({} - '2'); + +// 余数 +// console.log(5 % 2); + +// 在做取余运算时,小数对大数取余,直接得到这个小数 +// console.log(3 % 100); +// console.log(1 % -2); + +// 自增自减 +// 需要注意的就是运算符在前和在后的区别 +// 在前:先做自增或者自减,然后再参与运算 +// 在后:先做运算,然后再自增或者自减 + +// var i = 1; +// ++i; +// var j = i + 5; +// console.log(i); +// console.log(j); + +// 数值运算符 +// console.log(+5); +// console.log(-5); + +// console.log(-true); +// console.log(-[]); +// console.log(-{}); +// 之所以 [] 转出来是 0,是因为空数组在转为字符串的时候得到的是空字符串 +// 空字符串转为数字就是 0 + +// 而 {} 转字符串得到的是 [object Object] + +// console.log(Number([1])); + +// 指数运算符 + +// console.log(Math.pow(2, 3)); +// console.log(2 ** 3); + +// 比较运算符 + +// console.log(5 > 3); + +// NaN 在做比较的时候一定得到的是 false +// NaN 在做计算的时候得到的是 NaN +// console.log(5 > NaN); +// console.log(5 < NaN); +// console.log(5 + NaN); +// console.log(5 - NaN); +// console.log(5 * NaN); +// console.log(5 / NaN); +// console.log(NaN === NaN); + +// 会先将 '3' 转为数值 +// 当遇到不是数值的操作数,会先将其转为数值 +// console.log(5 > '3'); +// console.log(5 > true); +// console.log(5 > []); +// console.log(5 > {}); + +// 如果两边都是字符串,那么这个比较规则就又变化了 +// 比较的是字符的编码大小 + +// 如果是一个字符串,那么就取出字符串的每一个字符来进行比较 +// console.log('a' > 'A'); +// console.log('cat' > 'coyfriend'); +// console.log('cat' > 'cata'); +// console.log('大' > '小'); + +// 严格相等 +// console.log(5 === '5'); + +// 相等运算符 +// console.log(5 == '5'); + + +// console.log(3 === 0b11); + +// 对象在比较的时候,比较的就不是值了 +// 而是比较的地址 +// console.log([] > []); + +// var arr = []; +// var arr2 = arr; +// console.log(arr === arr2); + +// 严格不想等 + +// console.log(5 !== '5'); + +// 相等运算符 +// console.log('123' > 1); + +// console.log(5 != '5'); + +// console.log(Boolean(undefined)); +// console.log(Boolean(null)); +// console.log(Boolean('')); +// console.log(Boolean(0)); +// console.log(Boolean([])); + +// 快速将一个值转为布尔值 +// console.log(!!54); +// console.log(!!undefined); + +// && 运算符 +// 有一个为假就都为假,全部为真最终才会得到真 +// 注意短路现象 + +// console.log(1 && 2 && 0 && 4 && 5); + +// || 运算符 +// 有一个是真值,就为真,如果全部都为假最终就是假 +// console.log(0 || '' || null || null || undefined) + +// 位运算符 +// console.log(~5); + +// console.log(12 & 10); +// console.log(12 | 10); +// console.log(12 ^ 10); + +// console.log(10 >> 3); +// 等价于乘以 2 的 3 次方 + +var a = { + i: 1, + toString() { + return a.i++; + } +} +// Number ----> valueOf ----> toString +if (a == 1 && a == 2 && a == 3) { + console.log('1'); +} \ No newline at end of file diff --git a/05. 运算符/运算符.md b/05. 运算符/运算符.md new file mode 100644 index 0000000..9fb9b30 --- /dev/null +++ b/05. 运算符/运算符.md @@ -0,0 +1,1369 @@ +# 运算符 + + + +## 经典真题 + + + +- 下面代码中,*a* 在什么情况下会执行输出语句打印 *1* ? + +```js +var a = ?; +if(a == 1 && a == 2 && a == 3){ + console.log(1); +} +``` + + + +## 1. 算术运算符 + + + +*JavaScript* 共提供 *10* 个算术运算符,用来完成基本的算术运算。 + +- **加法运算符**:`x + y` +- **减法运算符**: `x - y` +- **乘法运算符**: `x * y` +- **除法运算符**:`x / y` +- **指数运算符**:`x ** y` +- **余数运算符**:`x % y` +- **自增运算符**:`++x` 或者 `x++` +- **自减运算符**:`--x` 或者 `x--` +- **数值运算符**: `+x` +- **负数值运算符**:`-x` + +减法、乘法、除法运算法比较单纯,就是执行相应的数学运算。 + +下面介绍其他几个算术运算符,重点是加法运算符。 + + + +#### 加法运算符 + +**(1)基本规则** + +加法运算符(`+`)是最常见的运算符,用来求两个数值的和。 + +```js +1 + 1 // 2 +``` + +JavaScript 允许非数值的相加。 + +```js +true + true // 2 +1 + true // 2 +``` + +上面代码中,第一行是两个布尔值相加,第二行是数值与布尔值相加。这两种情况,布尔值都会自动转成数值,然后再相加。 + +比较特殊的是,如果是两个字符串相加,这时加法运算符会变成连接运算符,返回一个新的字符串,将两个原字符串连接在一起。 + +```js +'a' + 'bc' // "abc" +``` + +如果一个运算子是字符串,另一个运算子是非字符串,这时非字符串会转成字符串,再连接在一起。 + +```js +1 + 'a' // "1a" +false + 'a' // "falsea" +``` + +加法运算符是在运行时决定,到底是执行相加,还是执行连接。也就是说,运算子的不同,导致了不同的语法行为,这种现象称为“重载”(overload)。由于加法运算符存在重载,可能执行两种运算,使用的时候必须很小心。 + +```js +'3' + 4 + 5 // "345" +3 + 4 + '5' // "75" +``` + +上面代码中,由于从左到右的运算次序,字符串的位置不同会导致不同的结果。 + +除了加法运算符,其他算术运算符(比如减法、除法和乘法)都不会发生重载。它们的规则是:所有运算子一律转为数值,再进行相应的数学运算。 + +```js +1 - '2' // -1 +1 * '2' // 2 +1 / '2' // 0.5 +``` + +上面代码中,减法、除法和乘法运算符,都是将字符串自动转为数值,然后再运算。 + + + +**(2)对象相加** + +如果运算子是对象,必须先转成原始类型的值,然后再相加。 + +```javascript +var obj = { p: 1 }; +obj + 2 // "[object Object]2" +``` + +上面代码中,对象`obj`转成原始类型的值是`[object Object]`,再加`2`就得到了上面的结果。 + +对象转成原始类型的值,规则如下。 + +首先,自动调用对象的`valueOf`方法。 + +```javascript +var obj = { p: 1 }; +obj.valueOf() // { p: 1 } +``` + +一般来说,对象的`valueOf`方法总是返回对象自身,这时再自动调用对象的`toString`方法,将其转为字符串。 + +```javascript +var obj = { p: 1 }; +obj.valueOf().toString() // "[object Object]" +``` + +对象的`toString`方法默认返回`[object Object]`,所以就得到了最前面那个例子的结果。 + +知道了这个规则以后,就可以自己定义`valueOf`方法或`toString`方法,得到想要的结果。 + +```javascript +var obj = { + valueOf: function () { + return 1; + } +}; + +obj + 2 // 3 +``` + +上面代码中,我们定义`obj`对象的`valueOf`方法返回`1`,于是`obj + 2`就得到了`3`。这个例子中,由于`valueOf`方法直接返回一个原始类型的值,所以不再调用`toString`方法。 + +下面是自定义`toString`方法的例子。 + +```javascript +var obj = { + toString: function () { + return 'hello'; + } +}; + +obj + 2 // "hello2" +``` + +上面代码中,对象`obj`的`toString`方法返回字符串`hello`。前面说过,只要有一个运算子是字符串,加法运算符就变成连接运算符,返回连接后的字符串。 + +这里有一个特例,如果运算子是一个`Date`对象的实例,那么会优先执行`toString`方法。 + +```javascript +var obj = new Date(); +obj.valueOf = function () { return 1 }; +obj.toString = function () { return 'hello' }; + +obj + 2 // "hello2" +``` + +上面代码中,对象`obj`是一个`Date`对象的实例,并且自定义了`valueOf`方法和`toString`方法,结果`toString`方法优先执行。 + + + +#### 余数运算符 + +余数运算符(`%`)返回前一个运算子被后一个运算子除,所得的余数。 + +```javascript +12 % 5 // 2 +``` + +需要注意的是,运算结果的正负号由第一个运算子的正负号决定。 + +```javascript +-1 % 2 // -1 +1 % -2 // 1 +``` + +所以,为了得到负数的正确余数值,可以先使用绝对值函数。 + +```javascript +// 错误的写法 +function isOdd(n) { + return n % 2 === 1; +} +isOdd(-5) // false +isOdd(-4) // false + +// 正确的写法 +function isOdd(n) { + return Math.abs(n % 2) === 1; +} +isOdd(-5) // true +isOdd(-4) // false +``` + +余数运算符还可以用于浮点数的运算。但是,由于浮点数不是精确的值,无法得到完全准确的结果。 + +```javascript +6.5 % 2.1 +// 0.19999999999999973 +``` + + + +#### 自增和自减运算符 + +自增和自减运算符,是一元运算符,只需要一个运算子。它们的作用是将运算子首先转为数值,然后加上1或者减去1。它们会修改原始变量。 + +```javascript +var x = 1; +++x // 2 +x // 2 + +--x // 1 +x // 1 +``` + +上面代码的变量`x`自增后,返回`2`,再进行自减,返回`1`。这两种情况都会使得,原始变量`x`的值发生改变。 + +运算之后,变量的值发生变化,这种效应叫做运算的副作用(side effect)。自增和自减运算符是仅有的两个具有副作用的运算符,其他运算符都不会改变变量的值。 + +自增和自减运算符有一个需要注意的地方,就是放在变量之后,会先返回变量操作前的值,再进行自增/自减操作;放在变量之前,会先进行自增/自减操作,再返回变量操作后的值。 + +```javascript +var x = 1; +var y = 1; + +x++ // 1 +++y // 2 +``` + +上面代码中,`x`是先返回当前值,然后自增,所以得到`1`;`y`是先自增,然后返回新的值,所以得到`2`。 + + + +#### 数值运算符,负数值运算符 + +数值运算符(`+`)同样使用加号,但它是一元运算符(只需要一个操作数),而加法运算符是二元运算符(需要两个操作数)。 + +数值运算符的作用在于可以将任何值转为数值(与`Number`函数的作用相同)。 + +```javascript ++true // 1 ++[] // 0 ++{} // NaN +``` + +上面代码表示,非数值经过数值运算符以后,都变成了数值(最后一行`NaN`也是数值)。具体的类型转换规则,参见《数据类型转换》一章。 + +负数值运算符(`-`),也同样具有将一个值转为数值的功能,只不过得到的值正负相反。连用两个负数值运算符,等同于数值运算符。 + +```javascript +var x = 1; +-x // -1 +-(-x) // 1 +``` + +上面代码最后一行的圆括号不可少,否则会变成自减运算符。 + +数值运算符号和负数值运算符,都会返回一个新的值,而不会改变原始变量的值。 + + + +#### 指数运算符 + +指数运算符(`**`)完成指数运算,前一个运算子是底数,后一个运算子是指数。 + +```javascript +2 ** 4 // 16 +``` + +注意,指数运算符是右结合,而不是左结合。即多个指数运算符连用时,先进行最右边的计算。 + +```javascript +// 相当于 2 ** (3 ** 2) +2 ** 3 ** 2 +// 512 +``` + +上面代码中,由于指数运算符是右结合,所以先计算第二个指数运算符,而不是第一个。 + + + +#### 赋值运算符 + +赋值运算符(Assignment Operators)用于给变量赋值。 + +最常见的赋值运算符,当然就是等号(`=`)。 + +```javascript +// 将 1 赋值给变量 x +var x = 1; + +// 将变量 y 的值赋值给变量 x +var x = y; +``` + +赋值运算符还可以与其他运算符结合,形成变体。下面是与算术运算符的结合。 + +```javascript +// 等同于 x = x + y +x += y + +// 等同于 x = x - y +x -= y + +// 等同于 x = x * y +x *= y + +// 等同于 x = x / y +x /= y + +// 等同于 x = x % y +x %= y + +// 等同于 x = x ** y +x **= y +``` + +下面是与位运算符的结合(关于位运算符,请见后文的介绍)。 + +```javascript +// 等同于 x = x >> y +x >>= y + +// 等同于 x = x << y +x <<= y + +// 等同于 x = x >>> y +x >>>= y + +// 等同于 x = x & y +x &= y + +// 等同于 x = x | y +x |= y + +// 等同于 x = x ^ y +x ^= y +``` + +这些复合的赋值运算符,都是先进行指定运算,然后将得到值返回给左边的变量。 + + + +## 2. 比较运算符 + + + +比较运算符用于比较两个值的大小,然后返回一个布尔值,表示是否满足指定的条件。 + +``` +2 > 1 // true +``` + +上面代码比较`2`是否大于`1`,返回`true`。 + +> 注意,比较运算符可以比较各种类型的值,不仅仅是数值。 + +JavaScript 一共提供了8个比较运算符。 + +- `>` 大于运算符 +- `<` 小于运算符 +- `<=` 小于或等于运算符 +- `>=` 大于或等于运算符 +- `==` 相等运算符 +- `===` 严格相等运算符 +- `!=` 不相等运算符 +- `!==` 严格不相等运算符 + +这八个比较运算符分成两类:相等比较和非相等比较。两者的规则是不一样的,对于非相等的比较,算法是先看两个运算子是否都是字符串,如果是的,就按照字典顺序比较(实际上是比较 Unicode 码点);否则,将两个运算子都转成数值,再比较数值的大小。 + + + +#### 非相等运算符:字符串的比较 + +字符串按照字典顺序进行比较。 + +``` +'cat' > 'dog' // false +'cat' > 'catalog' // false +``` + +JavaScript 引擎内部首先比较首字符的 Unicode 码点。如果相等,再比较第二个字符的 Unicode 码点,以此类推。 + +``` +'cat' > 'Cat' // true' +``` + +上面代码中,小写的`c`的 Unicode 码点(`99`)大于大写的`C`的 Unicode 码点(`67`),所以返回`true`。 + +由于所有字符都有 Unicode 码点,因此汉字也可以比较。 + +``` +'大' > '小' // false +``` + +上面代码中,“大”的 Unicode 码点是22823,“小”是23567,因此返回`false`。 + + + +#### 非相等运算符:非字符串的比较 + +如果两个运算子之中,至少有一个不是字符串,需要分成以下两种情况。 + +**(1)原始类型值** + +如果两个运算子都是原始类型的值,则是先转成数值再比较。 + +```javascript +5 > '4' // true +// 等同于 5 > Number('4') +// 即 5 > 4 + +true > false // true +// 等同于 Number(true) > Number(false) +// 即 1 > 0 + +2 > true // true +// 等同于 2 > Number(true) +// 即 2 > 1 +``` + +上面代码中,字符串和布尔值都会先转成数值,再进行比较。 + +这里需要注意与`NaN`的比较。任何值(包括`NaN`本身)与`NaN`使用非相等运算符进行比较,返回的都是`false`。 + +```javascript +1 > NaN // false +1 <= NaN // false +'1' > NaN // false +'1' <= NaN // false +NaN > NaN // false +NaN <= NaN // false +``` + + + +**(2)对象** + +如果运算子是对象,会转为原始类型的值,再进行比较。 + +对象转换成原始类型的值,算法是先调用`valueOf`方法;如果返回的还是对象,再接着调用`toString`方法,详细解释参见《数据类型的转换》一章。 + +```javascript +var x = [2]; +x > '11' // true +// 等同于 [2].valueOf().toString() > '11' +// 即 '2' > '11' + +x.valueOf = function () { return '1' }; +x > '11' // false +// 等同于 [2].valueOf() > '11' +// 即 '1' > '11' +``` + +两个对象之间的比较也是如此。 + +```javascript +[2] > [1] // true +// 等同于 [2].valueOf().toString() > [1].valueOf().toString() +// 即 '2' > '1' + +[2] > [11] // true +// 等同于 [2].valueOf().toString() > [11].valueOf().toString() +// 即 '2' > '11' + +{ x: 2 } >= { x: 1 } // true +// 等同于 { x: 2 }.valueOf().toString() >= { x: 1 }.valueOf().toString() +// 即 '[object Object]' >= '[object Object]' +``` + + + +#### 严格相等运算符 + +*JavaScript* 提供两种相等运算符:`==`和`===`。 + +简单说,它们的区别是相等运算符(`==`)比较两个值是否相等,严格相等运算符(`===`)比较它们是否为“同一个值”。如果两个值不是同一类型,严格相等运算符(`===`)直接返回`false`,而相等运算符(`==`)会将它们转换成同一个类型,再用严格相等运算符进行比较。 + + + +**(1)不同类型的值** + +如果两个值的类型不同,直接返回`false`。 + +```js +1 === "1" // false +true === "true" // false +``` + +上面代码比较数值的`1`与字符串的“1”、布尔值的`true`与字符串`"true"`,因为类型不同,结果都是`false`。 + + + +**(2)同一类的原始类型值** + +同一类型的原始类型的值(数值、字符串、布尔值)比较时,值相同就返回`true`,值不同就返回`false`。 + +```js +1 === 0x1 // true +``` + +上面代码比较十进制的`1`与十六进制的`1`,因为类型和值都相同,返回`true`。 + +需要注意的是,`NaN`与任何值都不相等(包括自身)。另外,正`0`等于负`0`。 + +```js +NaN === NaN // false ++0 === -0 // true +``` + + + +**(3)复合类型值** + +两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个地址。 + +```js +{} === {} // false +[] === [] // false +(function () {} === function () {}) // false +``` + +上面代码分别比较两个空对象、两个空数组、两个空函数,结果都是不相等。原因是对于复合类型的值,严格相等运算比较的是,它们是否引用同一个内存地址,而运算符两边的空对象、空数组、空函数的值,都存放在不同的内存地址,结果当然是`false`。 + +如果两个变量引用同一个对象,则它们相等。 + +```js +var v1 = {}; +var v2 = v1; +v1 === v2 // true +``` + +注意,对于两个对象的比较,严格相等运算符比较的是地址,而大于或小于运算符比较的是值。 + +```js +var obj1 = {}; +var obj2 = {}; + +obj1 > obj2 // false +obj1 < obj2 // false +obj1 === obj2 // false +``` + +上面的三个比较,前两个比较的是值,最后一个比较的是地址,所以都返回`false`。 + + + +**(4)undefined 和 null** + +`undefined`和`null`与自身严格相等。 + +```js +undefined === undefined // true +null === null // true +``` + +由于变量声明后默认值是`undefined`,因此两个只声明未赋值的变量是相等的。 + +```js +var v1; +var v2; +v1 === v2 // true +``` + + + +#### 严格不相等运算符 + +严格相等运算符有一个对应的“严格不相等运算符”(`!==`),它的算法就是先求严格相等运算符的结果,然后返回相反值。 + +```js +1 !== '1' // true +// 等同于 +!(1 === '1') +``` + +上面代码中,感叹号`!`是求出后面表达式的相反值。 + + + +#### 相等运算符 + +相等运算符用来比较相同类型的数据时,与严格相等运算符完全一样。 + +```js +1 == 1.0 +// 等同于 +1 === 1.0 +``` + +比较不同类型的数据时,相等运算符会先将数据进行类型转换,然后再用严格相等运算符比较。下面分成几种情况,讨论不同类型的值互相比较的规则。 + + + +**(1)原始类型值** + +原始类型的值会转换成数值再进行比较。 + +```js +1 == true // true +// 等同于 1 === Number(true) + +0 == false // true +// 等同于 0 === Number(false) + +2 == true // false +// 等同于 2 === Number(true) + +2 == false // false +// 等同于 2 === Number(false) + +'true' == true // false +// 等同于 Number('true') === Number(true) +// 等同于 NaN === 1 + +'' == 0 // true +// 等同于 Number('') === 0 +// 等同于 0 === 0 + +'' == false // true +// 等同于 Number('') === Number(false) +// 等同于 0 === 0 + +'1' == true // true +// 等同于 Number('1') === Number(true) +// 等同于 1 === 1 + +'\n 123 \t' == 123 // true +// 因为字符串转为数字时,省略前置和后置的空格 +``` + +上面代码将字符串和布尔值都转为数值,然后再进行比较。 + + + +**(2)对象与原始类型值比较** + +对象(这里指广义的对象,包括数组和函数)与原始类型的值比较时,对象转换成原始类型的值,再进行比较。 + +具体来说,先调用对象的`valueOf()`方法,如果得到原始类型的值,就按照上一小节的规则,互相比较;如果得到的还是对象,则再调用`toString()`方法,得到字符串形式,再进行比较。 + +下面是数组与原始类型值比较的例子。 + +``` +// 数组与数值的比较 +[1] == 1 // true + +// 数组与字符串的比较 +[1] == '1' // true +[1, 2] == '1,2' // true + +// 对象与布尔值的比较 +[1] == true // true +[2] == true // false +``` + +上面例子中,JavaScript 引擎会先对数组`[1]`调用数组的`valueOf()`方法,由于返回的还是一个数组,所以会接着调用数组的`toString()`方法,得到字符串形式,再按照上一小节的规则进行比较。 + +下面是一个更直接的例子。 + +```js +const obj = { + valueOf: function () { + console.log('执行 valueOf()'); + return obj; + }, + toString: function () { + console.log('执行 toString()'); + return 'foo'; + } +}; + +obj == 'foo' +// 执行 valueOf() +// 执行 toString() +// true +``` + +上面例子中,`obj`是一个自定义了`valueOf()`和`toString()`方法的对象。这个对象与字符串`'foo'`进行比较时,会依次调用`valueOf()`和`toString()`方法,最后返回`'foo'`,所以比较结果是`true`。 + + + +**(3)undefined 和 null** + +`undefined`和`null`只有与自身比较,或者互相比较时,才会返回`true`;与其他类型的值比较时,结果都为`false`。 + +```js +undefined == undefined // true +null == null // true +undefined == null // true + +false == null // false +false == undefined // false + +0 == null // false +0 == undefined // false +``` + + + +**(4)相等运算符的缺点** + +相等运算符隐藏的类型转换,会带来一些违反直觉的结果。 + +```js +0 == '' // true +0 == '0' // true + +2 == true // false +2 == false // false + +false == 'false' // false +false == '0' // true + +false == undefined // false +false == null // false +null == undefined // true + +' \t\r\n ' == 0 // true +``` + +上面这些表达式都不同于直觉,很容易出错。因此建议不要使用相等运算符(`==`),最好只使用严格相等运算符(`===`)。 + + + +#### 不相等运算符 + +相等运算符有一个对应的“不相等运算符”(`!=`),它的算法就是先求相等运算符的结果,然后返回相反值。 + +```js +1 != '1' // false + +// 等同于 +!(1 == '1') +``` + + + +## 3. 布尔运算符(逻辑运算符) + + + +布尔运算符用于将表达式转为布尔值,一共包含四个运算符。 + + + +- 取反运算符:`!` +- 且(并)运算符:`&&` +- 或运算符:`||` +- 三元运算符:`?:` + + + +#### 取反运算符(!) + +取反运算符是一个感叹号,用于将布尔值变为相反值,即`true`变成`false`,`false`变成`true`。 + +```js +!true // false +!false // true +``` + +对于非布尔值,取反运算符会将其转为布尔值。可以这样记忆,以下六个值取反后为`true`,其他值都为`false`。 + +- `undefined` +- `null` +- `false` +- `0` +- `NaN` +- 空字符串(`''`) + +```js +!undefined // true +!null // true +!0 // true +!NaN // true +!"" // true + +!54 // false +!'hello' // false +![] // false +!{} // false +``` + +上面代码中,不管什么类型的值,经过取反运算后,都变成了布尔值。 + +如果对一个值连续做两次取反运算,等于将其转为对应的布尔值,与`Boolean`函数的作用相同。这是一种常用的类型转换的写法。 + +```js +!!x +// 等同于 +Boolean(x) +``` + +上面代码中,不管`x`是什么类型的值,经过两次取反运算后,变成了与`Boolean`函数结果相同的布尔值。所以,两次取反就是将一个值转为布尔值的简便写法。 + + + +#### 且运算符(&&) + +且运算符(`&&`)往往用于多个表达式的求值。 + +它的运算规则是:如果第一个运算子的布尔值为`true`,则返回第二个运算子的值(注意是值,不是布尔值);如果第一个运算子的布尔值为`false`,则直接返回第一个运算子的值,且不再对第二个运算子求值。 + +```js +'t' && '' // "" +'t' && 'f' // "f" +'t' && (1 + 2) // 3 +'' && 'f' // "" +'' && '' // "" + +var x = 1; +(1 - 1) && ( x += 1) // 0 +x // 1 +``` + +上面代码的最后一个例子,由于且运算符的第一个运算子的布尔值为`false`,则直接返回它的值`0`,而不再对第二个运算子求值,所以变量`x`的值没变。 + +这种跳过第二个运算子的机制,被称为“短路”。有些程序员喜欢用它取代`if`结构,比如下面是一段`if`结构的代码,就可以用且运算符改写。 + +```js +if (i) { + doSomething(); +} + +// 等价于 + +i && doSomething(); +``` + +上面代码的两种写法是等价的,但是后一种不容易看出目的,也不容易除错,建议谨慎使用。 + +且运算符可以多个连用,这时返回第一个布尔值为`false`的表达式的值。如果所有表达式的布尔值都为`true`,则返回最后一个表达式的值。 + +```js +true && 'foo' && '' && 4 && 'foo' && true +// '' + +1 && 2 && 3 +// 3 +``` + +上面代码中,例一里面,第一个布尔值为`false`的表达式为第三个表达式,所以得到一个空字符串。例二里面,所有表达式的布尔值都是`true`,所以返回最后一个表达式的值`3`。 + + + +#### 或运算符(||) + +或运算符(`||`)也用于多个表达式的求值。它的运算规则是:如果第一个运算子的布尔值为`true`,则返回第一个运算子的值,且不再对第二个运算子求值;如果第一个运算子的布尔值为`false`,则返回第二个运算子的值。 + +```js +'t' || '' // "t" +'t' || 'f' // "t" +'' || 'f' // "f" +'' || '' // "" +``` + +短路规则对这个运算符也适用。 + +```js +var x = 1; +true || (x = 2) // true +x // 1 +``` + +上面代码中,或运算符的第一个运算子为`true`,所以直接返回`true`,不再运行第二个运算子。所以,`x`的值没有改变。这种只通过第一个表达式的值,控制是否运行第二个表达式的机制,就称为“短路”(short-cut)。 + +或运算符可以多个连用,这时返回第一个布尔值为`true`的表达式的值。如果所有表达式都为`false`,则返回最后一个表达式的值。 + +```js +false || 0 || '' || 4 || 'foo' || true +// 4 + +false || 0 || '' +// '' +``` + +上面代码中,例一里面,第一个布尔值为`true`的表达式是第四个表达式,所以得到数值4。例二里面,所有表达式的布尔值都为`false`,所以返回最后一个表达式的值。 + +或运算符常用于为一个变量设置默认值。 + +```js +function saveText(text) { + text = text || ''; + // ... +} + +// 或者写成 +saveText(this.text || '') +``` + +上面代码表示,如果函数调用时,没有提供参数,则该参数默认设置为空字符串。 + + + +#### 三元条件运算符(?:) + +三元条件运算符由问号(?)和冒号(:)组成,分隔三个表达式。它是 JavaScript 语言唯一一个需要三个运算子的运算符。如果第一个表达式的布尔值为`true`,则返回第二个表达式的值,否则返回第三个表达式的值。 + +```js +'t' ? 'hello' : 'world' // "hello" +0 ? 'hello' : 'world' // "world" +``` + +上面代码的`t`和`0`的布尔值分别为`true`和`false`,所以分别返回第二个和第三个表达式的值。 + +通常来说,三元条件表达式与`if...else`语句具有同样表达效果,前者可以表达的,后者也能表达。但是两者具有一个重大差别,`if...else`是语句,没有返回值;三元条件表达式是表达式,具有返回值。所以,在需要返回值的场合,只能使用三元条件表达式,而不能使用`if..else`。 + +```js +console.log(true ? 'T' : 'F'); +``` + +上面代码中,`console.log`方法的参数必须是一个表达式,这时就只能使用三元条件表达式。如果要用`if...else`语句,就必须改变整个代码写法了。 + + + +## 4. 位运算符 + + + +按位运算符是将操作数换算成 *32* 位的二进制整数,然后按每一位来进行运算。例如: + +*5* 的 *32* 位为: + +``` +00000000000000000000000000000101 +``` + +*100* 的 *32* 位为: + +``` +00000000000000000000000001100100 +``` + +*15* 的 *32* 位为: + +``` +00000000000000000000000000001111 +``` + + + +#### 按位非 + +按位非运算符`~`会把数字转为32位二进制整数,然后反转每一位。所有的 1 变为 0,所有的 0 变为 1 + +例如: + +5 的 32 位为: + +``` +00000000000000000000000000000101 +``` + +~5 的 32 位为: + +``` +11111111111111111111111111111010 +``` + +转换出来就为 -6 + +按位非,实质上是对操作数求负,然后减去1。 + + + +#### 按位与 + +按位或运算符`&`会把两个数字转为 32 位二进制整数,并对两个数的每一位执行按位与运算。按位与的规则如下表: + +| 第一个数字 | 第二个数字 | 结果 | +| ---------- | ---------- | ---- | +| 1 | 1 | 1 | +| 1 | 0 | 0 | +| 0 | 1 | 0 | +| 0 | 0 | 0 | + +具体示例: + +```js +console.log(12 & 10); // 8 +``` + +12 的 32 位二进制表示为:1100 +10 的 32 位二进制表示为:1010 + +按位与的结果为:1000 + + + +#### 按位或 + +按位或运算符`|`会把两个数字转为 32 位二进制整数,并对两个数的每一位执行按位或运算。按位或的规则如下表: + +| 第一个数字 | 第二个数字 | 结果 | +| ---------- | ---------- | ---- | +| 1 | 1 | 1 | +| 1 | 0 | 1 | +| 0 | 1 | 1 | +| 0 | 0 | 0 | + +具体示例: + +```js +console.log(12 | 10); // 14 +``` + +12 的 32 位二进制表示为:1100 +10 的 32 位二进制表示为:1010 + +按位或的结果为:1110 + + + +#### 按位异或 + +按位或运算符`^`会把两个数字转为 32 位二进制整数,并对两个数的每一位执行按位异或运算。运算规则为两位不同返回 1,两位相同返回 0,如下表: + +| 第一个数字 | 第二个数字 | 结果 | +| ---------- | ---------- | ---- | +| 1 | 1 | 0 | +| 1 | 0 | 1 | +| 0 | 1 | 1 | +| 0 | 0 | 0 | + +具体示例: + +```js +console.log(12 ^ 10); // 6 +``` + +12 的 32 位二进制表示为:1100 +10 的 32 位二进制表示为:1010 + +按位异或的结果为:0110 + +按位异或如果是非整数值,如果两个操作数中只有一个为真,就返回 1,如果两个操作数都是真,或者都是假,就返回 0,示例如下: + +```js +console.log(true ^ "Hello"); // 1 +console.log(false ^ "Hello"); // 0 +console.log(true ^ true); // 0 +console.log("Hello" ^ "Hello"); // 0 +console.log(false ^ false); // 0 +console.log(true ^ false); // 1 +``` + +注意这里的 Hello 被转换为了 NaN + + + +#### 按位移位 + +按位移位运算符`<<`和`>>`会将所有位向左或者向右移动指定的数量,实际上就是高效率地将数字乘以或者除以 2 的指定数的次方。 + +`<<`:乘以 2 的指定数次方 + +```js +console.log(2<<2); // 8 +``` + +2 乘以 2 的 2 次方 + +00000010 转换为 00001000 + +`>>`:除以 2 的指定数次方 + +```js +console.log(16>>1); // 8 +``` + +16 除以 2 的 1 次方 + +00010000转换为00001000 + + + +## 5. 其他运算符 + + + +#### *void* 运算符 + +*void* 运算符的作用是执行一个表达式,然后不返回任何值,或者说返回 *undefined*。 + +```js +void 0 // undefined +void(0) // undefined +``` + +上面是 *void* 运算符的两种写法,都正确。建议采用后一种形式,即总是使用圆括号。 + +因为 *void* 运算符的优先性很高,如果不使用括号,容易造成错误的结果。 + +比如,*“void 4 + 7”* 实际上等同于 *“(void 4) + 7”*。 + +下面是 *void* 运算符的一个例子。 + +```js +var x = 3; +void (x = 5) //undefined +x // 5 +``` + +这个运算符的主要用途是浏览器的书签工具(*Bookmarklet*),以及在超级链接中插入代码防止网页跳转。 + +请看下面的代码。 + +```js + +点击 +``` + +上面代码中,点击链接后,会先执行 *onclick* 的代码,由于 *onclick* 返回 *false*,所以浏览器不会跳转到 *example.com*。 + +*void* 运算符可以取代上面的写法。 + +```js +文字 +``` + +下面是一个更实际的例子,用户点击链接提交表单,但是不产生页面跳转。 + +```js + + 提交 + +``` + + + +#### 逗号运算符 + +逗号运算符用于对两个表达式求值,并返回后一个表达式的值。 + +```js +'a', 'b' // "b" + +var x = 0; +var y = (x++, 10); +x // 1 +y // 10 +``` + +上面代码中,逗号运算符返回后一个表达式的值。 + +逗号运算符的一个用途是,在返回一个值之前,进行一些辅助操作。 + +```javascript +var value = (console.log('Hi!'), true); +// Hi! + +value // true +``` + +上面代码中,先执行逗号之前的操作,然后返回逗号后面的值。 + + + +## 6. 运算顺序 + + + +#### 优先级 + +*JavaScript* 各种运算符的优先级别(*Operator Precedence*)是不一样的。优先级高的运算符先执行,优先级低的运算符后执行。 + +```js +4 + 5 * 6 // 34 +``` + +上面的代码中,乘法运算符( * )的优先性高于加法运算符( + ),所以先执行乘法,再执行加法,相当于下面这样。 + +```js +4 + (5 * 6) // 34 +``` + +如果多个运算符混写在一起,常常会导致令人困惑的代码。 + +```js +var x = 1; +var arr = []; + +var y = arr.length <= 0 || arr[0] === undefined ? x : arr[0]; +``` + +上面代码中,变量 *y* 的值就很难看出来,因为这个表达式涉及 *5* 个运算符,到底谁的优先级最高,实在不容易记住。 + +根据语言规格,这五个运算符的优先级从高到低依次为:小于等于( <= )、严格相等( === )、或( || )、三元( ?: )、等号( = )。因此上面的表达式,实际的运算顺序如下。 + +```js +var y = ((arr.length <= 0) || (arr[0] === undefined)) ? x : arr[0]; +``` + +记住所有运算符的优先级,是非常难的,也是没有必要的。 + + + +#### 圆括号的作用 + +圆括号可以用来提高运算的优先级,因为它的优先级是最高的,即圆括号中的表达式会第一个运算。 + +```js +(4 + 5) * 6 // 54 +``` + +上面代码中,由于使用了圆括号,加法会先于乘法执行。 + +运算符的优先级别十分繁杂,且都是硬性规定,因此建议总是使用圆括号,保证运算顺序清晰可读,这对代码的维护和除错至关重要。 + +顺便说一下,圆括号不是运算符,而是一种语法结构。它一共有两种用法:一种是把表达式放在圆括号之中,提升运算的优先级;另一种是跟在函数的后面,作用是调用函数。 + +注意,因为圆括号不是运算符,所以不具有求值作用,只改变运算的优先级。 + +```js +var x = 1; +(x) = 2; +``` + +上面代码的第二行,如果圆括号具有求值作用,那么就会变成 *1 = 2*,这是会报错了。但是,上面的代码可以运行,这验证了圆括号只改变优先级,不会求值。 + +这也意味着,如果整个表达式都放在圆括号之中,那么不会有任何效果。 + +```js +(expression) +// 等同于 +expression +``` + +函数放在圆括号中,会返回函数本身。如果圆括号紧跟在函数的后面,就表示调用函数。 + +```js +function f() { + return 1; +} + +(f) // function f(){return 1;} +f() // 1 +``` + +上面代码中,函数放在圆括号之中会返回函数本身,圆括号跟在函数后面则是调用函数。 + +圆括号之中,只能放置表达式,如果将语句放在圆括号之中,就会报错。 + +```javascript +(var a = 1) +// SyntaxError: Unexpected token var +``` + + + +#### 左结合和右结合 + +对于优先级别相同的运算符,同时出现的时候,就会有计算顺序的问题。 + +```javascript +a OP b OP c +``` + +上面代码中,*OP* 表示运算符。它可以有两种解释方式。 + +```javascript +// 方式一 +(a OP b) OP c + +// 方式二 +a OP (b OP c) +``` + +上面的两种方式,得到的计算结果往往是不一样的。 + +方式一是将左侧两个运算数结合在一起,采用这种解释方式的运算符,称为“左结合”(*left-to-right associativity*)运算符; + +方式二是将右侧两个运算数结合在一起,这样的运算符称为“右结合”运算符(*right-to-left associativity*)。 + +*JavaScript* 语言的大多数运算符是“左结合”,请看下面加法运算符的例子。 + +```javascript +x + y + z + +// 引擎解释如下 +(x + y) + z +``` + +上面代码中,*x* 与 *y* 结合在一起,它们的预算结果再与 *z* 进行运算。 + +少数运算符是“右结合”,其中最主要的是赋值运算符( = )和三元条件运算符( ?: )。 + +```javascript +w = x = y = z; +q = a ? b : c ? d : e ? f : g; +``` + +上面代码的解释方式如下。 + +```javascript +w = (x = (y = z)); +q = a ? b : (c ? d : (e ? f : g)); +``` + +上面的两行代码,都是右侧的运算数结合在一起。 + +另外,指数运算符(\**)也是右结合。 + +```javascript +2 ** 3 ** 2 +// 相当于 2 ** (3 ** 2) +// 512 +``` + + + +## 真题解答 + + + +- 下面代码中,*a* 在什么情况下会执行输出语句打印 *1* ? + +```js +var a = ?; +if(a == 1 && a == 2 && a == 3){ + console.log(1); +} +``` + +> 参考答案: +> +> 方法一:利用 *toString( )* 方法 +> +> ```js +> var a = { +> i: 1, +> toString() { +> return a.i++; +> } +> } +> if (a == 1 && a == 2 && a == 3) { +> console.log('1'); +> } +> ``` +> +> 方法二:利用 *valueOf( )* 方法 +> +> ```js +> var a = { +> i: 1, +> valueOf() { +> return a.i++ +> } +> } +> if (a == 1 && a == 2 && a == 3) { +> console.log('1'); +> } +> ``` + + + +-*EOF*- + diff --git a/06. 原型和原型链/原型和原型链.js b/06. 原型和原型链/原型和原型链.js new file mode 100644 index 0000000..f9e3392 --- /dev/null +++ b/06. 原型和原型链/原型和原型链.js @@ -0,0 +1,99 @@ +// var person = { +// arms : 2, +// legs : 2 +// } +// Object.create 方法第一个参数是原型对象 +// Object.create 方法接收第二个参数:对象 +// 该对象里面可以设置多个键值对 +// 每个键就是新对象的属性,所对应的值是一个属性描述符 +// var zhangsan = Object.create(person,{ +// name : { +// value : "zhangsan", +// enumerable : true +// }, +// age : { +// value : 18, +// enumerable : true +// } +// }); +// person 实际上就是 zhangsan 这个对象的原型对象 +// console.log(zhangsan); +// console.log(zhangsan.arms); +// console.log(zhangsan.legs); + +// console.log(zhangsan.__proto__ === person); + +// var zhangxiaosan = Object.create(zhangsan, { +// name : { +// value : "zhangxiaosan", +// enumerable : true +// }, +// born : { +// value : "beijing", +// enumerable: true +// } +// }) +// console.log(zhangxiaosan.name); // zhangxiaosan +// console.log(zhangxiaosan.arms); // 2 +// console.log(zhangxiaosan.gender); // undefined + +// 总结,当查找一个对象的属性的时候,如果该对象上面没有这个属性, +// 则会去该对象上面的原型对象上面进行查找 + +// console.log(zhangxiaosan.__proto__ === zhangsan); +// console.log(zhangxiaosan.__proto__.__proto__ === person); + +// 之后,随着 js 语言的发展,我们还是希望 js 能够像标准的面向对象语言一样 +// 通过类来批量的生产对象 +// 早期 js 通过构造函数来模拟其他语言里面的类 + +function Computer(name, price){ + this.name = name; + this.price = price; +} +// 将方法挂在原型对象上面 +Computer.prototype.showPrice = function(){ + console.log(`${this.name}的电脑价格为${this.price}`); +} +var apple = new Computer("苹果", 15000); +// console.log(apple); +apple.showPrice(); + +var huawei = new Computer("华为", 12000); +// console.log(huawei); +huawei.showPrice(); + +// 虽然上面的方式模拟出了其他语言中面向对象的语言创建对象的方式 +// 但是在 js 底层还是基于原型来创建的对象 + +// 比如我们的对象除了有属性,一般还有方法 +// 方法一般会选择挂到原型对象上面 + +// console.log(apple.__proto__ === Computer.prototype); +// console.log(apple.constructor === Computer); + +// 内置的构造函数也有这样的三角关系 +// var arr = []; +// console.log(Array.prototype === arr.__proto__); +// console.log(Array.__proto__ === Computer.__proto__); +// console.log(Date.__proto__ === Computer.__proto__); +// console.log(String.__proto__ === Computer.__proto__); +// console.log(Number.__proto__ === Computer.__proto__); +// console.log(Boolean.__proto__ === Computer.__proto__); +// console.log(Computer.__proto__); + +// 验证原型对象的终点是 null +// console.log(apple.__proto__.__proto__.__proto__); // null +// console.log(apple.__proto__.__proto__ === Object.prototype); +// console.log(Object.prototype.__proto__); // null + +// console.log(Computer.__proto__.__proto__ === Object.prototype); // true + +// Object.prototype 再往上一层(__proto__)就是 null +// console.log(Object.prototype.constructor); + +// console.log(Computer.__proto__ === Object.prototype.constructor.__proto__); + +console.log(Computer.__proto__.__proto__.__proto__); // null +console.log(Computer.__proto__.constructor.__proto__ === Computer.__proto__); // true +console.log(Computer.__proto__.__proto__.constructor.__proto__ === Computer.__proto__); \ No newline at end of file diff --git a/06. 原型和原型链/原型和原型链.md b/06. 原型和原型链/原型和原型链.md new file mode 100644 index 0000000..a6f1dbc --- /dev/null +++ b/06. 原型和原型链/原型和原型链.md @@ -0,0 +1,368 @@ +# 原型和原型链 + + + +## 经典真题 + + + +- 说一说你对 *JavaScript* 中原型与原型链的理解?(美团 *2019*年) +- 对一个构造函数实例化后,它的原型链指向什么? + + + +## 原型与原型链介绍 + + + +在 *Brendan Eich* 设计 *JavaScript* 时,借鉴了 *Self* 和 *Smalltalk* 这两门基于原型的语言。 + + + +之所以选择基于原型的对象系统,是因为 *Brendan Eich* 一开始就没有打算在 *JavaScript* 中加入类的概念,因为 *JavaScript* 的设计初衷就是为非专业的开发人员(例如网页设计者)提供一个方便的工具。由于大部分网页设计者都没有任何的编程背景,所以在设计 *JavaScript* 时也是尽可能使其简单、易学。 + + + +这因为如此,*JavaScript* 中的原型以及原型链成为了这门语言最大的一个特点,在面试的时候,面试官也经常会围绕原型和原型链展开提问。 + + + +*JavaScript* 是一门基于原型的语言,**对象的产生是通过原型对象而来的**。 + + + +*ES5* 中提供了 *Object.create* 方法,可以用来克隆对象。 + + + +示例如下: + +```js +const person = { + arms: 2, + legs: 2, + walk() { + console.log('walking'); + } +} +const zhangsan = Object.create(person); +console.log(zhangsan.arms); // 2 +console.log(zhangsan.legs); // 2 +zhangsan.walk(); // walking +console.log(zhangsan.__proto__ === person); // true +``` + + + +在上面的示例中,我们通过 *Object.create* 方法来对 *person* 对象进行克隆,克隆出来了一个名为 *zhangsan* 的对象,所以 *person* 对象就是 *zhangsan* 这个对象的原型对象。 + + + +*person* 对象上面的属性和方法,*zhangsan* 这个对象上面都有。 + + + +通过 \__*proto*__ 属性,我们可以访问到一个对象的原型对象。 + + + +从上面的代码可以看出,当我们打印`zhangsan.__proto__ === person`,返回的是 *true* ,因为对于 *zhangsan* 这个对象而言,它的原型对象就是 *person* 这个对象。 + + + +我们在使用 *Object.create* 方法来克隆对象的时候,还可以传入第 *2* 个参数,第 *2* 个参数是一个 *JSON* 对象,该对象可以书写新对象的**新属性**以及**属性特性**。 + + + +通过这种方式,基于对象创建的新对象,可以继承祖辈对象的属性和方法,这其实就是一个继承的关系,来看一个示例: + + + +```js +const person = { + arms: 2, + legs: 2, + walk() { + console.log('walking'); + } +} +const zhangsan = Object.create(person, { + name: { + value: "zhangsan", + }, + age: { + value: 18, + }, + born: { + value: "chengdu" + } +}); +const zhangxiaosan = Object.create(zhangsan, { + name: { + value: "zhangxiaosan" + }, + age: { + value: 1 + } +}) +console.log(zhangxiaosan.name); // zhangxiaosan +console.log(zhangxiaosan.age); // 1 +console.log(zhangxiaosan.born); // chengdu +console.log(zhangxiaosan.arms); // 2 +console.log(zhangxiaosan.legs); // 2 +zhangxiaosan.walk(); // walking +console.log(zhangsan.isPrototypeOf(zhangxiaosan)); // true +console.log(person.isPrototypeOf(zhangxiaosan)); // true +``` + + + +该例中,*zhangsan* 这个对象是从 *person* 这个对象克隆而来的,而 *zhangxiaosan* 这个对象又是从 *zhangsan* 这个对象克隆而来,以此**形成了一条原型链**。无论是 *person* 对象,还是 *zhangsan* 对象上面的属性和方法,*zhangxiaosan* 这个对象都能继承到。 + + + +来看下面的图: + + + +image-20210810130602385 + + + +这就是 *JavaScript* 中最原始的创建对象的方式,一个对象是通过克隆另外一个对象所得到的。就像克隆羊多莉一样,通过克隆可以创造一个一模一样的对象,被克隆的对象是新对象的原型对象。 + + + +image-20210810131613519 + + + +但是,随着 *JavaScript* 语言的发展,这样创建对象的方式还是太过于麻烦了。开发者还是期望 *JavaScript* 能够像 *Java、C#* 等标准面向对象语言一样,通过类来批量的生成对象。于是出现了通过构造函数来模拟类的形式。 + + + +来看下面的例子: + + + +```js +function Computer(name, price) { + // 属性写在类里面 + this.name = name; + this.price = price; +} +// 方法挂在原型对象上面 +Computer.prototype.showSth = function () { + console.log(`这是一台${this.name}电脑`); +} + +const apple = new Computer("苹果", 12000); +console.log(apple.name); // 苹果 +console.log(apple.price); // 12000 +apple.showSth(); // 这是一台苹果电脑 + +const huawei = new Computer("华为", 7000); +console.log(huawei.name); // 华为 +console.log(huawei.price); // 7000 +huawei.showSth(); // 这是一台华为电脑 +``` + + + +在上面的例子中,我们书写了一个 *Computer* 的函数,我们称之为构造函数,为了区分普通函数和构造函数,一般构造函数的函数名**首字母会大写**。 + + + +区别于普通函数的直接调用,构造函数一般通过配合 *new* 关键字一起使用,每当我们 *new* 一次,就会生成一个新的对象,而在构造函数中的 *this* 就指向这个新生成的对象。 + + + +在上面的例子中,我们 *new* 了两次,所以生成了两个对象,我们把这两个对象分别存储到 *apple* 和 *huawei* 这两个变量里面。 + + + +有一个非常有意思的现象,就是我们在书写 *Computer* 构造函数的实例方法的时候,并没有将这个方法书写在构造函数里面,而是写在了 *Computer.prototype* 上面,那么这个 *Computer.prototype* 是啥呢? + + + +这个 *Computer.prototype* 实际上就是 *Computer* 实例对象的原型对象。要搞清楚这个,看下面的图: + + + +image-20211027143330933 + + + +这是最重要的一个三角关系,也是我往往要求学生记下来的三角关系。 + + + +通过上图,我们可以得出以下的结论: + + + +- *JavaScript* 中每个对象都有一个原型对象。可以通过 \__*proto*__ 属性来访问到对象的原型对象。 +- 构造函数的 *prototype* 属性指向一个对象,这个对象是该构造函数实例化出来的对象的原型对象。 +- 原型对象的 *constructor* 属性也指向其构造函数。 +- 实例对象的 *constructor* 属性是从它的原型对象上面访问到。 + + + +实践才是检验真理的唯一标准。接下来我们在代码中来验证一下: + + + +```js +function Computer(name, price) { + // 属性写在类里面 + this.name = name; + this.price = price; +} +// 方法挂在原型对象上面 +Computer.prototype.showSth = function () { + console.log(`这是一台${this.name}电脑`); +} + +const apple = new Computer("苹果", 12000); + +console.log(apple.__proto__ === Computer.prototype); // true +console.log(apple.__proto__.constructor === Computer); // true +``` + + + +在上面的代码中,*apple* 是从 *Computer* 这个构造函数中实例化出来的对象,我们通过 \__*proto*__ 来访问到 *apple* 的原型对象,而这个原型对象和 *Computer.prototype* 是等价的。另外, 我们也发现 *apple* 和它原型对象的 *constructor* 属性都指向 *Computer* 这个构造函数。 + + + +接下来我们还可以来验证内置的构造函数是不是也是这样的关系,如下: + +```js +function Computer(name, price) { + // 属性写在类里面 + this.name = name; + this.price = price; +} +// 方法挂在原型对象上面 +Computer.prototype.showSth = function () { + console.log(`这是一台${this.name}电脑`); +} + +const apple = new Computer("苹果", 12000); + +console.log(apple.__proto__ === Computer.prototype); // true +console.log(apple.__proto__.constructor === Computer); // true + +// 数组的三角关系 +var arr = []; +console.log(arr.__proto__ === Array.prototype); // true + +// 其实所有的构造函数的原型对象都相同 +console.log(Computer.__proto__ === Array.__proto__); // true +console.log(Computer.__proto__ === Date.__proto__); // true +console.log(Computer.__proto__ === Number.__proto__); // true +console.log(Computer.__proto__ === Function.__proto__); // true +console.log(Computer.__proto__ === Object.__proto__); // true +console.log(Computer.__proto__); // {} +``` + +通过上面的代码,我们发现所有的构造函数,无论是自定义的还是内置的,它们的原型对象都是同一个对象。 + + + +如果你能够把上面的三角关系理清楚,恭喜你,你已经把整个原型和原型链的知识掌握一大部分。 + + + +如果你还想继续往下深究,那么上面的图可以扩展成这样: + + + +image-20211027144428458 + + + +在 *JavaScript* 中,每一个对象,都有一个原型对象。而原型对象上面也有一个自己的原型对象,一层一层向上找,最终会到达 *null*。 + + + +我们可以在上面代码的基础上,继续进行验证,如下: + + + +```js +function Computer(name, price) { + // 属性写在类里面 + this.name = name; + this.price = price; +} +// 方法挂在原型对象上面 +Computer.prototype.showSth = function () { + console.log(`这是一台${this.name}电脑`); +} + +var apple = new Computer("苹果", 12000); + +console.log(apple.__proto__.__proto__); // [Object: null prototype] {} +console.log(apple.__proto__.__proto__.__proto__); // null +console.log(apple.__proto__.__proto__ === Object.prototype); // true +``` + + + +可以看到,在上面的代码中,我们顺着原型链一层一层往上找,最终到达了 *null*。 + +但是目前来看我们这个图还是不完整,既然构造函数的原型对象也是对象,那么必然该对象也有自己的原型,所以完整的图其实如下: + +image-20211027152845110 + +下面可以简单验证一下,如下: + +```js +// 自定义构造函数函数 +function Computer() {} + +console.log(Computer.__proto__.__proto__.__proto__); // null +console.log(Computer.__proto__.constructor.__proto__ === Computer.__proto__); // true +console.log(Computer.__proto__.__proto__.constructor.__proto__ === Computer.__proto__); // true + +``` + + + +## 真题解答 + + + +- 说一说你对 *JavaScript* 中原型与原型链的理解?(美团 *2019*年) + +> 参考答案: +> +> - 每个对象都有一个 \__*proto*__ 属性,该属性指向自己的原型对象 +> - 每个构造函数都有一个 *prototype* 属性,该属性指向实例对象的原型对象 +> - 原型对象里的 *constructor* 指向构造函数本身 +> +> 如下图: +> +> image-20211027143330933 +> +> 每个对象都有自己的原型对象,而原型对象本身,也有自己的原型对象,从而形成了一条原型链条。 +> +> 当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。 + + + +- 对一个构造函数实例化后,它的原型链指向什么? + +> 参考答案: +> +> 指向该构造函数实例化出来对象的原型对象。 +> +> 对于构造函数来讲,可以通过 *prototype* 访问到该对象。 +> +> 对于实例对象来讲,可以通过隐式属性 \__*proto*__ 来访问到。 + + + +-*EOF*- + diff --git a/07. 执行栈和执行上下文/执行栈和执行上下文.js b/07. 执行栈和执行上下文/执行栈和执行上下文.js new file mode 100644 index 0000000..8ba656e --- /dev/null +++ b/07. 执行栈和执行上下文/执行栈和执行上下文.js @@ -0,0 +1,120 @@ +// function a(){ +// var i= 10; +// console.log(i); +// } + +// a() + +// // 创建一个执行上下文环境 +// // 开始执行代码 + + +// var i = 1; +// console.log(i); + +// 在执行上面的全局代码之前,还有一个准备工作 +// 创建一个全局上下文 + +// console.log("Hello"); +// function foo () { +// // .... +// function bar () { +// return 'I am bar'; +// } +// return bar(); +// } +// foo(); +// console.log("World"); + +// const foo = function(i){ +// console.log(b); +// console.log(c); +// var a = "Hello"; +// var b = function privateB(){}; +// function c(){} +// } +// foo(10); + +// 生成一个 foo 的函数上下文环境 + +// 1. 创建上下文阶段 + +// vo 里面要确定的东西 +// - 确定函数的形参(并赋值) +// - 函数环境会初始化创建 Arguments对象(并赋值) +// - 确定普通字面量形式的函数声明(并赋值) +// - 变量声明,函数表达式声明(未赋值) + +// fooExecutionContext = { +// // vo = { +// // i : 10, +// // arguments : {0 : 10, length : 1}, +// // c : 指向 c 那个函数 +// // a : undefined +// // b : undefined +// // } +// // this, +// // scope +// } + +// 2. 执行代码 +// vo = { +// i : 10, +// arguments : {0 : 10, length : 1}, +// c : 指向 c 那个函数 +// a : "Hello" +// b : privateB 函数 +// } + + +(function () { + console.log(typeof foo); + console.log(typeof bar); + var foo = "Hello"; + var bar = function () { + return "World"; + } + + function foo() { + return "good"; + } + console.log(foo, typeof foo); +})() + +// 上面的代码也会创建一个函数上下文 + +// 上下文的分为两个阶段:1. 创建阶段 2. 执行阶段 +// 1. 创建阶段 +// vo 里面要确定的东西 +// - 确定函数的形参(并赋值) +// - 函数环境会初始化创建 Arguments对象(并赋值) +// - 确定普通字面量形式的函数声明(并赋值) +// - 变量声明,函数表达式声明(未赋值) +// 在进行变量声明的时候,如果发现该变量名已经存在,则不会再声明 +// executionContext = { +// vo : { +// // foo : 指向 foo 函数 +// // bar : undefiend +// }, +// // this, +// // scope +// } + +// 2. 执行代码 +// vo : { +// // foo : "Hello" +// // bar : function A +// }, + + +// console.log(typeof foo); // function +// console.log(typeof bar); // undefined +// var foo = "Hello"; +// var bar = function A() { +// return "World"; +// } + +// function foo() { +// return "good"; +// } +// console.log(foo, typeof foo); // Hello, string \ No newline at end of file diff --git a/07. 执行栈和执行上下文/执行栈和执行上下文.md b/07. 执行栈和执行上下文/执行栈和执行上下文.md new file mode 100644 index 0000000..82b929f --- /dev/null +++ b/07. 执行栈和执行上下文/执行栈和执行上下文.md @@ -0,0 +1,318 @@ +# 执行栈和执行上下文 + + + +## 经典真题 + + + +- 谈谈你对 *JavaScript* 执行上下文栈理解 + + + +## 执行上下文 + + + +执行上下文,英文全称为 *Execution Context*,一句话概括就是“代码(全局代码、函数代码)执行前进行的准备工作”,也称之为“执行上下文环境”。 + +运行 *JavaScript* 代码时,当代码执行进入一个环境时,就会为该环境创建一个执行上下文,它会在你运行代码前做一些准备工作,如确定作用域,创建局部变量对象等。 + +具体做了什么我们后面再说,先来看下 *JavaScript* 执行环境有哪些? + + + +***JavaScript* 中执行环境** + +1. 全局环境 +2. 函数环境 +3. *eval* 函数环境 (已不推荐使用) + +那么与之对应的执行上下文类型同样有 *3* 种: + +1. 全局执行上下文 +2. 函数执行上下文 +3. *eval* 函数执行上下文 + + + +*JavaScript* 运行时首先会进入全局环境,对应会生成全局上下文。程序代码中基本都会存在函数,那么**调用函数**,就会进入函数执行环境,对应就会生成该函数的执行上下文。 + +由于代码中会声明多个函数,对应的函数执行上下文也会存在多个。在 *JavaScript* 中,通过栈的存取方式来管理执行上下文,我们可称其为执行栈,或函数调用栈(*Call Stack*)。 + + + +## 栈数据结构 + + + +先来简单复习一下栈这种数据结构。 + +要简单理解栈的存取方式,我们可以通过类比乒乓球盒子来分析。如下图: + +![img](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-10-02-060310.png) + +栈遵循**“先进后出,后进先出”**的规则,或称 ***LIFO*** (”*Last In First Out*“)规则。 + +如图所示,我们只能从栈顶取出或放入乒乓球,最先放进盒子的总是最后才能取出。 + +栈中**“放入/取出”**,也可称为**“入栈/出栈”**。 + +总结栈数据结构的特点: + +1. 后进先出,先进后出 +2. 出口在顶部,且仅有一个 + + + +**执行栈(函数调用栈)** + +理解完栈的存取方式,我们接着分析 *JavaScript* 中如何通过栈来管理多个执行上下文。 + +程序执行进入一个执行环境时,它的执行上下文就会被创建,并被推入执行栈中(入栈);程序执行完成时,它的执行上下文就会被销毁,并从栈顶被推出(出栈),控制权交由下一个执行上下文。 + +因为 *JavaScript* 在执行代码时最先进入全局环境,所以**处于栈底的永远是全局环境的执行上下文**。而处于**栈顶的是当前正在执行函数的执行上下文**。 + +当函数调用完成后,它就会从栈顶被推出,理想的情况下,闭包会阻止该操作,闭包可以参阅《闭包》章节。 + +而全局环境只有一个,对应的全局执行上下文也只有一个,只有当页面被关闭之后它才会从执行栈中被推出,否则一直存在于栈底。 + + + +下面我们来看一段具体的代码示例: + +```js +function foo () { + function bar () { + return 'I am bar'; + } + return bar(); +} +foo(); +``` + +对应图解如下: + +image-20211002140848188 + + + +**执行上下文的数量限制(堆栈溢出)** + +执行上下文可存在多个,虽然没有明确的数量限制,但如果超出栈分配的空间,会造成堆栈溢出。常见于递归调用,没有终止条件造成死循环的场景。 + +```js +// 递归调用自身 +function foo() { + foo(); +} +foo(); +// 报错: Uncaught RangeError: Maximum call stack size exceeded +``` + + + +## 执行上下文生命周期 + + + +前面我们有说到,运行 *JavaScript* 代码时,当代码执行进入一个环境时,就会为该环境创建一个执行上下文,它会在你运行代码前做一些准备工作。接下来我们就来看一下具体会做哪些准备工作。 + +具体要做的事,和执行上下文的生命周期有关。 + +执行上下文的生命周期有两个阶段: + +1. 创建阶段(**进入**执行上下文):函数被调用时,进入函数环境,为其创建一个执行上下文,此时进入创建阶段。 +2. 执行阶段(代码**执行**):执行函数中代码时,此时执行上下文进入执行阶段。 + + + +**创建阶段** + +创建阶段要做的事情主要如下: + +1. 创建变量对象(*VO:variable object*) + + - 确定函数的形参(**并赋值**) + + - 函数环境会初始化创建 *Arguments*对象(**并赋值**) + - 确定普通字面量形式的函数声明(**并赋值**) + - 变量声明,函数表达式声明(**未赋值**) + +2. 确定 *this* 指向(***this* 由调用者确定**) + +3. 确定作用域(**词法环境决定,哪里声明定义,就在哪里确定**) + + + +这里有必要说一下变量对象。 + +当处于执行上下文的建立阶段时,我们可以将整个上下文环境看作是一个对象。该对象拥有 *3* 个属性,如下: + +```js +executionContextObj = { + variableObject : {}, // 变量对象,里面包含 Arguments 对象,形式参数,函数和局部变量 + scopeChain : {},// 作用域链,包含内部上下文所有变量对象的列表 + this : {}// 上下文中 this 的指向对象 +} +``` + +可以看到,这里执行上下文抽象成为了一个对象,拥有 *3* 个属性,分别是**变量对象**,**作用域链**以及 ***this* 指向**,这里我们重点来看一下变量对象里面所拥有的东西。 + +在函数的建立阶段,首先会建立 *Arguments* 对象。然后确定形式参数,检查当前上下文中的函数声明,每找到一个函数声明,就在 *variableObject* 下面用函数名建立一个属性,属性值就指向该函数在内存中的地址的一个引用。 + +如果上述函数名已经存在于 *variableObject*(简称 *VO*) 下面,那么对应的属性值会被新的引用给覆盖。 + +最后,是确定当前上下文中的局部变量,如果遇到和函数名同名的变量,则会忽略该变量。 + + + +**执行阶段** + +1. 变量对象赋值 + - 变量赋值 + - 函数表达式赋值 +2. 调用函数 +3. 顺序执行其它代码 + + + +两个阶段要做的事情介绍完毕,接下来我们来通过代码来演示一下这两个阶段做的每一件事以及变量对象是如何变化的。 + +```js +const foo = function(i){ + var a = "Hello"; + var b = function privateB(){}; + function c(){} +} +foo(10); +``` + +首先在建立阶段的变量对象如下: + +```js +fooExecutionContext = { + variavleObject : { + arguments : {0 : 10,length : 1}, // 确定 Arguments 对象 + i : 10, // 确定形式参数 + c : pointer to function c(), // 确定函数引用 + a : undefined, // 局部变量 初始值为 undefined + b : undefined // 局部变量 初始值为 undefined + }, + scopeChain : {}, + this : {} +} +``` + +由此可见,在建立阶段,除了 *Arguments*,函数的声明,以及形式参数被赋予了具体的属性值外,其它的变量属性默认的都是 *undefined*。并且普通形式声明的函数的提升是在变量的上面的。 + +一旦上述建立阶段结束,引擎就会进入代码执行阶段,这个阶段完成后,上述执行上下文对象如下,变量会被赋上具体的值。 + +```js +fooExecutionContext = { + variavleObject : { + arguments : {0 : 10,length : 1}, + i : 10, + c : pointer to function c(), + a : "Hello",// a 变量被赋值为 Hello + b : pointer to function privateB() // b 变量被赋值为 privateB() 函数 + }, + scopeChain : {}, + this : {} +} +``` + +我们看到,只有在代码执行阶段,局部变量才会被赋予具体的值。在建立阶段局部变量的值都是 *undefined*。 + +这其实也就解释了变量提升的原理。 + + + +接下来我们再通过一段代码来加深对函数这两个阶段的过程的理解,代码如下: + +```js +(function () { + console.log(typeof foo); + console.log(typeof bar); + var foo = "Hello"; + var bar = function () { + return "World"; + } + + function foo() { + return "good"; + } + console.log(foo, typeof foo); +})() +``` + +这里,我们定义了一个 *IIFE*,该函数在建立阶段的变量对象如下: + +```js +fooExecutionContext = { + variavleObject : { + arguments : {length : 0}, + foo : pointer to function foo(), + bar : undefined + }, + scopeChain : {}, + this : {} +} +``` + +首先确定 *Arguments* 对象,接下来是形式参数,由于本例中不存在形式参数,所以接下来开始确定函数的引用,找到 *foo* 函数后,创建 *foo* 标识符来指向这个 *foo* 函数,之后同名的 *foo* 变量不会再被创建,会直接被忽略。 + +然后创建 *bar* 变量,不过初始值为 *undefined*。 + +建立阶段完成之后,接下来进入代码执行阶段,开始一句一句的执行代码,结果如下: + +```js +(function () { + console.log(typeof foo); // function + console.log(typeof bar); // undefined + var foo = "Hello"; // foo 被重新赋值 变成了一个字符串 + var bar = function () { + return "World"; + } + + function foo() { + return "good"; + } + console.log(foo, typeof foo); //Hello string +})() +``` + + + +## 真题解答 + + + +- 谈谈你对 *JavaScript* 执行上下文栈理解 + +> 参考答案: +> +> **什么是执行上下文?** +> +> 简而言之,执行上下文是评估和执行 *JavaScript* 代码的环境的抽象概念。每当 *Javascript* 代码在运行的时候,它都是在执行上下文中运行。 +> +> **执行上下文的类型** +> +> *JavaScript* 中有三种执行上下文类型。 +> +> - **全局执行上下文:**这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事,创建一个全局的 *window* 对象(浏览器的情况下),并且设置 *this* 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。 +> - **函数执行上下文:**每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。 +> - ***Eval* 函数执行上下文:**执行在 *eval* 函数内部的代码也会有它属于自己的执行上下文。 +> +> **调用栈** +> +> 调用栈是解析器(如浏览器中的的 *JavaScript* 解析器)的一种机制,可以在脚本调用多个函数时,跟踪每个函数在完成执行时应该返回控制的点。(如什么函数正在执行,什么函数被这个函数调用,下一个调用的函数是谁) +> +> - 当脚本要调用一个函数时,解析器把该函数添加到栈中并且执行这个函数。 +> - 任何被这个函数调用的函数会进一步添加到调用栈中,并且运行到它们被上个程序调用的位置。 +> - 当函数运行结束后,解释器将它从堆栈中取出,并在主代码列表中继续执行代码。 +> - 如果栈占用的空间比分配给它的空间还大,那么则会导致“栈溢出”错误。 + + + +-*EOF*- \ No newline at end of file diff --git a/08. 作用域和作用域链/作用域和作用域链.html b/08. 作用域和作用域链/作用域和作用域链.html new file mode 100644 index 0000000..d080ed2 --- /dev/null +++ b/08. 作用域和作用域链/作用域和作用域链.html @@ -0,0 +1,25 @@ + + + + + + + + Document + + + + + + + + + + \ No newline at end of file diff --git a/08. 作用域和作用域链/作用域和作用域链.js b/08. 作用域和作用域链/作用域和作用域链.js new file mode 100644 index 0000000..287c391 --- /dev/null +++ b/08. 作用域和作用域链/作用域和作用域链.js @@ -0,0 +1,37 @@ +// let i = 1; +// function a(){ +// let i = 2; +// } +// a(); +// console.log(i); + + +// function test(){ +// i = 10; // i 就会是一个全局变量 +// } +// test(); +// console.log(i); + +// var i = 10; +// function a(){ +// function b(){ +// function c(){ +// console.log(i); +// } +// c(); +// } +// b(); +// } +// a(); + +var x = 10 +function fn() { + console.log(x) +} +function show(f) { + var x = 20; + (function () { + f() // 10,而不是 20 + })() +} +show(fn) \ No newline at end of file diff --git a/08. 作用域和作用域链/作用域和作用域链.md b/08. 作用域和作用域链/作用域和作用域链.md new file mode 100644 index 0000000..fa9e4cc --- /dev/null +++ b/08. 作用域和作用域链/作用域和作用域链.md @@ -0,0 +1,423 @@ +# 作用域和作用域链 + + + +## 经典真题 + + + +- 谈谈你对作用域和作用域链的理解? + + + +## 作用域(*Scope*) + + + +### 什么是作用域 + + + +作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。 + +换句话说,作用域决定了代码区块中变量和其他资源的可见性。 + +可能这两句话并不好理解,我们先来看个例子: + +```js +function outFun2() { + var inVariable = "内层变量2"; +} +outFun2(); +console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined +``` + +从上面的例子可以体会到作用域的概念,变量 *inVariable* 在全局作用域没有声明,所以在全局作用域下取值会报错。 + +我们可以这样理解:**作用域就是一个独立的地盘,让变量不会外泄、暴露出去**。也就是说**作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。** + +***ES6* 之前 *JavaScript* 没有块级作用域,只有全局作用域和函数作用域**。 + +*ES6* 的到来,为我们提供了“块级作用域”,可通过新增命令 *let* 和 *const* 来体现。 + + + +### 全局作用域和函数作用域 + + + +**(1)全局作用域** + +在代码中任何地方都能访问到的对象拥有全局作用域,一般来说以下几种情形拥有全局作用域: + +- 最外层函数和在最外层函数外面定义的变量拥有全局作用域 + +```js +var outVariable = "我是最外层变量"; //最外层变量 +function outFun() { //最外层函数 + var inVariable = "内层变量"; + function innerFun() { //内层函数 + console.log(inVariable); + } + innerFun(); +} +console.log(outVariable); // 我是最外层变量 +outFun(); // 内层变量 +console.log(inVariable); // inVariable is not defined +innerFun(); // innerFun is not defined +``` + + + +- 所有未定义直接赋值的变量自动声明为拥有全局作用域 + +```js +function outFun2() { + variable = "未定义直接赋值的变量"; + var inVariable2 = "内层变量2"; +} +outFun2();//要先执行这个函数,否则根本不知道里面是啥 +console.log(variable); //未定义直接赋值的变量 +console.log(inVariable2); //inVariable2 is not defined +``` + + + +- 所有 *window* 对象的属性拥有全局作用域 + +一般情况下,*window* 对象的内置属性都拥有全局作用域,例如 *window.name、window.location、window.top* 等等。 + + + +全局作用域有个弊端:如果我们写了很多行 *JS* 代码,变量定义都没有用函数包括,那么它们就全部都在全局作用域中。这样就会污染全局命名空间, 容易引起命名冲突。 + +```js +// 张三写的代码中 +var data = {a: 100} + +// 李四写的代码中 +var data = {x: true} +``` + +这就是为何 *jQuery、Zepto* 等库的源码,所有的代码都会放在 *(function(){....})( )* 中。 + +因为放在里面的所有变量,都不会被外泄和暴露,不会污染到外面,不会对其他的库或者 *JS* 脚本造成影响。这是函数作用域的一个体现。 + + + +**(2)函数作用域** + +函数作用域,是指声明在函数内部的变量,和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部。 + +```js +function doSomething(){ + var stuName="zhangsan"; + function innerSay(){ + console.log(stuName); + } + innerSay(); +} +console.log(stuName); // 脚本错误 +innerSay(); // 脚本错误 +``` + +**作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行**。 + +我们看个例子,用泡泡来比喻作用域可能好理解一点: + +image-20211001231740498 + +最后输出的结果为 *2、4、12* + +- 泡泡 *1* 是全局作用域,有标识符 *foo*; +- 泡泡 *2* 是作用域 *foo*,有标识符 *a、bar、b*; +- 泡泡 *3* 是作用域 *bar*,仅有标识符 *c*。 + +值得注意的是:**块语句(大括号“{ }”中间的语句),如 *if* 和 *switch* 条件语句或 *for* 和 *while* 循环语句,不像函数,它们不会创建一个新的作用域**。在块语句中定义的变量将保留在它们已经存在的作用域中。 + +```js +if (true) { + // 'if' 条件语句块不会创建一个新的作用域 + var name = 'Hammad'; // name 依然在全局作用域中 +} +console.log(name); // logs 'Hammad' +``` + +*JS* 的初学者经常需要花点时间才能习惯变量提升,而如果不理解这种特有行为,就可能导致 *bug* 。 + +正因为如此, *ES6* 引入了块级作用域,让变量的生命周期更加可控。 + + + +### 块级作用域 + + + +块级作用域可通过新增命令 *let* 和 *const* 声明,所声明的变量在指定块的作用域外无法被访问。 + +块级作用域在如下情况被创建: + +1. 在一个函数内部 +2. 在一个代码块(由一对花括号包裹)内部 + +*let* 声明的语法与 *var* 的语法一致。你基本上可以用 *let* 来代替 *var* 进行变量声明,但会将变量的作用域限制在当前代码块中。块级作用域有以下几个特点: + + + +- 声明变量不会提升到代码块顶部 + +*let、const* 声明并不会被提升到当前代码块的顶部,因此你需要手动将 *let、const* 声明放置到顶部,以便让变量在整个代码块内部可用。 + +```js +function getValue(condition) { + if (condition) { + let value = "blue"; + return value; + } else { + // value 在此处不可用 + return null; + } + // value 在此处不可用 +} +``` + + + +- 禁止重复声明 + +如果一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符进行 *let* 声明就会导致抛出错误。例如: + +```js +var count = 30; +let count = 40; // Uncaught SyntaxError: Identifier 'count' has already been declared +``` + +在本例中, *count* 变量被声明了两次:一次使用 *var* ,另一次使用 *let*。 + +因为 *let* 不能在同一作用域内重复声明一个已有标识符,此处的 *let* 声明就会抛出错误。但如果在嵌套的作用域内使用 *let* 声明一个同名的新变量,则不会抛出错误。 + +```js +var count = 30; +// 不会抛出错误 +if (condition) { + let count = 40; + // 其他代码 +} +``` + + + +- 循环中的绑定块作用域的妙用 + +开发者可能最希望实现 *for* 循环的块级作用域了,因为可以把声明的计数器变量限制在循环内。 + +例如,以下代码在 *JS* 经常见到: + +```html + + + +``` + +```js +var btns = document.getElementsByTagName('button') +for (var i = 0; i < btns.length; i++) { + btns[i].onclick = function () { + console.log('第' + (i + 1) + '个') + } +} +``` + +我们要实现这样的一个需求: 点击某个按钮, 提示"点击的是第 *n* 个按钮"。 + +此处我们先不考虑事件代理,万万没想到,点击任意一个按钮,后台都是弹出“第四个”。 + +这是因为 *i* 是全局变量,执行到点击事件时,此时 *i* 的值为 *3*。 + +那该如何修改,最简单的是用 *let* 声明 *i* + +```js +for (let i = 0; i < btns.length; i++) { + btns[i].onclick = function () { + console.log('第' + (i + 1) + '个') + } +} +``` + + + +## 作用域链 + + + +### 什么是自由变量 + + + +首先认识一下什么叫做**自由变量** 。 + +如下代码中,*console.log(a)* 要得到 *a* 变量,但是在当前的作用域中没有定义 *a*(可对比一下 *b*)。当前作用域没有定义的变量,这成为自由变量 。 + +自由变量的值如何得到 ? + +需要向父级作用域寻找(注意:这种说法并不严谨,下文会重点解释)。 + +```js +var a = 100 +function fn() { + var b = 200 + console.log(a) // 这里的 a 在这里就是一个自由变量 + console.log(b) +} +fn() +``` + + + +### 什么是作用域链 + + + +如果父级也没呢? + +再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链 。 + +```js +var a = 100 +function f1() { + var b = 200 + function f2() { + var c = 300 + console.log(a) // 100 自由变量,顺作用域链向父作用域找 + console.log(b) // 200 自由变量,顺作用域链向父作用域找 + console.log(c) // 300 本作用域的变量 + } + f2() +} +f1() +``` + + + +### 关于自由变量的取值 + + + +关于自由变量的值,上文提到要到父作用域中取,其实有时候这种解释会产生歧义。 + +```js +var x = 10 +function fn() { + console.log(x) +} +function show(f) { + var x = 20; + (function () { + f() // 10,而不是 20 + })() +} +show(fn) +``` + +在 *fn* 函数中,取自由变量 *x* 的值时,要到哪个作用域中取 ? + +要到创建 *fn* 函数的那个作用域中取,**无论 *fn* 函数将在哪里调用**。 + +所以,不要在用以上说法了。相比而言,用这句话描述会更加贴切:**要到创建这个函数的那个域”。作用域中取值,这里强调的是“创建”,而不是“调用”**,切记切记,其实这就是所谓的"静态作用域"。 + +再来看一个例子: + +```js +const food = "rice"; +const eat = function () { + console.log(`eat ${food}`); +}; +(function () { + const food = "noodle"; + eat(); // eat rice +})(); +``` + +在本示例中,最终打印的结果为 *eat rice*。因为对于 *eat( )* 函数来说,创建该函数时它的父级上下文为全局上下文,所以 *food* 的值为 *rice*。 + +如果我们将代码稍作修改,改成如下: + +```js +const food = "rice"; +(function () { + const food = "noodle"; + const eat = function () { + console.log(`eat ${food}`); + }; + eat(); // eat noodle +})(); +``` + +这个时候,打印出来的值就为 *eat noodle*。因为对于 *eat( )* 函数来讲,创建它的时候父级上下文为 *IIFE*,所以 *food* 的值为 *noodle*。 + + + +## 作用域与执行上下文 + + + +许多开发人员经常混淆作用域和执行上下文的概念,误认为它们是相同的概念,但事实并非如此。 + +我们知道 *JavaScript* 属于解释型语言,*JavaScript* 的执行分为:解释和执行两个阶段,这两个阶段所做的事并不一样。 + + + +**解释阶段** + +- 词法分析 +- 语法分析 +- 作用域规则确定 + + + +**执行阶段** + +- 创建执行上下文 +- 执行函数代码 +- 垃圾回收 + + + +*JavaScript* 解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。 + +执行上下文最明显的就是 *this* 的指向是执行时确定的。而作用域访问的变量是编写代码的结构确定的。 + +作用域和执行上下文之间最大的区别是: + +**执行上下文在运行时确定,随时可能改变,作用域在定义时就确定,并且不会改变**。 + + + +## 真题解答 + + + +- 谈谈你对作用域和作用域链的理解? + +> 参考答案: +> +> **什么是作业域 ?** +> +> *ES5* 中只存在两种作用域:全局作用域和函数作用域。 +> +> 在 *JavaScript* 中,我们将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套子作用域中根据标识符名称进行变量(变量名或者函数名)查找。*ES6* 新增了块级作用域。 +> +> **什么是作用域链 ?** +> +> 当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止。 +> +> 而作用域链,就是有当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域对符合访问权限的变量和函数的有序访问。 +> +> 作用域链有一个非常重要的特性,**那就是作用域中的值是在函数创建的时候,就已经被存储了,是静态的**。 +> +> 所谓静态,就是说作用域中的值一旦被确定了,永远不会变。**函数可以永远不被调用,但是作用域中的值在函数创建的时候就已经被写入了,**并且存储在函数作用域链对象里面。 + + + +-*EOF*- + diff --git a/09. this指向/this指向.js b/09. this指向/this指向.js new file mode 100644 index 0000000..927fb0b --- /dev/null +++ b/09. this指向/this指向.js @@ -0,0 +1,264 @@ +// 只要这个函数是以普通函数的形式被调用 +// function fn1(){ +// console.log(this); // 指向全局对象 +// } +// fn1(); + +// 如果是严格模式,那么 this 的值为 undefiend +// function fn2(){ +// 'use strict' +// console.log(this); +// } +// fn2(); + +// 上面介绍了以函数的形式调用,this 的指向 +// 这种题目有一种变形 +// var foo = { +// bar : 10, +// func(){ +// console.log(this); +// console.log(this.bar); +// } +// } +// var fn2 = foo.func; +// fn2(); +// foo.func(); + +// 如果一个函数是以对象的方法的形式被调用 +// 那么 this 指向该对象 +// var stu = { +// name : "zhangsan", +// fn(){ +// return this; +// } +// } +// console.log(stu.fn() === stu); + +// var stu = { +// name : "zhangsan", +// son : { +// name : "zhangxiaosan", +// fn(){ +// return this.name; +// } +// } +// } +// console.log(stu.son.fn()); + +// var o1 = { +// text : 'o1', +// fn(){ +// return this.text; +// } +// } + +// var o2 = { +// text : 'o2', +// fn(){ +// return o1.fn(); +// } +// } + +// var o3 = { +// text : 'o3', +// fn(){ +// var fn2 = o1.fn; +// return fn2(); // 这里就相当于是普通函数的形式被调用 +// } +// } +// console.log(o1.fn()); // o1 +// console.log(o2.fn()); // o1 +// console.log(o3.fn()); // undefined + +// call +// A.call(B) +// A 通常是一个方法(函数) +// B 通常是一个对象 +// 调用 A 方法,但是 this 指向 B 这个对象 + +// var obj = {}; +// function fn(){ +// return this; +// } +// console.log(fn() === global); +// console.log(fn.call(obj) === obj); + +// 下面的情况,this 指向全局对象 +// console.log(fn.call()); +// console.log(fn.call(null)); +// console.log(fn.call(undefined)); +// 总之,this 就指向你传入进去的对象 + +// console.log(fn.call(true)); + +// call 第一个参数是 this 指向的对象 +// 之后的参数就是参数列表,这些参数会传递给前面的方法 +// function add(a, b){ +// return a + b; +// } +// console.log(add.call(null, 1, 2)); + +// call 一个经常的应用,就是调用原生的方法 + +// var obj = {}; +// hasOwnProperty 该方法是查看一个对象是否有某一个属性或者方法 +// 这个属性或者方法必须是自身就有的,而不是继承而来 +// console.log(obj.hasOwnProperty('toString')); // false +// console.log(obj.toString()); // [object Object] + +// 通过上面的例子,我们可以知道 +// obj 能够调用 toString,但是 toString 这个方法并不是他自身所拥有的 +// 来自于它的原型对象上面 + +// obj.hasOwnProperty = function(){ +// return 'aaaaa'; +// } +// console.log(obj.hasOwnProperty('toString')); // aaaaa + +// 上面我们对 hasOwnProperty 这个方法进行了覆盖 +// 使用 call 可以调用原生的方法 +// console.log(Object.prototype.hasOwnProperty.call(obj, 'toString'));; + +// apply +// 该方法和 call 基本上一模一样 +// 区别仅仅是后面参数的区别,call 后面是参数列表 +// 而 apply 后面是一个参数数组 + +// 使用 apply 调用原生方法 + +// var arr = [1, 2, 3, 4, 5]; + +// console.log(Math.max.apply(null, arr)); + +// console.log(Array.prototype.slice.apply({ 0: 1, 1: 2, 2: 3 })); +// console.log(Array.prototype.slice.apply({ 0: 1, 1: 2, 2: 3, length:3 })); +// console.log(Array.prototype.slice.apply({ 0: 1, 1: 2, 2: 3, length:5 })); +// console.log(Array.prototype.slice); + +// bind 绑定 this 指向,返回一个新的函数 + +// var d = new Date(); +// console.log(d); +// console.log(d.getTime()); + +// var fn = d.getTime; +// fn(); +// 上面的调用方式,使得 this 指向了全局对象,而非 Date 实例对象 +// 下面使用 bind 来绑定 + +// var fn = d.getTime.bind(d); +// console.log(fn()); + +// bind 示例2 +// var counter = { +// count : 0, +// add(){ +// this.count++; +// } +// } +// var obj = { +// count : 100 +// } +// var fn = counter.add.bind(obj); +// fn(); +// console.log(counter.count); +// console.log(obj.count); + + +// var counter = { +// count : 0, +// add(){ +// 'use strict' +// this.count++; +// } +// } + +// function callback(fn){ +// fn(); +// } + +// callback(counter.add); +// console.log(counter.count); // 1 + +// var obj = { +// name : "zhangsan", +// arr : [1,2,3], +// print(){ +// this.arr.forEach(function(n){ +// console.log(this.name); +// console.log(this === global); +// }.bind(this)) +// } +// } +// obj.print(); + +// bind 方法结合 call 方法使用 + + +// console.log([1, 2, 3].slice(0, 1)); + +// slice 方法来源于 Array.prototype + +// console.log(Array.prototype.slice.call([1, 2, 3], 0, 1)); + +// call 方法来源于 Function.prototype +// var slice = Function.prototype.call.bind(Array.prototype.slice); +// 这里就相当于改写了 slice 方法 +// 以前用 slice 方法 [1,2,3].slice(0,1) +// console.log(slice([1,2,3], 0, 1)); + +// function fn(){ +// console.log(this.v); +// } + +// var obj = { +// v : 123 +// } + +// var func = Function.prototype.call.bind(Function.prototype.bind); +// func(fn, obj)(); + + +// 箭头函数 this 指向 +// var x = 20; +// const obj = { +// x: 10, +// test: () => { +// console.log(this); // {} +// console.log(this.x); // undefined +// } +// } +// obj.test(); + +// var obj = { +// name: '张三', +// times: [1, 2, 3], +// print: function () { +// this.times.forEach((n)=>{ +// console.log(this.name); +// }); +// } +// }; + +// obj.print() + +// var name = "JavaScript"; +// const obj = { +// name: "PHP", +// test: function () { +// const i = ()=> { +// console.log(this.name); +// // i 是以函数的形式被调用的,所以 this 指向全局 +// // 在浏览器环境中打印出 JavaScript,node 里面为 undeifned +// } +// i(); +// } +// } +// obj.test(); + +// 箭头函数不能作为构造函数 +const Test = (name, age) => { + this.name = name; + this.age = age; +}; +const test = new Test("xiejie", 18); \ No newline at end of file diff --git a/09. this指向/this指向.md b/09. this指向/this指向.md new file mode 100644 index 0000000..cd289d5 --- /dev/null +++ b/09. this指向/this指向.md @@ -0,0 +1,803 @@ +# *this* 指向 + + + +## 经典真题 + + + +- *this* 的指向哪几种 ? + + + +## *this* 指向总结 + + + +*this* 关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。 + +*this* 可以用在构造函数之中,表示实例对象。除此之外,*this* 还可以用在别的场合。**但不管是什么场合,*this* 都有一个共同点:它总是返回一个对象**。 + + + +关于 *this* 的指向,有一种广为流传的说法就是“谁调用它,*this* 就指向谁”。 + +这样的说法没有太大的问题,但是并不是太全面。总结起来,*this* 的指向规律有如下几条: + + + +- 在函数体中,非显式或隐式地简单调用函数时,在严格模式下,函数内的 *this* 会被绑定到 *undefined* 上,在非严格模式下则会被绑定到全局对象 *window/global* 上。 + + + +- 一般使用 *new* 方法调用构造函数时,构造函数内的 *this* 会被绑定到新创建的对象上。 + + + +- 一般通过 *call/apply/bind* 方法显式调用函数时,函数体内的 *this* 会被绑定到指定参数的对象上。 + + + +- 一般通过上下文对象调用函数时,函数体内的 *this* 会被绑定到该对象上。 + + + +- 在箭头函数中,*this* 的指向是由外层(函数或全局)作用域来决定的。 + + + +当然,真实环境多种多样,下面我们就来根据实战例题逐一梳理。 + + + +### 全局环境中的 *this* + +例题 *1*: + +```js +function f1() { + console.log(this); +} + +function f2() { + 'use strict' + console.log(this); +} + +f1(); // window or global +f2(); // undefined +``` + +这种情况相对简单、直接,函数在浏览器全局环境下被简单调用,在非严格模式下 *this* 指向 *window*,在通过 *use strict* 指明严格模式的情况下指向 *undefined*。 + +虽然上面的题目比较基础,但是需要注意上面题目的变种,例如 + +例题 *2*: + +```js +const foo = { + bar : 10, + fn : function(){ + console.log(this); // window or global + console.log(this.bar); // undefined + } +} +var fn1 = foo.fn; +fn1(); +``` + +这里的 *this* 仍然指向 *window*。虽然 *fn* 函数在 *foo* 对象中作为该对象的一个方法,但是在赋值给 *fn1* 之后,*fn1* 仍然是在 *window* 的全局环境下执行的。因此上面的代码仍然会输出 *window* 和 *undefined*。 + +还是上面这道题目,如果改成如下的形式 + +例题 *3*: + +```js +const foo = { + bar : 10, + fn : function(){ + console.log(this); // { bar: 10, fn: [Function: fn] } + console.log(this.bar); // 10 + } +} +foo.fn(); +``` + +这时,*this* 指向的是最后调用它的对象,在 *foo.fn( )* 语句中,this 指向的是 *foo* 对象。 + + + +### 上下文对象调用中的 *this* + + + +例题 *4*: + +```js +const student = { + name: 'zhangsan', + fn: function () { + return this; + } +} +console.log(student.fn() === student); // true +``` + +在上面的代码中,*this* 指向当前的对象 *student*,所以最终会返回 *true*。 + +当存在更复杂的调用关系时,如以下代码中的嵌套关系,*this* 将指向最后调用它的对象,例如 + + + +例题 *5*: + +```js +const student = { + name: 'zhangsan', + son: { + name: 'zhangxiaosan', + fn: function () { + return this.name + } + } +} +console.log(student.son.fn()); // zhangxiaosan +``` + +在上面的代码中,*this* 会指向最后调用它的对象,因此输出的是 *zhangxiaosan*。 + +至此,*this* 的上下文对象调用已经介绍得比较清楚了。我们再来看一道比较高阶的题目 + +例题 *6*: + +```js +const o1 = { + text: 'o1', + fn: function () { + return this.text; + } +} + +const o2 = { + text: 'o2', + fn: function () { + return o1.fn(); + } +} + +const o3 = { + text: 'o3', + fn: function () { + var fn = o1.fn; + return fn(); + } +} + +console.log(o1.fn()); // o1 +console.log(o2.fn()); // o1 +console.log(o3.fn()); // undefined +``` + +答案是 *o1、o1、undefined*。 + +这里主要讲一下为什么第三个是 *undefined*。这里将 *o1.fn* 赋值给了 *fn*,所以 *fn* 等价于 *function () { return this.text; }*,然后该函数在调用的时候,是直接 *fn( )* 的形式调用的,并不是以对象的形式,相当于还是全局调用,指向 *window*,所以打印出 *undefined*。 + + + +### *this* 指向绑定事件的元素 + + + +*DOM* 元素绑定事件时,事件处理函数里面的 *this* 指向绑定了事件的元素。 + +这个地方一定要注意它和 *target* 的区别,*target* 是指向触发事件的元素。 + +示例如下: + +```html + +``` + +```js +// this 是绑定事件的元素 +// target 是触发事件的元素 和 srcElememnt 等价 +let colorList = document.getElementById("color-list"); +colorList.addEventListener("click", function (event) { + console.log('this:', this); + console.log('target:', event.target); + console.log('srcElement:', event.srcElement); +}) +``` + +当我点击如下位置时打印出来的信息如下: + +image-20210928113303839 + + + +有些时候我们会遇到一些困扰,比如在 *div* 节点的事件函数内部,有一个局部的 *callback* 方法,该方法被作为普通函数调用时,*callback* 内部的 *this* 是指向全局对象 *window* 的 + +例如: + +```html +
我是一个div
+``` + +```js +window.id = 'window'; +document.getElementById('div1').onclick = function(){ + console.log(this.id); // div1 + const callback = function(){ + console.log(this.id); // 因为是普通函数调用,所以 this 指向 window + } + callback(); +} +``` + +此时有一种简单的解决方案,可以用一个变量保存 *div* 节点的引用,如下: + +```js +window.id = 'window'; +document.getElementById('div1').onclick = function(){ + console.log(this.id); // div1 + const that = this; // 保存当前 this 的指向 + const callback = function(){ + console.log(that.id); // div1 + } + callback(); +} +``` + + + +### 改变 *this* 指向 + + + +#### 1. *call、apply、bind* 方法修改 *this* 指向 + + + +由于 *JavaScript* 中 *this* 的指向受函数运行环境的影响,指向经常改变,使得开发变得困难和模糊,所以在封装 *sdk* 或者写一些复杂函数的时候经常会用到 *this* 指向绑定,以避免出现不必要的问题。 + +*call、apply、bind* 基本都能实现这一功能,起到确定 *this* 指向的作用 + + + +***Function.prototype.call( )*** + + + +*call* 方法可以指定 *this* 的指向(即函数执行时所在的的作用域),然后再指定的作用域中,执行函数。 + + + +```js +var obj = {}; +var f = function(){ + return this; +}; +console.log(f() === window); // this 指向 window +console.log(f.call(obj) === obj) // 改变this 指向 obj +``` + +上面代码中,全局环境运行函数 *f* 时,*this* 指向全局环境(浏览器为 *window* 对象); + +*call* 方法可以改变 *this* 的指向,指定 *this* 指向对象 *obj*,然后在对象 *obj* 的作用域中运行函数 *f*。 + + + +*call* 方法的参数,应该是对象 *obj*,如果参数为空或 *null、undefind*,则默认传参全局对象。 + +```js +var n = 123; +var obj = { n: 456 }; + +function a() { + console.log(this.n); +} + +a.call() // 123 +a.call(null) // 123 +a.call(undefined) // 123 +a.call(window) // 123 +a.call(obj) // 456 +``` + +上面代码中,*a* 函数中的 *this* 关键字,如果指向全局对象,返回结果为 *123*。 + +如果使用 *call* 方法将 *this* 关键字指向 *obj* 对象,返回结果为 *456*。可以看到,如果 *call* 方法没有参数,或者参数为 *null* 或 *undefined*,则等同于指向全局对象。 + + + +如果 *call* 传参不是以上类型,则转化成对应的包装对象,然后传入方法。 + +例如,*5* 转成 *Number* 实例,绑定 *f* 内部 *this* + +```js +var f = function () { + return this; +}; + +f.call(5); // Number {[[PrimitiveValue]]: 5} +``` + +*call* 可以接受多个参数,第一个参数是 *this* 指向的对象,之后的是函数回调所需的参数。 + +```js +function add(a, b) { + return a + b; +} + +add.call(this, 1, 2) // 3 +``` + +*call* 方法的一个应用是调用对象的原生方法。 + +```js +var obj = {}; +obj.hasOwnProperty('toString') // false + +// 覆盖掉继承的 hasOwnProperty 方法 +obj.hasOwnProperty = function () { + return true; +}; +obj.hasOwnProperty('toString') // true + +Object.prototype.hasOwnProperty.call(obj, 'toString') // false +``` + +上面代码中 *hasOwnProperty* 是 *obj* 继承来的方法,用来判断对象是否包含自身特点(非继承)属性,但是 *hasOwnProperty* 并不是保留字,如果被对象覆盖,会造成结果错误。 + +*call* 方法可以解决这个问题,它将 *hasOwnProperty* 方法的原始定义放到 *obj* 对象上执行,这样无论 *obj* 上有没有同名方法,都不会影响结果。 + + + +***Function.prototype.apply( )*** + + + +*apply* 和 *call* 作用类似,也是改变 *this* 指向,然后调用该函数,唯一区别是 *apply* 接收数组作为函数执行时的参数。语法如下: + +```js +func.apply(thisValue, [arg1, arg2, ...]) +``` + +*apply* 方法的第一个参数也是 *this* 所要指向的那个对象,如果设为 *null* 或 *undefined*,则等同于指定全局对象。 + +第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。 + +原函数的参数,在 *call* 方法中必须一个个添加,但是在 *apply* 方法中,必须以数组形式添加。 + +```js +function f(x, y){ + console.log(x + y); +} + +f.call(null, 1, 1) // 2 +f.apply(null, [1, 1]) // 2 +``` + +利用这一特性,可以实现很多小功能。比如,输出数组的最大值: + +```js +var a = [24,30,2,33,1] +Math.max.apply(null,a) //33 +``` + +还可以将数组中的空值,转化成 *undefined*。 + +通过 *apply* 方法,利用 *Array* 构造函数将数组的空元素变成 *undefined*。 + +```js +var a = ['a',,'b']; +Array.apply(null,a) //['a',undefind,'b'] +``` + +空元素与 *undefined* 的差别在于,数组的 *forEach* 方法会跳过空元素,但是不会跳过 *undefined*。因此,遍历内部元素的时候,会得到不同的结果。 + +```js +var a = ['a', , 'b']; + +function print(i) { + console.log(i); +} + +a.forEach(print) +// a +// b + +Array.apply(null, a).forEach(print) +// a +// undefined +// b +``` + +配合数组对象的 *slice* 方法,可以将一个类似数组的对象(比如 *arguments* 对象)转为真正的数组。 + +```js +Array.prototype.slice.apply({0: 1, length: 1}) // [1] +Array.prototype.slice.apply({0: 1}) // [] +Array.prototype.slice.apply({0: 1, length: 2}) // [1, undefined] +Array.prototype.slice.apply({length: 1}) // [undefined] +``` + +上面代码的 *apply* 方法的参数都是对象,但是返回结果都是数组,这就起到了将对象转成数组的目的。 + +从上面代码可以看到,这个方法起作用的前提是,被处理的对象必须有 *length* 属性,以及相对应的数字键。 + + + +***Function.prototype.bind( )*** + + + +*bind* 用于将函数体内的 *this* 绑定到某个对象,然后返回一个新函数 + +```js +var d = new Date(); +d.getTime() // 1481869925657 + +var print = d.getTime; +print() // Uncaught TypeError: this is not a Date object. +``` + +报错是因为 *d.getTime* 赋值给 *print* 后,*getTime* 内部的 *this* 指向方式变化,已经不再指向 *date* 对象实例了。 + +解决方法: + +```js +var print = d.getTime.bind(d); +print() // 1481869925657 +``` + +*bind* 接收的参数就是所要绑定的对象 + +```js +var counter = { + count: 0, + inc: function () { + this.count++; + } +}; + +var func = counter.inc.bind(counter); +func(); +counter.count // 1 +``` + +绑定到其他对象 + +```js +var counter = { + count: 0, + inc: function () { + this.count++; + } +}; + +var obj = { + count: 100 +}; +var func = counter.inc.bind(obj); +func(); +obj.count // 101 +``` + +*bind* 还可以接收更多的参数,将这些参数绑定到原函数的参数 + +```js +var add = function (x, y) { + return x * this.m + y * this.n; +} + +var obj = { + m: 2, + n: 2 +}; + +var newAdd = add.bind(obj, 5); +newAdd(5) // 20 +``` + +上面代码中,*bind* 方法除了绑定 *this* 对象,还将 *add* 函数的第一个参数 *x* 绑定成 *5*,然后返回一个新函数 *newAdd*,这个函数只要再接受一个参数 *y* 就能运行了。 + + + +如果 *bind* 方法的第一个参数是 *null* 或 *undefined*,等于将 *this* 绑定到全局对象,函数运行时 *this* 指向顶层对象(浏览器为 *window*)。 + +```js +function add(x, y) { + return x + y; +} + +var plus5 = add.bind(null, 5); +plus5(10) // 15 +``` + +上面代码中,函数 *add* 内部并没有 *this*,使用 *bind* 方法的主要目的是绑定参数 *x*,以后每次运行新函数 *plus5*,就只需要提供另一个参数 *y* 就够了。 + +而且因为 *add* 内部没有 *this*,所以 *bind* 的第一个参数是 *null*,不过这里如果是其他对象,也没有影响。 + + + +*bind* 方法有一些使用注意点。 + +(1)每一次返回一个新函数 + +*bind* 方法每运行一次,就返回一个新函数,这会产生一些问题。比如,监听事件的时候,不能写成下面这样。 + +``` +element.addEventListener('click', o.m.bind(o)); +``` + +上面代码中,*click* 事件绑定 *bind* 方法生成的一个匿名函数。这样会导致无法取消绑定,所以,下面的代码是无效的。 + +```js +element.removeEventListener('click', o.m.bind(o)); +``` + +正确的方法是写成下面这样: + +```js +var listener = o.m.bind(o); +element.addEventListener('click', listener); +// ... +element.removeEventListener('click', listener); +``` + +(2)结合回调函数使用 + +回调函数是 *JavaScript* 最常用的模式之一,但是一个常见的错误是,将包含 *this* 的方法直接当作回调函数。解决方法就是使用 *bind* 方法,将 *counter.inc* 绑定 *counter*。 + +```js +var counter = { + count: 0, + inc: function () { + 'use strict'; + this.count++; + } +}; + +function callIt(callback) { + callback(); +} + +callIt(counter.inc.bind(counter)); +counter.count // 1 +``` + +上面代码中,*callIt* 方法会调用回调函数。这时如果直接把 *counter.inc* 传入,调用时 *counter.inc* 内部的 *this* 就会指向全局对象。使用 *bind* 方法将 *counter.inc* 绑定 *counter* 以后,就不会有这个问题,*this* 总是指向 *counter*。 + +还有一种情况比较隐蔽,就是某些数组方法可以接受一个函数当作参数。这些函数内部的 *this* 指向,很可能也会出错。 + +```js +var obj = { + name: '张三', + times: [1, 2, 3], + print: function () { + this.times.forEach(function (n) { + console.log(this.name); + }); + } +}; + +obj.print() +// 没有任何输出 +``` + +上面代码中,*obj.print* 内部 *this.times* 的 *this* 是指向 *obj* 的,这个没有问题。 + +但是,*forEach* 方法的回调函数内部的 *this.name* 却是指向全局对象,导致没有办法取到值。稍微改动一下,就可以看得更清楚。 + +```js +obj.print = function () { + this.times.forEach(function (n) { + console.log(this === window); + }); +}; + +obj.print() +// true +// true +// true +``` + +解决这个问题,也是通过 *bind* 方法绑定 *this*。 + +```js +obj.print = function () { + this.times.forEach(function (n) { + console.log(this.name); + }.bind(this)); +}; + +obj.print() +// 张三 +// 张三 +// 张三 +``` + +(3)结合 *call* 方法使用 + +利用 *bind* 方法,可以改写一些 *JavaScript* 原生方法的使用形式,以数组的 *slice* 方法为例。 + +```js +[1, 2, 3].slice(0, 1) // [1] +// 等同于 +Array.prototype.slice.call([1, 2, 3], 0, 1) // [1] +``` + +上面的代码中,数组的 *slice* 方法从 *[1, 2, 3]* 里面,按照指定位置和长度切分出另一个数组。这样做的本质是在 *[1, 2, 3]* 上面调用 *Array.prototype.slice* 方法,因此可以用 *call* 方法表达这个过程,得到同样的结果。 + +*call* 方法实质上是调用 *Function.prototype.call* 方法,因此上面的表达式可以用 *bind* 方法改写。 + +```js +var slice = Function.prototype.call.bind(Array.prototype.slice); +slice([1, 2, 3], 0, 1) // [1] +``` + +上面代码的含义就是,将 *Array.prototype.slice* 变成 *Function.prototype.call* 方法所在的对象,调用时就变成了 *Array.prototype.slice.call*。类似的写法还可以用于其他数组方法。 + +```js +var push = Function.prototype.call.bind(Array.prototype.push); +var pop = Function.prototype.call.bind(Array.prototype.pop); + +var a = [1 ,2 ,3]; +push(a, 4) +a // [1, 2, 3, 4] + +pop(a) +a // [1, 2, 3] +``` + +如果再进一步,将 *Function.prototype.call* 方法绑定到 *Function.prototype.bind* 对象,就意味着 *bind* 的调用形式也可以被改写。 + +```js +function f() { + console.log(this.v); +} + +var o = { v: 123 }; +var bind = Function.prototype.call.bind(Function.prototype.bind); +bind(f, o)() // 123 +``` + +上面代码的含义就是,将 *Function.prototype.bind* 方法绑定在 *Function.prototype.call* 上面,所以 *bind* 方法就可以直接使用,不需要在函数实例上使用。 + + + +#### 2. 箭头函数的 *this* 指向 + + + +当我们的 *this* 是以函数的形式调用时,*this* 指向的是全局对象。 + +不过对于箭头函数来讲,却比较特殊。箭头函数的 *this* 指向始终为外层的作用域。 + +先来看一个普通函数作为对象的一个方法被调用时,*this* 的指向,如下: + +```js +const obj = { + x : 10, + test : function(){ + console.log(this); // 指向 obj 对象 + console.log(this.x); // 10 + } +} +obj.test(); +// { x: 10, test: [Function: test] } +// 10 +``` + +可以看到,普通函数作为对象的一个方法被调用时,*this* 指向当前对象。 + +在上面的例子中,就是 *obj* 这个对象,*this.x* 的值为 *10*。 + + + +接下来是箭头函数以对象的方式被调用的时候的 *this* 的指向,如下: + +```js +var x = 20; +const obj = { + x: 10, + test: () => { + console.log(this); // {} + console.log(this.x); // undefined + } +} +obj.test(); +// {} +// undefined +``` + +这里的结果和上面不一样,*this* 打印出来为 { },而 *this.x* 的值为 *undefined*。 + +为什么呢? + +实际上刚才我们有讲过,箭头函数的 *this* 指向与普通函数不一样,它的 *this* 指向始终是指向的外层作用域。所以这里的 *this* 实际上是指向的全局对象。 + +如果证明呢? + +方法很简单,将这段代码放入浏览器运行,在浏览器中用 *var* 所声明的变量会成为全局对象 *window* 的一个属性,如下: + +image-20210928132058878 + + + +接下来我们再来看一个例子,来证明箭头函数的 *this* 指向始终是指向的外层作用域。 + +```js +var name = "JavaScript"; +const obj = { + name: "PHP", + test: function () { + const i = function () { + console.log(this.name); + // i 是以函数的形式被调用的,所以 this 指向全局 + // 在浏览器环境中打印出 JavaScript,node 里面为 undeifned + } + i(); + } +} +obj.test(); // JavaScript +``` + +接下来我们将 i 函数修改为箭头函数,如下: + +```js +var name = "JavaScript"; +const obj = { + name : "PHP", + test : function(){ + const i = ()=>{ + console.log(this.name); + // 由于 i 为一个箭头函数,所以 this 是指向外层的 + // 所以 this.name 将会打印出 PHP + } + i(); + } +} +obj.test();// PHP +``` + +最后需要说一点的就是,箭头函数不能作为构造函数,如下: + +```js +const Test = (name, age) => { + this.name = name; + this.age = age; +}; +const test = new Test("xiejie", 18); +// TypeError: Test is not a constructor +``` + + + +## 真题解答 + + + +- *this* 的指向哪几种 ? + +> 参考答案: +> +> 总结起来,*this* 的指向规律有如下几条: +> +> - 在函数体中,非显式或隐式地简单调用函数时,在严格模式下,函数内的 *this* 会被绑定到 *undefined* 上,在非严格模式下则会被绑定到全局对象 *window/global* 上。 +> - 一般使用 *new* 方法调用构造函数时,构造函数内的 *this* 会被绑定到新创建的对象上。 +> - 一般通过 *call/apply/bind* 方法显式调用函数时,函数体内的 *this* 会被绑定到指定参数的对象上。 +> - 一般通过上下文对象调用函数时,函数体内的 *this* 会被绑定到该对象上。 +> - 在箭头函数中,*this* 的指向是由外层(函数或全局)作用域来决定的。 + + + +-*EOF*- \ No newline at end of file diff --git a/10. 闭包/闭包.js b/10. 闭包/闭包.js new file mode 100644 index 0000000..bcd367f --- /dev/null +++ b/10. 闭包/闭包.js @@ -0,0 +1,80 @@ +// 外部函数 +// function eat(){ +// var food = "鸡翅"; +// return function(){ +// console.log(food); +// } +// } +// // 执行到第 8 行代码时,形成了一个闭包(封闭的空间) +// // 在该闭包中,引用了 food 这个变量 +// // 所以 food 是没有被销毁的 +// var i = eat(); +// i(); + +// 作用域是在函数创建的时候就确定下来了 +// var food = "rice"; +// // function eat(){ +// // console.log(food); +// // } +// (function(){ +// var food = "noodle"; +// function eat(){ +// console.log(food); +// } +// eat(); +// })() + +// 这里就是一个普通的计数器 +// var count = 0; // 全局变量 +// function counter() { +// count++; +// console.log(count); +// } +// for (var i = 0; i < 10; i++) { +// counter(); +// } +// 上面的代码有一个最大的问题,就是全局污染 + +// ... +// var count = 5; +// console.log(count); + +// 接下来我们使用闭包来解决上面的全局污染的问题 +// function counter() { +// var count = 0; // 计数器变量 +// return function(){ +// count++; +// console.log(count); +// } +// } +// var func = counter(); +// for (var i = 0; i < 10; i++) { +// func(); +// } + +// var count = "aaa"; + + +// 下面这段代码也是一个闭包 +// 一个函数中要嵌套一个内部函数,并且内部函数要访问外部函数的变量 +// 内部函数要被外部引用 +// var a = 100; +// setTimeout(function () { +// console.log(++a); +// }, 1000); + +// 有些时候因为闭包所存在的问题 +for (let i = 1; i <= 3; i++) { + setTimeout(function () { + console.log(i); + }, 1000); +} + + +// for (var i = 1; i <= 3; i++) { +// (function (i) { +// setTimeout(function () { +// console.log(i); +// }, 1000); +// })(i); +// } diff --git a/10. 闭包/闭包.md b/10. 闭包/闭包.md new file mode 100644 index 0000000..d908133 --- /dev/null +++ b/10. 闭包/闭包.md @@ -0,0 +1,430 @@ +# 闭包 + + + +## 经典真题 + + + +- 闭包是什么?闭包的应用场景有哪些?怎么销毁闭包? + + + +## 为什么需要闭包 + + + +首先我们来看一下为什么需要闭包。先看下嘛的例子: + +```js +function eat(){ + var food = "鸡翅"; + console.log(food); +} +eat(); // 鸡翅 +console.log(food); // 报错 +``` + +在上面的例子中,我们声明了一个名为 *eat* 的函数,并对它进行调用。 + +*JavaScript* 引擎会创建一个 *eat* 函数的执行上下文,其中声明 *food* 变量并赋值。 + +当该方法执行完后,上下文被销毁,*food* 变量也会跟着消失。这是因为 *food* 变量属于 *eat* 函数的局部变量,它作用于 *eat* 函数中,会随着 *eat* 的执行上下文创建而创建,销毁而销毁。所以当我们再次打印 *food* 变量时,就会报错,告诉我们该变量不存在。 + + + +那么,如何在函数销毁后也能继续使用变量 *food* 呢? + +这就涉及到了要使用闭包。 + + + +## 什么是闭包 + + + +要解释闭包,可以从**广义**和**狭义**上去理解。 + + + +- 广义上的闭包:所有的函数就是闭包。 +- 狭义上的闭包:需要满足两个条件。 + - 一个函数中要嵌套一个内部函数,并且内部函数要访问外部函数的变量 + - 内部函数要被外部引用 + + + +关于广义上闭包的含义,估计很多人很难理解,我就正常写个函数,怎么这玩意儿就变成闭包了? + +关于这一点,我们稍后再来解释。 + +我们先来看一下狭义上的闭包。 + +```js +function eat(){ + var food = '鸡翅'; + return function(){ + console.log(food); + } +} +var look = eat(); +look(); // 鸡翅 +look(); // 鸡翅 +``` + +在这个例子中,*eat* 函数返回一个函数,并在这个内部函数中访问 *food* 这个局部变量。调用 *eat* 函数并将结果赋给 *look* 变量,这个 *look* 指向了 *eat* 函数中的内部函数,然后调用它,最终输出 *food* 的值。 + +按照之前的说法,这个 *food* 变量应该当 *eat* 函数调用完后就销毁,后续为什么还能通过调用 *look* 方法访问到这个变量呢? + +这就是因为闭包起了作用。返回的内部函数和它外部的变量 *food* 实际上就是一个闭包。 + +闭包的实质,就是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使离开了创造它的环境也不例外。 + + + +这里提到了自由变量,它又是什么呢? + +**自由变量可以理解成跨作用域的变量,比如子作用域访问父作用域的变量。** + +如下代码中,*console.log(a)* 要得到 *a* 变量,但是在当前的作用域中没有定义 *a*(可对比一下 *b*)。当前作用域没有定义的变量,这成为自由变量 。 + +```js +var a = 100 +function fn() { + var b = 200 + console.log(a) // 这里的 a 就是一个自由变量,需要顺着作用域链来查找 a 变量的值 + console.log(b) +} +fn() +``` + + + +## 闭包的原理 + + + +接下来,我们来看一下闭包的原理。 + +要解释闭包的原理,这里需要回答 *2* 个问题。 + +**(1)为什么函数内部可以访问外部函数的变量?** + +原因很简单,当一个函数上下文产生的时候,会确定 *3* 个东西:变量对象、作用域链条以及 *this* 指向。 + +正因为有作用域链的存在,所以能够通过作用域链来访问到外部函数的变量。 + + + +**(2)为什么当外部函数的上下文执行完以后,其中的局部变量还是能通过闭包访问到呢?** + +其实用上一个问题的答案再延伸一下,这个问题的答案就出来了。 + +在介绍作用域的时候,我们有介绍过作用域是在函数创建的时候就确定下来了(参阅《作用域》章节)。 + +所以即使外部函数的上下文结束了,但内部的函数只要不销毁(被外部引用了,就不会销毁),就会一直引用着刚才上下文的作用域链对象,那么包含在作用域链中的变量也就可以一直被访问到。 + + + +综上所述,闭包其实就是利用到了作用域链的知识。 + + + +把这个理解了,闭包的原理也就明白了。 + +那么为什么说每一个函数都是一个闭包呢? + +因为每一个函数都能通过作用域链访问到全局上下文中的变量,例如: + +```js +var stuName = "zhangsan"; +function test(){ + console.log(stuName); +} +test(); +``` + +在上面的代码中,我们在 *test* 函数中访问了自由变量 *stuName*,这个被引用的自由变量将和这个函数一同存在。 + + + +## 闭包的优缺点 + + + +**闭包的优点** + +先来看看闭包的优点,主要有以下 *2* 点: + +- 通过闭包可以让外部环境访问到函数内部的局部变量。 +- 通过闭包可以让局部变量持续保存下来,不随着它的上下文环境一起销毁。 + +看下面这个例子: + +```javascript +var count = 0; // 全局变量 +function compute() { // 将计数器加 1 + count++; + console.log(count); +} +for (var i = 0; i < 100; i++) { + compute(); // 循环 100 次 +} +``` + +这个例子是对一个全局变量进行加 *1* 的操作,一共加 *100* 次,得到值为 *100* 的结果。 + +但是因为使用了全局变量,所以存在全局变量污染的问题。 + +下面用闭包的方式重构它: + +```javascript +function compute() { + var count = 0; // 局部变量 + return function () { + count++; // 内部函数访问外部变量 + console.log(count); + } +} +var func = compute(); // 引用了内部函数,形成闭包 +for (var i = 0; i < 100; i++) { + func(); +} +// 在外面新创建一个 count 的变量,完全不冲突 +var count = "Hello"; +console.log(count); +for (var i = 0; i < 100; i++) { + func(); +} +``` + +这个例子就不再使用全局变量,其中 *count* 这个局部变量依然可以被保存下来。我们甚至可以在外面新创建一个 *count* 变量,完全不会和内部的 *count* 变量产生冲突。 + + + +**闭包的缺点** + +说完闭包的优点,接下来来看一下闭包的缺点。 + +局部变量本来应该在函数退出时被解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就能一直生存下去。也就是说,闭包会将局部变量保存下来。如果大量使用闭包,而其中的变量又未得到清理,闭包的确会使一些数据无法被及时销毁,从而造成内存泄漏。 + +但是使用闭包的一部分原因,是我们选择主动把一些变量封闭在闭包中,因为可能在以后还需要使用这些变量。 + +把这些变量放在闭包中和放在全局作用域中,对内存方面的影响是一样的,所以这里并不能说成是内存泄漏。如果在将来需要回收这些变量,我们可以手动把这些变量设置为 *null*。 + + + +如果要说闭包和内存泄漏有关系的地方,那就是使用闭包的同时比较容易形成循环引用,如果闭包的作用域中保存着一些 *DOM* 节点,这个时候就有可能造成内存泄漏。 + +但这本身并非闭包的问题,也并非 *JavaScript* 的问题。在 *IE* 浏览器中,由于 *BOM* 和 *DOM* 中的对象是使用 *C++* 以 *COM* 对象的方式实现的,而 *COM* 对象的垃圾收集机制采用的是引用计数策略。在基于引用计数策略的垃圾回收机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收,但循环引用造成的内存泄漏在本质上也不是闭包造成的。 + +同样,如果要解决循环引用带来的内存泄漏问题,我们只需要把循环引用中的变量设为 *null* 即可。将变量设置为 *null* 意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。 + + + +## 闭包的应用场景 + + + +最后,我们来看一下闭包的一些实际的应用场景。 + + + +```js +var a = 100; +setTimeout(function () { + console.log(++a); +}, 1000); +``` + +上面是一段很简单的代码,但是这实际上就在你毫无察觉的情况下使用用到了闭包。 + +在这个例子中,用到了时间函数 *setTimeout*,并在等待 *1* 秒钟后对变量 *a* 进行加 *1* 的操作。 + +之所以说这是闭包,是因为 *setTimeout* 中的匿名函数对外部变量(自由变量)进行访问,然后该函数又被 *setTimeout* 方法引用。满足了形成闭包的两个条件。所以你看,即使外部上下文结束了,*1* 秒后仍然能对变量 *a* 进行加 *1* 操作。 + + + +在 *DOM* 的事件操作中,也经常用到闭包,比如下面这个例子: + +```html + +``` + +```js +(function(){ + var cnt = 0; // 计数器 + var count = document.getElementById("count"); + count.onclick = function(){ + console.log(++cnt); + } +})() +``` + +*onclick* 指向的函数中访问了外部变量 *cnt*,同时该函数又被 *onclick* 事件引用了,满足 *2* 个条件,是闭包。 + +所以当外部上下文结束后,你继续点击按钮,在触发的事件处理方法中仍然能访问到变量 cnt。 + +再比如,*img* 对象经常用于进行数据上报,如下所示: + +```js +const report = function (src) { + var img = new Image(); + img.src = src; +} +report('http://xxx.com/getUserInfo'); +``` + +但是通过查询后台的记录我们得知,因为一些低版本的浏览器的实现存在 *bug*,在这些浏览器下使用 *report* 函数进行数据上报时会丢失 *30%* 左右的数据,也就是说,*report* 函数并不是每一次都成功发起了 *HTTP* 请求。 + +丢失数据的原因是 *img* 是 *report* 函数中的局部变量,当 *report* 函数在调用结束后,*img* 局部变量随即被销毁,而此时或许还没来得及发出 *HTTP* 请求,所以此次请求就会丢失掉。 + +现在我们把 *img* 变量用闭包封闭起来,便能解决请求丢失的问题,如下: + +```js +const report = (function () { + var imgs = []; + return function (src) { + var img = new Image(); + imgs.push(img); + img.src = src; + } +})(); +``` + + + +在有些时候,闭包还会引起一些奇怪的问题,比如下面这个例子: + +```js +for (var i = 1; i <= 3; i++) { + setTimeout(function () { + console.log(i); + }, 1000); +} +``` + +我们预期的结果是过 *1* 秒后分别输出 *i* 变量的值为 *1,2,3*。但是,执行的结果是:*4,4,4*。 + +实际上,问题就出在闭包身上。你看,循环中的 *setTimeout* 访问了它的外部变量 *i*,形成闭包。 + +而 *i* 变量只有 *1* 个,所以循环 *3* 次的 *setTimeout* 中都访问的是同一个变量。循环到第 *4* 次,*i* 变量增加到 *4*,不满足循环条件,循环结束,代码执行完后上下文结束。但是,那 *3* 个 *setTimeout* 等 *1* 秒钟后才执行,由于闭包的原因,所以它们仍然能访问到变量 *i*,不过此时 *i* 变量值已经是 *4* 了。 + +既然是闭包引起的问题,那么解决的方法就是去掉闭包。 + +我们知道形成闭包有两个条件,只要不满足其一,那就不再是闭包。 + +条件之一,内部函数被外部引用,这个我们没办法去掉。条件二,内部函数访问外部变量。这个条件我们有办法去掉,比如: + +```js +for (var i = 1; i <= 3; i++) { + (function (index) { + setTimeout(function () { + console.log(index); + }, 1000); + })(i) +} +``` + +这样 *setTimeout* 中就可以不用访问 *for* 循环声明的变量 *i* 了。而是采用调用函数传参的方式把变量 *i* 的值传给了 *setTimeout*,这样它们就不再形成闭包。也就是说 *setTimeout* 中访问的已经不是外部的变量 *i*,所以即使 *i* 的值增长到 *4*,跟它内部也没关系,最后达到了我们想要的效果。 + +当然,解决这个问题还有个更简单的方法,就是使用 *ES6* 中的 *let* 关键字。 + +它声明的变量有块作用域,如果将它放在循环中,那么每次循环都会有一个新的变量 *i*,这样即使有闭包也没问题,因为每个闭包保存的都是不同的 *i* 变量,那么刚才的问题也就迎刃而解。 + +```js +for (let i = 1; i <= 3; i++) { + setTimeout(function () { + console.log(i); + }, 1000); +} +``` + + + +另外,使用闭包还可以模拟出面向对象中的私有方法。 + +过程与数据的结合是形容面向对象中的“对象”时经常使用的表达。 + +对象以属性的形式包含了数据,以方法的形式包含了过程。 + +而闭包则是在过程中以环境的形式包含了数据。通常用面向对象思想能实现的功能,用闭包也能够实现,反之亦然。 + +在 *JavaScript* 语言的祖先 *Scheme* 语言中,甚至都没有提供面向对象的原生设计,但却可以使用闭包来实现一个完整的面向对象的系统。 + +下面我们来看看这段跟闭包相关的代码: + +```js +function Test(){ + var value = 0; // 相当于是对象的属性 + return { + call : function(){ + value++; + console.log(value); + } + } +} +const test = new Test(); +test.call(); // 1 +test.call(); // 2 +test.call(); // 3 +``` + +如果换成面向对象的写法,那就是如下: + +```js +const test = { + value: 0, + call: function () { + this.value++; + console.log(this.value); + } +} +test.call(); // 1 +test.call(); // 2 +test.call(); // 3 +``` + +或者 + +```js +function Test() { + this.value = 0; +} +Test.prototype.call = function () { + this.value++; + console.log(this.value); +} +const test = new Test(); +test.call(); // 1 +test.call(); // 2 +test.call(); // 3 +``` + + + +## 真题解答 + + + +- 闭包是什么?闭包的应用场景有哪些?怎么销毁闭包? + +> 参考答案: +> +> 闭包是指有权访问另外一个函数作用域中的变量的函数。 +> +> 因为闭包引用着另一个函数的变量,导致另一个函数已经不使用了也无法销毁,所以**闭包使用过多,会占用较多的内存,这也是一个副作用,内存泄漏。** +> +> 如果要销毁一个闭包,可以 把被引用的变量设置为 *null*,即手动清除变量,这样下次 *JS* 垃圾回收机制回收时,就会把设为 *null* 的量给回收了。 +> +> 闭包的应用场景: +> +> 1. 匿名自执行函数 +> 2. 结果缓存 +> 3. 封装 +> 4. 实现类和继承 +> 5. 解决全局污染 + + + +-*EOF*- \ No newline at end of file diff --git a/11. DOM事件的注册和移除/DOM 事件的注册和移除.html b/11. DOM事件的注册和移除/DOM 事件的注册和移除.html new file mode 100644 index 0000000..666c1ee --- /dev/null +++ b/11. DOM事件的注册和移除/DOM 事件的注册和移除.html @@ -0,0 +1,40 @@ + + + + + + + + Document + + + + + + + + \ No newline at end of file diff --git a/11. DOM事件的注册和移除/DOM 事件的注册和移除.md b/11. DOM事件的注册和移除/DOM 事件的注册和移除.md new file mode 100644 index 0000000..475021c --- /dev/null +++ b/11. DOM事件的注册和移除/DOM 事件的注册和移除.md @@ -0,0 +1,199 @@ +# *DOM* 事件的注册和移除 + + + +## 经典真题 + + + +- 总结一下 *DOM* 中如何注册事件和移除事件 + + + +## *DOM* 注册事件 + + + +使用 *JavaScript* 为 *DOM* 元素注册事件的方式有多种。但是并不是一开始就设计了多种方式,而是随着技术的发展,发展前一种方式有所缺陷,所以设计了新的 *DOM* 元素注册事件的方式。 + + + +这里我们就一起来总结一下 *DOM* 中注册事件的方式有哪些。 + + + +### *HTML* 元素中注册事件 + + + +*HTML* 元素中注册的事件,又被称之为行内事件监听器。这是在浏览器中处理事件最原始的方法。 + +具体的示例如下: + +```html + +``` + +```js +function test(name) { + console.log(`我知道你已经点击了,${name}`); +} +``` + +在上面的代码中,我们为 *button* 元素添加了 *onclick* 属性,然后绑定了一个名为 *test* 的事件处理器。 + +在 *JavaScript* 中只需要书写对应的 *test* 事件处理函数即可。 + +但是有一点需要注意,就是这种方法已经过时了,原因如下: + +- *JavaScript* 代码与 *HTML* 标记混杂在一起,破坏了结构和行为分离的理念。 +- 每个元素只能为每种事件类型绑定一个事件处理器。 +- 事件处理器的代码隐藏于标记中,很难找到事件是在哪里声明的。 + +但是如果是做简单的事件测试,那么这种写法还是非常方便快捷的。 + + + +### *DOM0* 级方式注册事件 + + + +这种方式是首先取到要为其绑定事件的元素节点对象,然后给这些节点对象的事件处理属性赋值一个函数。 + +这样就可以达到 *JavaScript* 代码和 *HTML* 代码相分离的目的。 + +具体的示例如下: + +```html + +``` + +```js +var test = document.getElementById("test"); +test.onclick = function(){ + console.log("this is a test"); +} +``` + +这种方式虽然相比 *HTML* 元素中注册事件有所改进,但是它也有一个缺点,那就是它依然存在每个元素只能绑定一个函数的局限性。 + +下面我们尝试使用这种方式为同一个元素节点绑定 *2* 个事件,如下: + +```js +var test = document.getElementById("test"); +test.onclick = function(){ + console.log("this is a test"); +} +test.onclick = function(){ + console.log("this is a test,too"); +} +``` + +当我们为该 *DOM* 元素绑定 *2* 个相同类型的事件时,后面的事件处理函数就会把前面的事件处理函数给覆盖掉。 + + + +### *DOM2* 级方式注册事件 + + + +*DOM2* 级再次对事件的绑定方式进行了改进。 + +*DOM2* 级通过 *addEventListener* 方法来为一个 *DOM* 元素添加多个事件处理函数。 + +该方法接收 *3* 个参数:事件名、事件处理函数、布尔值。 + +如果这个布尔值为 *true*,则在捕获阶段处理事件,如果为 *false*,则在冒泡阶段处理事件。若最后的布尔值不填写,则和 *false* 效果一样,也就是说默认为 *false*,在冒泡阶段进行事件的处理。 + + + +接下来我们来看下面的示例:这里我们为 *button* 元素绑定了 *2* 个事件处理程序,并且 *2* 个事件处理程序都是通过点击来触发。 + +```js +var test = document.getElementById("test"); +test.addEventListener("click", function () { + console.log("this is a test"); +}, false); +test.addEventListener("click", function () { + console.log("this is a test,too"); +}, false); +``` + +在上面的代码中,我们通过 *addEventListener* 为按钮绑定了 *2* 个点击的事件处理程序,*2* 个事件处理程序都会执行。 + +另外需要注意的是,在 *IE* 中和 *addEventListener* 方法与之对应的是 *attachEvent* 方法。 + + + +## *DOM* 移除事件 + + + +通过 *DOM0* 级来添加的事件,删除的方法很简单,只需要将 *DOM* 元素的事件处理属性赋值为 *null* 即可。 + +例如: + +```js +var test = document.getElementById("test"); +test.onclick = function(){ + console.log("this is a test"); + test.onclick = null; +} +``` + +在上面的代码中,我们通过 *DOM0* 级的方式为 *button* 按钮绑定了点击事件,但是在事件处理函数中又移除了该事件。所以该事件只会生效一次。 + + + +如果是通过 *DOM2* 级来添加的事件,我们可以使用 *removeEventLister* 方法来进行事件的删除。 + +需要注意的是,如果要通过该方法移除**某一类事件类型的一个事件**的话,在通过 *addEventListener* 来绑定事件时的写法就要稍作改变。 + +先单独将绑定函数写好,然后 *addEventListener* 进行绑定时第 *2* 个参数传入要绑定的函数名即可。 + +示例如下: + +```js +var test = document.getElementById("test"); +//DOM 2级添加事件 +function fn1() { + console.log("this is a test"); + test.removeEventListener("click", fn1); // 只删除第一个点击事件 +} +function fn2() { + console.log("this is a test,too"); +} +test.addEventListener("click", fn1, false); +test.addEventListener("click", fn2, false); +``` + +在上面的代码中,我们为 *button* 元素绑定了两个 *click* 事件,之后在第一个事件处理函数中,对 *fn1* 事件处理函数进行了移除。所以第一次点击时,*fn1* 和 *fn2* 都会起作用,之后因为 *fn1* 被移除,所以只会 *fn2* 有作用。 + + + +## 真题解答 + + + +- 总结一下 *DOM* 中如何注册事件和移除事件 + +> 参考答案: +> +> 注册事件的方式常见的有 *3* 种方式: +> +> - *HTML* 元素中注册的事件:这种方式又被称之为行内事件监听器。这是在浏览器中处理事件最原始的方法。 +> +> - *DOM0* 级方式注册事件:这种方式是首先取到要为其绑定事件的元素节点对象,然后给这些节点对象的事件处理属性赋值一个函数。 +> +> - *DOM2* 级方式注册事件:*DOM2* 级通过 *addEventListener* 方法来为一个 *DOM* 元素添加多个事件处理函数。 +> +> 该方法接收 *3* 个参数:事件名、事件处理函数、布尔值。 +> +> 如果这个布尔值为 *true*,则在捕获阶段处理事件,如果为 *false*,则在冒泡阶段处理事件。若最后的布尔值不填写,则和 *false* 效果一样,也就是说默认为 *false*,在冒泡阶段进行事件的处理。 +> +> 关于移除注册的事件,如果是 *DOM0* 级方式注册的事件,直接将值设置为 *null* 即可。如果是 *DOM2* 级注册的事件,可以使用 *removeEventListener* 方法来移除事件。 + + + +-*EOF*- + diff --git a/12. DOM事件的传播机制/DOM 事件的传播机制.html b/12. DOM事件的传播机制/DOM 事件的传播机制.html new file mode 100644 index 0000000..c87d137 --- /dev/null +++ b/12. DOM事件的传播机制/DOM 事件的传播机制.html @@ -0,0 +1,47 @@ + + + + + + + + Document + + + + + + + + \ No newline at end of file diff --git a/12. DOM事件的传播机制/DOM 事件的传播机制.md b/12. DOM事件的传播机制/DOM 事件的传播机制.md new file mode 100644 index 0000000..4bdd981 --- /dev/null +++ b/12. DOM事件的传播机制/DOM 事件的传播机制.md @@ -0,0 +1,316 @@ +# *DOM* 事件的传播机制 + + + +## 经典真题 + + + +- 谈一谈事件委托以及冒泡原理 + + + +## 事件与事件流 + + + +事件最早是在 *IE3* 和 *NetscapeNavigator2* 中出现的,当时是作为分担服务器运算负担的一种手段。 + +要实现和网页的互动,就需要通过 *JavaScript* 里面的事件来实现。 + +每次用户与一个网页进行交互,例如点击链接,按下一个按键或者移动鼠标时,就会触发一个事件。我们的程序可以检测这些事件,然后对此作出响应。从而形成一种交互。 + +这样可以使我们的页面变得更加的有意思,而不仅仅像以前一样只能进行浏览。 + + + +在早期拨号上网的年代,如果所有的功能都放在服务器端进行处理的话,效率是非常低的。 + +所以 *JavaScript* 最初被设计出来就是用来解决这些问题的。通过允许一些功能在客户端处理,以节省到服务器的往返时间。 + +*JavaScript* 中采用一个叫做事件监听器的东西来监听事件是否发生。这个事件监听器类似于一个通知,当事件发生时,事件监听器会让我们知道,然后程序就可以做出相应的响应。 + +通过这种方式,就可以避免让程序不断地去检查事件是否发生,让程序在等待事件发生的同时,可以继续做其他的任务。 + + + +### 事件流 + + + +当浏览器发展到第 *4* 代时(*IE4* 及 *Netscape4*),浏览器开发团队遇到了一个很有意思的问题:页面的哪一部分会拥有某个特定的事件? + +想象在一张纸上的一组同心圆。如果把手指放在圆心上,那么手指指向的不是一个圆,而是纸上的所有圆。 + +image-20211002174941387 + +好在两家公司的浏览器开发团队在看待浏览器事件方面还是一致的。 + +如果单击了某个按钮,他们都认为单击事件不仅仅发生在按钮上,甚至也单击了整个页面。 + +但有意思的是,*IE* 和 *Netscape* 开发团队居然提出了差不多是完全相反的事件流的概念。 + +*IE* 的事件流是事件冒泡流,而 *Netscape* 的事件流是事件捕获流。 + + + +### 事件冒泡流 + + + +*IE* 的事件流叫做事件冒泡(*event bubbling*),即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。 + +以下列 *HTML* 结构为例,来说明事件冒泡。如下: + +```html + + + + + Document + + +
+ + +``` + +如果单击了页面中的 *div* 元素,那么这个 *click* 事件沿 *DOM* 树向上传播,在每一级节点上都会发生,按照如下顺序进行传播: + +1. *div* +2. *body* +3. *html* +4. *document* + +所有现代浏览器都支持事件冒泡,但在具体实现在还是有一些差别。 + +*IE9、Firefox、Chrome、Safari* 将事件一直冒泡到 *window* 对象。 + + + +我们可以通过下面的代码,来查看文档具体的冒泡顺序,示例如下: + +```html +
+ +``` + +```js +// IE8 以下浏览器返回 div body html document +// 其他浏览器返回 div body html document window +reset.onclick = function () { + history.go(); +} +box.onclick = function () { + box.innerHTML += 'div\n'; +} +document.body.onclick = function () { + box.innerHTML += 'body\n'; +} +document.documentElement.onclick = function () { + box.innerHTML += 'html\n'; +} +document.onclick = function () { + box.innerHTML += 'document\n'; +} +window.onclick = function () { + box.innerHTML += 'window\n'; +} +``` + +在上面的示例中,我们为 *div* 以及它的祖先元素绑定了点击事件,由于事件冒泡的存在,当我们点击 *div* 时,所有祖先元素的点击事件也会被触发。 + +如下图所示: + +image-20211002172307085 + + + +### 事件捕获流 + + + +*Netscape Communicator* 团队提出的另一种事件流叫做事件捕获(*event captruing*)。 + +事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。 + +事件捕获的思想是在事件到达预定目标之前就捕获它。 + +以同样的 *HTML* 结构为例来说明事件捕获,如下: + +```html + + + + + Document + + +
+ + +``` + +在事件捕获过程中,*document* 对象首先接收到 *click* 事件,然后事件沿 *DOM* 树依次向下,一直传播到事件的实际目标,即 *div* 元素: + +1. *document* +2. *html* +3. *body* +4. *div* + +*IE9、Firefox、Chrome、Safari* 等现代浏览器都支持事件捕获,但是也是从 *window* 对象开始捕获。 + +下面我们来演示一个事件捕获流的示例: + +```html +
+ +``` + +```js +// IE8 以下浏览器不支持 +// 其他浏览器返回 window document html body div +reset.onclick = function () { + history.go(); +} +box.addEventListener('click', function () { + box.innerHTML += 'div\n' +}, true) +document.body.addEventListener('click', function () { + box.innerHTML += 'body\n'; +}, true); +document.documentElement.addEventListener('click', function () { + box.innerHTML += 'html\n'; +}, true); +document.addEventListener('click', function () { + box.innerHTML += 'document\n'; +}, true); +window.addEventListener('click', function () { + box.innerHTML += 'window\n'; +}, true); +``` + +在上面的示例中,我们为 *div* 以及它所有的祖先元素绑定了点击事件,使用的 *addEventListener* 的方式来绑定的事件,并将第 *2* 个参数设置为了 *true* 表示使用事件捕获的方式来触发事件。 + +效果如下图所示: + +image-20211002173549252 + + + +### 标准 *DOM* 事件流 + + + +*DOM* 标准采用的是**捕获 + 冒泡**的方式。 + +两种事件流都会触发 *DOM* 的所有对象,从 *document* 对象开始,也在 *document* 对象结束。 + +换句话说,起点和终点都是 *document* 对象(很多浏览器可以一直捕获 + 冒泡到 *window* 对象) + +*DOM* 事件流示意图: + + + +image-20211002174148423 + +*DOM* 标准规定事件流包括三个阶段:**事件捕获阶段**、**处于目标阶段**和**事件冒泡阶段**。 + +- **事件捕获阶段:**实际目标 *div* 在捕获阶段不会触发事件。捕获阶段从 *window* 开始,然后到 *document、html*,最后到 *body* 意味着捕获阶段结束。 + + + +- **处于目标阶段:**事件在 *div* 上发生并处理,但是本次事件处理会被看成是冒泡阶段的一部分。 + + + +- **冒泡阶段:**事件又传播回文档。 + + + +## 事件委托 + + + +上面介绍了事件冒泡流,事件冒泡一个最大的好处就是可以实现事件委托。 + +事件委托,又被称之为事件代理。在 *JavaScript* 中,添加到页面上的事件处理程序数量将直接关系到页面整体的运行性能。导致这一问题的原因是多方面的。 + +首先,每个函数都是对象,都会占用内存。内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 *DOM* 访问次数,会延迟整个页面的交互就绪时间。 + +对事件处理程序过多问题的解决方案就是事件委托。 + +事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。 + +例如,*click* 事件会一直冒泡到 *document* 层次。也就是说,我们可以为整个页面指定一个 *onclick* 事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。 + +举一个具体的例子,例如现在我的列表项有如下内容: + +```html + +``` + +如果我们想把事件监听器绑定到所有的 *li* 元素上面,这样它们被单击的时候就弹出一些文字,为此我们需要给每一个元素来绑定一个事件监听器。 + +虽然上面的例子中好像问题也不大,但是想象一下如果这个列表有 *100* 个元素,那我们就需要添加 *100* 个事件监听器,这个工作量还是很恐怖的。 + +这个时候我们就可以利用事件代理来帮助我们解决这个问题。 + +将事件监听器绑定到父元素 *ul* 上,这样即可对所有的 *li* 元素添加事件,如下: + +```js +var colorList = document.getElementById("color-list"); +colorList.addEventListener("click",function(){ + alert("Hello"); +}) +``` + +现在我们单击列表中的任何一个 *li* 都会弹出东西,就好像这些 *li* 元素就是 *click* 事件的目标一样。 + +并且如果我们之后再为这个 *ul* 添加新的 *li* 元素的话,新的 *li* 元素也会自动添加上相同的事件。 + +但是,这个时候也存在一个问题,虽然我们使用事件代理避免了为每一个 *li* 元素添加相同的事件,但是如果用户没有点击 *li*,而是点击的 *ul*,同样也会触发事件。 + +这也很正常,因为我们事件就是绑定在 *ul* 上面的。 + +此时我们可以对点击的节点进行一个小小的判断,从而保证用户只在点击 *li* 的时候才触发事件,如下: + +```js +var colorList = document.getElementById("color-list"); +colorList.addEventListener("click", function (event) { + if (event.target.nodeName === 'LI') { + alert('点击 li'); + } +}) +``` + + + +## 真题解答 + + + +- 谈一谈事件委托以及冒泡原理 + +> 参考答案: +> +> 事件委托,又被称之为事件代理。在 *JavaScript* 中,添加到页面上的事件处理程序数量将直接关系到页面整体的运行性能。导致这一问题的原因是多方面的。 +> +> 首先,每个函数都是对象,都会占用内存。内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 *DOM* 访问次数,会延迟整个页面的交互就绪时间。 +> +> 对事件处理程序过多问题的解决方案就是事件委托。 +> +> 事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,*click* 事件会一直冒泡到 *document* 层次。也就是说,我们可以为整个页面指定一个 *onclick* 事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。 +> +> 事件冒泡(*event bubbling*),是指事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。 + + + +-*EOF*- \ No newline at end of file diff --git a/13. 阻止事件的默认行为/阻止事件默认行为.html b/13. 阻止事件的默认行为/阻止事件默认行为.html new file mode 100644 index 0000000..ff7ea55 --- /dev/null +++ b/13. 阻止事件的默认行为/阻止事件默认行为.html @@ -0,0 +1,23 @@ + + + + + + + Document + + + 百度 + + + \ No newline at end of file diff --git a/13. 阻止事件的默认行为/阻止事件默认行为.md b/13. 阻止事件的默认行为/阻止事件默认行为.md new file mode 100644 index 0000000..ae79d81 --- /dev/null +++ b/13. 阻止事件的默认行为/阻止事件默认行为.md @@ -0,0 +1,170 @@ +# 阻止事件默认行为 + + + +## 经典真题 + + + +- 如何阻止默认事件? + + + +## 什么是默认行为 + + + +所谓默认行为,一般是指 *HTML* 元素所自带的行为。例如点击一个 *a* 元素表示的是跳转: + +```html +百度一下 +``` + +在上面的代码中,设置了 *a* 元素的 *href* 属性指向百度,当用户点击该 *a* 元素时,就会跳转至百度。 + + + +在例如: + +```html +
+``` + +上面的代码中我们书写了一个 *form* 元素,该元素有一个 *action* 属性,指的是表单内容要提交的地址。而当用户点击表单元素中嵌套的提交按钮时,就会进行一个默认的提交操作。 + + + +这些,就是 *HTML* 元素中的默认行为。 + + + +但是有些时候,我们是不需要这些默认行为的,例如,用户在填写了一个表单后,提交信息时我们采用 *ajax* 来异步发送到服务器,此时就不需要表单 *form* 元素默认的提交跳转这个行为了。 + + + +所以此时,我们就需要阻止默认行为。 + + + +## 阻止默认行为的方式汇总 + + + +下面我们来对阻止默认行为的方式进行一个总结。 + + + +**(1)*cancelable* 属性** + +首先要介绍的是 *cancelable* 属性,该属性返回一个布尔值,表示事件是否可以取消。 + +该属性为只读属性。返回 *true* 时,表示可以取消。否则,表示不可取消。 + +```html +百度 +``` + +```js +var test = document.getElementById("test"); +test.onclick = function (event) { + test.innerHTML = event.cancelable; // true +} +``` + +在上面的代码中,我们为 *a* 元素绑定了一个点击事件,点击之后通过 *event* 对象的 *cancelable* 属性来查看该元素的默认行为是否能阻止。 + +最终返回的是 *true*,说明是能够阻止的。 + + + +**(2)*preventDefault* 方法** + +*preventDefault* 方法是 *DOM* 中最常见,也是最标准的取消浏览器默认行为的方式,无返回值。 + +```js +var test = document.getElementById("test"); +test.onclick = function(event){ + event.preventDefault(); +} +``` + +在上面的代码中,我们仍然是通过 *event* 对象来调用的 *preventDefault* 方法,从而阻止了 *a* 元素的默认跳转行为。 + + + +**(3)*returnValue* 属性** + +这种方式使用的人比较少,知道这种方式的人也比较少。 + +首先 *returnValue* 是一个 *event* 对象上面的属性。该属性可读可写,默认值是 *true*,将其设置为 *false* 就可以取消事件的默认行为,与 *preventDefault* 方法的作用相同。 + +该属性最早是在 *IE* 的事件对象中,实现了这种取消默认行为的方式,但是现在大多数浏览器都实现了该方式。 + +```js +var test = document.getElementById("test"); +test.onclick = function(event){ + event.returnValue = false; +} +``` + + + +**(4)*return false*** + +*return false* 是一条语句,该语句写在事件处理函数中也可以阻止默认行为。 + +但是需要注意的是,如果该条语句写在 *jQuery* 代码中,能够同时阻止默认行为和阻止冒泡,但是在原生 *JavaScript* 中只能阻止默认行为。 + +```js +var test = document.getElementById("test"); +test.onclick = function(){ + return false; +} +``` + + + +**(5)*defaultPrevented* 方法** + +*defaultPrevented* 属性也是 *event* 对象上面的一个属性。该属性表示默认行为是否被阻止,返回 *true* 表示被阻止,返回 *false* 表示未被阻止。 + +```js +var test = document.getElementById("test"); +test.onclick = function (event) { + // 采用两种不同的方式来阻止浏览器默认行为,这是为了照顾其兼容性 + if (event.preventDefault) { + event.preventDefault(); + } else { + event.returnValue = false; + } + // 将是否阻止默认行为的结果赋值给 标签的文本内容 + test.innerHTML = event.defaultPrevented; +} +``` + +在上面的代码中,我们点击 *a* 元素时,使用 *preventDefault* 方法阻止了浏览器默认行为。 + +之后访问 *event.defaultPrevented* 属性会得到 *true*,说明默认行为已经被阻止。 + + + +## 真题解答 + + + +- 如何阻止默认事件? + +> 参考答案: +> +> ```js +> // 方法一:全支持 +> event.preventDefault(); +> // 方法二:该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。 +> event.returnValue = false; +> // 方法三:不建议滥用,jQuery 中可以同时阻止冒泡和默认事件 +> return false; +> ``` + + + +-*EOF*- \ No newline at end of file diff --git a/14. 递归/递归.js b/14. 递归/递归.js new file mode 100644 index 0000000..9b56e68 --- /dev/null +++ b/14. 递归/递归.js @@ -0,0 +1,49 @@ +// 5! +// 5 * 4 * 3 * 2 *1 +// 10! +// 10 * 9 * 8 * 7 * 6 .... * 1 + +// factorial(10) ----> 10! +// factorial(5) ----> 5! + +// 委托 +// 5! === 5 * 4! === 4 * 3! === 3 * 2! === 2 * 1! === 1 + +// function factorial(x) { +// if (x === 1) { +// return 1; +// } else { +// return x * factorial(x - 1); +// } +// } +// console.log(factorial(5)); + +// func(x, y) ----> 从 x 加到 y +// 1,100 ---> 从 1 加到 100,1 + 2 + 3 + 4 ... + 99 + 100 +// func(1,100) === 100 + func(1,99) === 99 + func(1,98) === func(1,1) === 1 + +// function func(x, y){ +// if(x === y){ +// return x; +// } else { +// return y + func(x, y-1); +// } +// } +// console.log(func(1, 100)); + + +// 递归第三个例子:斐波那契数列 +// 0、1、1、2、3、5、8、13、21、34 + +// func(5) ----> 第 5 位所对应的斐波那契数 func(4) + func(3) + +function func(x) { + if (x === 1) { + return 0; + } else if (x === 2) { + return 1; + } else { + return func(x - 1) + func(x - 2); + } +} +console.log(func(7)); \ No newline at end of file diff --git a/14. 递归/递归.md b/14. 递归/递归.md new file mode 100644 index 0000000..28cd938 --- /dev/null +++ b/14. 递归/递归.md @@ -0,0 +1,146 @@ +# 递归 + + + +## 经典真题 + + + +- 使用递归完成 *1* 到 *100* 的累加 + + + +## 递归 + + + +*A recursive method is a method that calls itself.* + +递归调用是一种特殊的调用形式,指的是方法自己调用自己的形式。 + + + +image-20211019143738665 + + + +下面是一个递归的示例: + +```java +function neverEnd() { + console.log("This is the method that never ends!"); + neverEnd(); +} +``` + +*method* 会先输出 *This is the method that never ends!* 然后再调用自己,导致无限递归(*infinite recursion*)。当然这一般是我们需要避免的状况。 + + + +在进行递归操作的时候,我们需要满足以下几个条件: + +- 递归调用必须有结束条件 +- 每次调用的时候都需要根据需求改变传递的参数内容 + + + +下面是递归的一个示例,求某个数的阶乘。 + +```java +function factorial(x) { + if (x === 1) { + return 1; + } else { + return x * factorial(x - 1); + } +} +console.log(factorial(5)); // 120 +``` + + + +整个递归的计算过程如下: + +``` +===> factorial(5) +===> 5 * factorial(4) +===> 5 * (4 * factorial(3)) +===> 5 * (4 * (3 * factorial(2))) +===> 5 * (4 * (3 * (2 * factorial(1)))) +===> 5 * (4 * (3 * (2 * 1))) +===> 5 * (4 * (3 * 2)) +===> 5 * (4 * 6) +===> 5 * 24 +===> 120 +``` + +image-20211019143535293 + + + +使用递归时需要注意如下事项: + + + +- 递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以用循环的方式来实现。 + + + +- 使用递归时需要注意防止栈溢出。在计算机中,函数调用是通过栈(*stack*)这种数据结构实现的,每当一个函数调用,栈就会加一层,每当函数返回,栈就会减一层。由于栈的大小不是无限的,所以递归调用的次数过多,会导致栈溢出。 + + + +下面再来看几个递归的示例: + + + +示例 *1*:使用递归来计算从 *x* 加到 *y* 的结果 + +```go +function calc(i, j) { + if (i == j) { + return i; + } + return calc(i, j - 1) + j; +} +console.log(calc(1, 100)); // 5050 +``` + +示例 *2*:使用递归来计算斐波那契数列 + +```go +function calc(i) { + if (i == 1) { + return 1; + } else if (i == 2) { + return 2; + } else { + return calc(i - 1) + calc(i - 2); + } +} +console.log(calc(7)); // 21 +``` + + + +## 真题解答 + + + +- 使用递归完成 *1* 到 *100* 的累加 + +> 参考答案: +> +> ```js +> function calc(i, j) { +> if (i == j) { +> return i; +> } +> return calc(i, j - 1) + j; +> } +> console.log(calc(1, 100)); // 5050 +> ``` + + + +-*EOF*- \ No newline at end of file diff --git a/15. 属性描述符/属性描述符.js b/15. 属性描述符/属性描述符.js new file mode 100644 index 0000000..c7b49c5 --- /dev/null +++ b/15. 属性描述符/属性描述符.js @@ -0,0 +1,154 @@ +// var obj = {}; +// obj.name = "xiejie"; +// obj.age = 18; +// obj.age = "aaaaa"; + + +// console.log(obj.name); + + +// var obj = {}; +// obj.x = 100; + +// 接下来我们通过属性描述符的形式来添加属性 +// 属性描述符是一个对象,作为第三个参数传入 +// Object.defineProperty(obj, 'x', { +// value : 100, +// writable : false +// }) +// console.log(obj.x); +// obj.x = 200; +// console.log(obj.x); + +// 获取某一个对象的属性的属性描述符 +// console.log(Object.getOwnPropertyDescriptor(obj, 'x')); + +// var obj = Object.defineProperty({}, 'x', { +// value : 100, +// configurable: true // 禁止配置 +// }); +// obj.x = 5; //试图修改其值 +// console.log(obj.x); //修改失败,返回undefined +// delete obj.x; +// console.log(obj.x); + +// // 包括如果想要重新定义属性描述符也是不可以的 +// Object.defineProperty(obj,'x',{ +// value : 10 +// }) + +// getter 和 setter 示例 + +// var obj = Object.create(Object.prototype, { +// // 私有属性,不对外 +// _x: { //数据属性 +// value: 1, //初始值 +// writable: true +// }, +// // 对外的,外部可以访问和修改 +// x: { //访问器属性 +// // 通过 getter 和 setter 访问器来访问和设置属性值,可以做一些限制。 +// get: function () { //getter +// return this._x; //返回_x属性值 +// }, +// set: function (value) { //setter +// if (typeof value != "number") { +// throw new Error('请输入数字'); +// } +// this._x = value; //赋值 +// } +// } +// }); +// console.log(obj.x); // 1 +// obj.x = 100; +// console.log(obj.x); // 100 + +// obj.x = "2"; //抛出异常 + +// var obj = { +// _x: 1, // 定义 _x 属性 +// get x() { +// return this._x +// }, //定义 x 属性的 getter +// set x(value) { //定义 x 属性的 setter +// if (typeof value != "number") { +// throw new Error('请输入数字'); +// } +// this._x = value; // 赋值 +// } +// }; +// console.log(obj.x); //1 +// obj.x = 2; +// console.log(obj.x); //2 + + +// var obj = {}; +// obj.name = "xiejie"; +// obj.age = 18; + +// // console.log(Object.getOwnPropertyNames(obj)); + +// console.log(obj.propertyIsEnumerable('name')); + +// var obj = Object.create(Object.prototype, { +// _x: { //数据属性 +// value: 1, //初始值 +// writable: true +// }, +// x: { //访问器属性 +// configurable: true, //允许修改配置 +// get: function () { //getter +// return this._x; //返回_x属性值 +// }, +// set: function (value) { +// if (typeof value != "number") { +// throw new Error('请输入数字'); +// } +// this._x = value; //赋值 +// } +// } +// }); +// var des = Object.getOwnPropertyDescriptor(obj, "x"); //获取属性x的属性描述符 +// console.log(des); +// des.set = function (value) { +// //修改属性x的属性描述符set函数 +// //允许非数值型的数字,也可以进行赋值 +// if (typeof value != "number" && isNaN(value * 1)) { +// throw new Error('请输入数字'); +// } +// this._x = value; +// } +// obj = Object.defineProperty(obj, "x", des); +// console.log(obj.x); //1 +// obj.x = "2"; //把一个给数值型数字赋值给属性x +// console.log(obj.x); //2 + + +function extend(toObj, fromObj) { //扩展对象 + for (var property in fromObj) { //遍历对象属性 + if (!fromObj.hasOwnProperty(property)) continue; //过滤掉继承属性 + Object.defineProperty( //复制完整的属性信息 + toObj, //目标对象 + property, //私有属性 + Object.getOwnPropertyDescriptor(fromObj, property) //获取属性描述符 + ); + } + return toObj; //返回目标对象 +} + +var obj = { + name : "xiejie", + age : 18 +} + +var obj2 = {}; +Object.defineProperty(obj2,'x',{ + value : 100, + writable : false, + enumerable : true +}) + +extend(obj,obj2); +console.log(obj); +obj.x = 200; +console.log(obj.x); \ No newline at end of file diff --git a/15. 属性描述符/属性描述符.md b/15. 属性描述符/属性描述符.md new file mode 100644 index 0000000..d8a3f01 --- /dev/null +++ b/15. 属性描述符/属性描述符.md @@ -0,0 +1,315 @@ +# 属性描述符 + + + +## 经典真题 + + + +- *JavaScript* 中对象的属性描述符有哪些?分别有什么作用? + + + +## 属性描述符详解 + + + +在 *JavaScript* 中,对象的属性可以分为两种: + + + +- 数据属性:它的本质就是一个数据 + +- 存取器属性:它的本质是一个函数,但是可以将它当作普通属性来使用,当给该属性赋值时,会运行相应的 *setter* 函数,当获取该属性的值时,会运行相应的 *getter* 函数。除了存取器,还有一些其他的关键字,用以表示当前属性是否可写、是否有默认值、是否可枚举等,这些关键字就是属性描述符。 + + + +属性描述符是 *ECMAScript* 5 新增的语法,它其实就是一个内部对象,用来描述对象的属性的特性。 + + + +### 属性描述符的结构 + + + +在定义对象、定义属性时,我们曾经介绍过属性描述符,属性描述符实际上就是一个对象。 + +属性描述符一共有 *6* 个,可以选择使用。 + +- *value*:设置属性值,默认值为 *undefined*。 +- *writable*:设置属性值是否可写,默认值为 *true*。 +- *enumerable*:设置属性是否可枚举,即是否允许使用 *for/in* 语句或 *Object.keys( )* 函数遍历访问,默认为 *true*。 +- *configurable*:设置是否可设置属性特性,默认为 *true*。如果为 *false*,将无法删除该属性,不能够修改属性值,也不能修改属性的属性描述符。 +- *get*:取值函数,默认为 *undefined*。 +- *set*:存值函数,默认为 *undefined*。 + + + +注意这几个属性不是都可以一起设置,具体如下图: + +![image-20211021111647398](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-10-21-031647.png) + + + +**示例 1** + +下面示例演示了使用 *value* 读写属性值的基本用法。 + +```js +var obj = {}; //定义空对象 +Object.defineProperty(obj, 'x', {value : 100}); //添加属性x,值为100 +console.log(Object.getOwnPropertyDescriptor(obj, 'x').value); //返回100 +``` + + + +**示例 2** + +下面示例演示了使用 *writable* 属性禁止修改属性 *x*。 + +```js +var obj = {}; +Object.defineProperty(obj, 'x', { + value : 1, //设置属性默认值为1 + writable : false //禁止修改属性值 +}); +obj.x = 2; //修改属性x的值 +console.log(obj.x); // 1 说明修改失败 +``` + +在正常模式下,如果 *writable* 为 *false*,重写属性值不会报错,但是操作失败,而在严格模式下则会抛出异常。 + + + +**示例 3** + +*configurable* 可以禁止修改属性描述符,当其值为 *false* 时,*value、writable、enumerable* 和 *configurable* 禁止修改,同时禁止删除属性。 + +在下面示例中,当设置属性 *x* 禁止修改配置后,下面操作都是不允许的,其中 *obj.x=5;* 若操作失败,则后面 *4* 个操作方法都将抛出异常。 + +```js +var obj = Object.defineProperty({}, 'x', { + configurable : false // 禁止配置 +}); +obj.x = 5; //试图修改其值 +console.log(obj.x); //修改失败,返回undefined +Object.defineProperty(obj, 'x', {value : 2}); //抛出异常 +Object.defineProperty(obj, 'x', {writable: true}); //抛出异常 +Object.defineProperty(obj, 'x', {enumerable: true}); //抛出异常 +Object.defineProperty(obj, 'x', {configurable: true}); //抛出异常 +``` + +当 *configurable* 为 *false* 时,如果把 *writable=true* 改为 *false* 是允许的。只要 *writable* 或 *configurable* 有一个为 *true*,则 *value* 也允许修改。 + + + +### *get* 和 *set* 函数 + + + +除了使用点语法或中括号语法访问属性的 *value* 外,还可以使用访问器,包括 *set* 和 *get* 两个函数。 + +其中,*set( )* 函数可以设置 *value* 属性值,而 *get( )* 函数可以读取 *value* 属性值。 + +借助访问器,可以为属性的 *value* 设计高级功能,如禁用部分特性、设计访问条件、利用内部变量或属性进行数据处理等。 + + + +**示例 1** + +下面示例设计对象 *obj* 的 *x* 属性值必须为数字。为属性 *x* 定义了 *get* 和 *set* 特性,*obj.x* 取值时,就会调用 *get*;赋值时,就会调用 *set*。 + +```js +var obj = Object.create(Object.prototype, { + _x : { //数据属性 + value : 1, //初始值 + writable : true + }, + x : { //访问器属性 + get : function () { //getter + return this._x; //返回_x属性值 + }, + set : function (value) { //setter + if (typeof value != "number"){ + throw new Error('请输入数字'); + } + this._x = value; //赋值 + } + } +}); +console.log(obj.x); //1 +obj.x = "2"; //抛出异常 +``` + + + +**示例 2** + +*JavaScript* 也支持一种简写方法。针对示例 *1*,通过以下方式可以快速定义属性。 + +```js +var obj = { + _x : 1, // 定义 _x 属性 + get x() { return this._x }, //定义 x 属性的 getter + set x(value) { //定义 x 属性的 setter + if (typeof value != "number"){ + throw new Error('请输入数字'); + } + this._x = value; // 赋值 + } +}; +console.log(obj.x); //1 +obj.x = 2; +console.log(obj.x); //2 +``` + +取值函数 *get( )* 不能接收参数,存值函数 *set( )* 只能接收一个参数,用于设置属性的值。 + + + +### 操作属性描述符 + +属性描述符是一个内部对象,无法直接读写,可以通过下面几个函数进行操作。 + +- *Object.getOwnPropertyDescriptor( )*:可以读出指定对象私有属性的属性描述符。 +- *Object.defineProperty( )*:通过定义属性描述符来定义或修改一个属性,然后返回修改后的描述符。 +- *Object.defineProperties( )*:可以同时定义多个属性描述符。 +- *Object.getOwnPropertyNames( )*:获取对象的所有私有属性。 +- *Object.keys( )*:获取对象的所有本地可枚举的属性。 +- *propertyIsEnumerable( )*:对象实例方法,直接调用,判断指定的属性是否可枚举。 + + + +**示例 1** + +在下面示例中,定义 *obj* 的 *x* 属性允许配置特性,然后使用 *Object.getOwnPropertyDescriptor( )* 函数获取对象 *obj* 的 *x* 属性的属性描述符。修改属性描述符的 *set* 函数,重设检测条件,允许非数值型数字赋值。 + +```js +var obj = Object.create(Object.prototype, { + _x: { //数据属性 + value: 1, //初始值 + writable: true + }, + x: { //访问器属性 + configurable: true, //允许修改配置 + get: function () { //getter + return this._x; //返回_x属性值 + }, + set: function (value) { + if (typeof value != "number") { + throw new Error('请输入数字'); + } + this._x = value; //赋值 + } + } +}); +var des = Object.getOwnPropertyDescriptor(obj, "x"); //获取属性x的属性描述符 +des.set = function (value) { + //修改属性x的属性描述符set函数 + //允许非数值型的数字,也可以进行赋值 + if (typeof value != "number" && isNaN(value * 1)) { + throw new Error('请输入数字'); + } + this._x = value; +} +obj = Object.defineProperty(obj, "x", des); +console.log(obj.x); //1 +obj.x = "2"; //把一个给数值型数字赋值给属性x +console.log(obj.x); //2 +``` + + + +**示例 2** + +下面示例先定义一个扩展函数,使用它可以把一个对象包含的属性以及丰富的信息复制给另一个对象。 + +【实现代码】 + +```js +function extend (toObj, fromObj) { //扩展对象 + for (var property in fromObj) { //遍历对象属性 + if (!fromObj.hasOwnProperty(property)) continue; //过滤掉继承属性 + Object.defineProperty( //复制完整的属性信息 + toObj, //目标对象 + property, //私有属性 + Object.getOwnPropertyDescriptor(fromObj, property) //获取属性描述符 + ); + } + return toObj; //返回目标对象 +} +``` + +【应用代码】 + +```js +var obj = {}; //新建对象 +obj.x = 1; //定义对象属性 +extend(obj, { get y() { return 2} }) //定义读取器对象 +console.log(obj.y); //2 +``` + + + +### 控制对象状态 + + + +*JavaScript* 提供了 *3* 种方法,用来精确控制一个对象的读写状态,防止对象被改变。 + +- *Object.preventExtensions*:阻止为对象添加新的属性。 +- *Object.seal*:阻止为对象添加新的属性,同时也无法删除旧属性。等价于属性描述符的 *configurable* 属性设为 *false*。注意,该方法不影响修改某个属性的值。 +- *Object.freeze*:阻止为一个对象添加新属性、删除旧属性、修改属性值。 + + +同时提供了 *3* 个对应的辅助检查函数,简单说明如下: + +- *Object.isExtensible*:检查一个对象是否允许添加新的属性。 +- *Object.isSealed*:检查一个对象是否使用了 *Object.seal* 方法。 +- *Object.isFrozen*:检查一个对象是否使用了 *Object.freeze* 方法。 + + + +**示例** + +下面代码分别使用 *Object.preventExtensions、Object.seal* 和 *Object.freeze* 函数控制对象的状态,然后再使用 *Object.isExtensible、Object.isSealed* 和 *Object.isFrozen* 函数检测对象的状态。 + +```js +var obj1 = {}; +console.log(Object.isExtensible(obj1)); //true +Object.preventExtensions(obj1); +console.log(Object.isExtensible(obj1)); //false +var obj2 = {}; +console.log(Object.isSealed(obj2)); //true +Object.seal(obj2); +console.log(Object.isSealed(obj2)); //false +var obj3 = {}; +console.log(Object.isFrozen(obj3)); //true +Object.freeze(obj3); +console.log(Object.isFrozen(obj3)); //false +``` + + + +## 真题解答 + + + +- *JavaScript* 中对象的属性描述符有哪些?分别有什么作用? + +> 参考答案: +> +> 属性描述符一共有 *6* 个,可以选择使用。 +> +> - *value*:设置属性值,默认值为 *undefined*。 +> - *writable*:设置属性值是否可写,默认值为 *true*。 +> - *enumerable*:设置属性是否可枚举,即是否允许使用 *for/in* 语句或 *Object.keys( )* 函数遍历访问,默认为 *true*。 +> - *configurable*:设置是否可设置属性特性,默认为 *true*。如果为 *false*,将无法删除该属性,不能够修改属性值,也不能修改属性的属性描述符。 +> - *get*:取值函数,默认为 *undefined*。 +> - *set*:存值函数,默认为 *undefined*。 +> +> 使用属性描述符的时候,*get* 和 *set* 以及 *value* 和 *writable* 这两组是互斥的,设置了 *get* 和 *set* 就不能设置 *value* 和 *writable*,反之设置了 *value* 和 *writable* 也就不可以设置 *get* 和 *set*。 + + + +-*EOF*- \ No newline at end of file diff --git a/16. Class和普通构造器的区别/class 和构造函数区别.js b/16. Class和普通构造器的区别/class 和构造函数区别.js new file mode 100644 index 0000000..df2ae88 --- /dev/null +++ b/16. Class和普通构造器的区别/class 和构造函数区别.js @@ -0,0 +1,121 @@ +// // 电脑类 +// // 通过 ES6 的 class 语法来创建一个类 +// class Computer1{ +// // 构造器 +// constructor(name, price){ +// // 实例属性 +// this.name = name; +// this.price = price; +// } +// // 实例方法 +// showPrice(){ +// console.log(`这台${this.name}电脑的价格为${this.price}元。`); +// } +// // 静态方法 +// static staticFunc(){ +// console.log("这是 Computer1 类的静态方法"); +// } +// } +// // var apple = new Computer("苹果", 15000); +// // console.log(apple.name); // 苹果 +// // console.log(apple.price); // 15000 +// // apple.showPrice(); +// // Computer.staticFunc(); + + +// // 使用 ES5 的构造函数的方法来创建 +// function Computer2(name, price){ +// this.name = name; +// this.price = price; +// } +// Computer2.prototype.showPrice = function(){ +// console.log(`这台${this.name}电脑的价格为${this.price}元。`); +// } +// Computer2.staticFunc = function(){ +// console.log("这是 Computer2 类的静态方法"); +// } + + + +// var apple = new Computer1("苹果", 15000); +// // new apple.showPrice() + +// var huawei = new Computer2("华为", 12000); +// console.log(new huawei.showPrice()); + + +"use strict"; +// 核对 class 类的调用方法,如果是以普通函数的形式调用的,就会抛出错误 +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} + +// 对原型和静态方法做特殊处理,设置其特性 +function _defineProperties(target, props) { + + // console.log("target:::",target); + // console.log("props:::",props); + // target::: {} + // props::: [ { key: 'showSth', value: [Function: showSth] } ] + // target::: [Function: Computer] + // props::: [ { key: 'comStruct', value: [Function: comStruct] } ] + + // 遍历原型方法和静态方法 + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) + descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +// 调用前面的函数,对原型方法和静态方法进行特性设置 +function _createClass(Constructor, protoProps, staticProps) { + + console.log("Constructor:::",Constructor); + console.log("protoProps:::",protoProps); + console.log("staticProps:::",staticProps); + // Constructor::: [Function: Computer] + // protoProps::: [ { key: 'showSth', value: [Function: showSth] } ] + // staticProps::: [ { key: 'comStruct', value: [Function: comStruct] } ] + + if (protoProps) + _defineProperties(Constructor.prototype, protoProps); + if (staticProps) + _defineProperties(Constructor, staticProps); + return Constructor; +} + +var Computer = /*#__PURE__*/function () { + // 构造器 + function Computer(name, price) { + // 1. 核对你是如何进行调用的 + _classCallCheck(this, Computer); + + this.name = name; + this.price = price; + } + + + _createClass(Computer, [{ + key: "showSth", + value: function showSth() { + console.log("\u8FD9\u662F\u4E00\u53F0".concat(this.name, "\u7535\u8111")); + } // 原型方法 + + }], [{ + key: "comStruct", + value: function comStruct() { + console.log("电脑由显示器,主机,键鼠组成"); + } // 静态方法 + }]); + + return Computer; +}(); + + +var apple = new Computer("苹果",15000); \ No newline at end of file diff --git a/16. Class和普通构造器的区别/class 和构造函数区别.md b/16. Class和普通构造器的区别/class 和构造函数区别.md new file mode 100644 index 0000000..50f1d2e --- /dev/null +++ b/16. Class和普通构造器的区别/class 和构造函数区别.md @@ -0,0 +1,578 @@ +# *class* 和构造函数区别 + + + +## 经典真题 + + + +- 根据下面 *ES6* 构造函数的书写方式,要求写出 *ES5* 的 + +```js +class Example { + constructor(name) { + this.name = name; + } + init() { + const fun = () => { console.log(this.name) } + fun(); + } +} +const e = new Example('Hello'); +e.init(); +``` + + + +## 回顾 *class* 的写法 + + + +上面的这道面试题,典型的就是考察 *ES6* 中新增的 *class* 和以前构造函数上面的区别是什么,以及如果通过 *ES5* 去模拟的话,具体如何实现。 + + + +那么在此之前,我们就先来回顾一下 *ES6* 中的 *class* 写法。 + + + +代码如下: + +```js +class Computer { + // 构造器 + constructor(name, price) { + this.name = name; + this.price = price; + } + // 原型方法 + showSth() { + console.log(`这是一台${this.name}电脑`); + } + // 静态方法 + static comStruct() { + console.log("电脑由显示器,主机,键鼠组成"); + } +} +``` + + + +上面的代码非常的简单,我们定义了一个名为 Computer 的类,该类存在 *name、price* 这两个实例属性,一个 *showSth* 的原型方法以及一个 *comStruct* 的静态方法。 + + + +我们可以简单的实例化一个对象出来,例如: + +```js +var apple = new Computer("苹果", 15000); +console.log(apple.name); // 苹果 +console.log(apple.price); // 15000 +apple.showSth(); // 这是一台苹果电脑 +Computer.comStruct(); // 电脑由显示器,主机,键鼠组成 +``` + +在上面的代码中,我们从 *Computer* 类中实例化出来了一个 *apple* 的实例对象,然后简单访问了该对象的属性和方法。 + + + +## 回顾构造函数的写法 + + + +那么,在 *ES6* 出现之前,我们是如何实现类似于其他语言中的“类”的呢? + +没错,我们是通过的构造函数,然后将方法挂在原型上面。例如: + +```js +function Computer(name, price){ + this.name = name; + this.price = price; +} +Computer.prototype.showSth = function(){ + console.log(`这是一台${this.name}电脑`); +} +Computer.comStruct = function(){ + console.log("电脑由显示器,主机,键鼠组成"); +} + +var apple = new Computer("苹果", 15000); +console.log(apple.name); // 苹果 +console.log(apple.price); // 15000 +apple.showSth(); // 这是一台苹果电脑 +Computer.comStruct(); // 电脑由显示器,主机,键鼠组成 +``` + +上面的代码就是我们经常在 *ES5* 中所书写的代码,通过构造函数来模拟类,实例方法挂在原型上面,静态方法就挂在构造函数上。 + +仿佛 *ES6* 的 *class* 写法就是上面构造函数写法的一种语法糖,但是事实真的如此么? + + + +## *class* 和构造函数区别上的细则 + + + +接下来我们来详细比较一下两种写法在细节上面的一些差异。 + +首先我们书写两个“类”,一个用 *ES5* 的构造函数书写,一个用 *ES6* 的类的写法来书写,如下: + +```js +class Computer1 { + // 构造器 + constructor(name, price) { + this.name = name; + this.price = price; + } + // 原型方法 + showSth() { + console.log(`这是一台${this.name}电脑`); + } + // 静态方法 + static comStruct() { + console.log("电脑由显示器,主机,键鼠组成"); + } +} + +function Computer2(name, price){ + this.name = name; + this.price = price; +} +Computer2.prototype.showSth = function(){ + console.log(`这是一台${this.name}电脑`); +} +Computer2.comStruct = function(){ + console.log("电脑由显示器,主机,键鼠组成"); +} +``` + + + +我们知道,构造函数也是函数,既然是函数,那么就可以通过函数调用的形式来调用该函数,例如: + +```js +var i = Computer2(); +console.log(i); // undefined +``` + +运行上面的代码,代码不会报错,因为没有使用 *new* 的方式来调用,所以不会生成一个对象,返回值就为 *undefined*。 + + + +但是如果我们这样来调用 *ES6* 书写的类,会直接报错: + +```js +Computer1(); +// TypeError: Class constructor Computer1 cannot be invoked without 'new' +``` + +可以看到,*ES6* 所书写的 *class* ,虽然我们认为背后就是构造函数实现的,但是明显是做了特殊处理的,必须通过 *new* 关键字来调用。 + + + +接下来,我们来针对两种写法,各自实例化一个对象,代码如下: + +```js +var apple = new Computer2("苹果", 15000); +for(var i in apple){ + console.log(i); +} +console.log('-------'); +var huawei = new Computer1("华为", 12000); +for(var i in huawei){ + console.log(i); +} +``` + +在上面的代码中, *apple* 对象是 *ES5* 构造函数的形式创建的实例,*huawei* 是 *ES6* 类的形式创建的实例。有了这两个对象后,我们遍历这两个对象的键,结果如下: + +```js +name +price +showSth +------- +name +price +``` + +可以看到,*ES6* 中的原型方法是不可被枚举的,说明 *ES6* 对此也是做了特殊处理的。 + + + +另外,*ES6* 的 *class* 中的所有代码均处于严格模式之下,这里我们也可以进行一个简单的验证。例如,对两种方式的 *showSth* 原型方法稍作修改,如下: + +```js +class Computer1 { + ... + // 原型方法 + showSth(i,i) { + console.log(`这是一台${this.name}电脑`); + } + ... +} +function Computer2(name, price){ + ... +} +Computer2.prototype.showSth = function(j,j){ + i = 10; + console.log(`这是一台${this.name}电脑`); +} +... +``` + +在上面的代码中,我们为各自的 *showSth* 方法添加了重复的形式参数。我们知道,在严格模式中方法书写重复形参是不被允许的。 + +所以在运行代码时,*ES6* 的 *class* 声明方式会报错,错误信息如下: + +```js +// SyntaxError: Duplicate parameter name not allowed in this context +``` + + + +还有就是,如果是 *ES6* 形式所声明的类,原型上的方法是不允许通过 *new* 来调用的。 + +这里我们也可以做一个简单的测试,如下: + +```js +function Computer2(name, price){ + this.name = name; + this.price = price; +} +Computer2.prototype.showSth = function(){ + i = 10; + console.log(`这是一台${this.name}电脑`); +} +Computer2.comStruct = function(){ + console.log("电脑由显示器,主机,键鼠组成"); +} + +var apple = new Computer2("苹果", 15000); +var i = new apple.showSth(); // 这是一台undefined电脑 +console.log(i); // {} +``` + +在上面的代码中,我们首先实例化了一个 *apple* 对象,在该对象的原型上面拥有一个 *showSth* 的实例方法,然后我们对其进行了 *new* 操作,可以看到返回了一个对象。 + + + +但是如果是 *ES6* 形式所声明的类,上面的做法将不被允许。示例如下: + +```js +class Computer1 { + // 构造器 + constructor(name, price) { + this.name = name; + this.price = price; + } + // 原型方法 + showSth() { + console.log(`这是一台${this.name}电脑`); + } + // 静态方法 + static comStruct() { + console.log("电脑由显示器,主机,键鼠组成"); + } +} +var huawei = new Computer1("华为", 12000); +var i = new huawei.showSth(); // TypeError: huawei.showSth is not a constructor +console.log(i); +``` + +在上面的代码中,我们企图对 *Computer1* 实例对象 *huawei* 的原型方法 *showSth* 进行 *new* 操作,可以看到,这里报出了 *TypeError*。 + + + +## *Babel* 中具体的实现 + + + +通过上面的各种例子,我们可以知道 *ES6* 中的 *class* 实现并不是我们单纯所想象的就是之前 *ES5* 写构造函数的写法,虽然本质上是构造函数,但是内部是做了各种处理的。 + + + +这里,我们就来使用 *Babel* 对下面的代码进行转义,转义之前的代码如下: + +```js +class Computer { + // 构造器 + constructor(name, price) { + this.name = name; + this.price = price; + } + // 原型方法 + showSth() { + console.log(`这是一台${this.name}电脑`); + } + // 静态方法 + static comStruct() { + console.log("电脑由显示器,主机,键鼠组成"); + } +} +``` + +转义后的代码如下: + +```js +"use strict"; +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} + +function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) + descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) + _defineProperties(Constructor.prototype, protoProps); + if (staticProps) + _defineProperties(Constructor, staticProps); + return Constructor; +} + +var Computer = /*#__PURE__*/function () { + // 构造器 + function Computer(name, price) { + _classCallCheck(this, Computer); + + this.name = name; + this.price = price; + } // 原型方法 + + + _createClass(Computer, [{ + key: "showSth", + value: function showSth() { + console.log("\u8FD9\u662F\u4E00\u53F0".concat(this.name, "\u7535\u8111")); + } // 静态方法 + + }], [{ + key: "comStruct", + value: function comStruct() { + console.log("电脑由显示器,主机,键鼠组成"); + } + }]); + + return Computer; +}(); +var apple = new Computer("苹果", 15000); +console.log(apple.name); // 苹果 +console.log(apple.price); // 15000 +apple.showSth(); // 这是一台苹果电脑 +Computer.comStruct(); // 电脑由显示器,主机,键鼠组成 +``` + +可以看到,果然没有我们想象的那么简单,接下来我们就来一点一点剖析转义的结果。 + +首先整体来讲分为下面几块: + +```js +"use strict"; +function _classCallCheck(instance, Constructor) { ... } + +function _defineProperties(target, props) { ... } + +function _createClass(Constructor, protoProps, staticProps) { ... } + +var Computer = /*#__PURE__*/function () { ... }(); +``` + +我们一块一块的来看。 + +```js +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} +``` + +第一个方法叫做 *classCallCheck*,从名字上面我们也可以看出,这个方法是核对构造方法的调用形式的,接收两个参数,一个是实例对象,另一个是构造函数,通过 *instanceof* 来看参数 *instance* 是否是 *Constructor* 的实例,如果不是就抛出错误。 + + + +接下来是 *_defineProperties* 方法,我们对此方法稍作了修改,打印 *target* 和 *props* 的值 + +```js +function _defineProperties(target, props) { + console.log("target:::",target); + console.log("props:::",props); + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) + descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} +``` + +结果如下: + +```js +target::: {} +props::: [ { key: 'showSth', value: [Function: showSth] } ] +target::: [Function: Computer] +props::: [ { key: 'comStruct', value: [Function: comStruct] } ] +``` + +可以看出,该方法就是设置对象方法的属性描述符,包含是否可遍历呀,是否可写呀等信息,设置完成后将方法挂在 *target* 对象上面。 + + + +下一个是 *_createClass* 函数,我们仍然将三个参数打印出来 + +```js +function _createClass(Constructor, protoProps, staticProps) { + console.log("Constructor::",Constructor); + console.log("protoProps::",protoProps); + console.log("staticProps::",staticProps); + if (protoProps) + _defineProperties(Constructor.prototype, protoProps); + if (staticProps) + _defineProperties(Constructor, staticProps); + return Constructor; +} +``` + +结果如下: + +```js +Constructor:: [Function: Computer] +protoProps:: [ { key: 'showSth', value: [Function: showSth] } ] +staticProps:: [ { key: 'comStruct', value: [Function: comStruct] } ] +``` + +可以看出,接收的三个参数一次为构造函数、原型上的方法,静态方法。接下来在该方法里面所做的事情也就非常清晰了。 + + + +最后就是我们的构造函数了: + +```js +var Computer = /*#__PURE__*/function () { + // 构造器 + function Computer(name, price) { + // 进行调用确认 + _classCallCheck(this, Computer); + // 添加实例属性 + this.name = name; + this.price = price; + } // 原型方法 + + // 将实例方法和静态方法添加到构造函数上面 + _createClass(Computer, [{ + key: "showSth", + value: function showSth() { + console.log("\u8FD9\u662F\u4E00\u53F0".concat(this.name, "\u7535\u8111")); + } // 静态方法 + + }], [{ + key: "comStruct", + value: function comStruct() { + console.log("电脑由显示器,主机,键鼠组成"); + } + }]); + + return Computer; +}(); +``` + +明白了 *_createClass* 方法的作用后,该方法的代码也就非常的清晰了。 + + + +## 真题解答 + + + +- 根据下面 *ES6* 构造函数的书写方式,要求写出 *ES5* 的 + +```js +class Example { + constructor(name) { + this.name = name; + } + init() { + const fun = () => { console.log(this.name) } + fun(); + } +} +const e = new Example('Hello'); +e.init(); +``` + +> 参考答案: +> +> ```js +> "use strict"; +> +> function _classCallCheck(instance, Constructor) { +> if (!(instance instanceof Constructor)) { +> throw new TypeError("Cannot call a class as a function"); +> } +> } +> +> function _defineProperties(target, props) { +> for (var i = 0; i < props.length; i++) { +> var descriptor = props[i]; +> descriptor.enumerable = descriptor.enumerable || false; +> descriptor.configurable = true; +> if ("value" in descriptor) +> descriptor.writable = true; +> Object.defineProperty(target, descriptor.key, descriptor); +> } +> } +> +> function _createClass(Constructor, protoProps, staticProps) { +> if (protoProps) +> _defineProperties(Constructor.prototype, protoProps); +> if (staticProps) +> _defineProperties(Constructor, staticProps); +> return Constructor; +> } +> +> var Example = /*#__PURE__*/function () { +> function Example(name) { +> _classCallCheck(this, Example); +> +> this.name = name; +> } +> +> _createClass(Example, [{ +> key: "init", +> value: function init() { +> var _this = this; +> +> var fun = function fun() { +> console.log(_this.name); +> }; +> +> fun(); +> } +> }]); +> +> return Example; +> }(); +> +> var e = new Example('Hello'); +> e.init(); +> ``` +> +> 这里可以解释出 *_classCallCheck、_defineProperties、_createClass* 这几个方法各自的作用是什么。 + + + +-*EOF*- + diff --git a/17. 浮点数精度问题/浮点数精度问题.js b/17. 浮点数精度问题/浮点数精度问题.js new file mode 100644 index 0000000..4695e27 --- /dev/null +++ b/17. 浮点数精度问题/浮点数精度问题.js @@ -0,0 +1,29 @@ +// console.log(0.1 + 0.2 === 0.3); +// console.log(1.00); + +// // 加法 +// console.log(0.1 + 0.2); // 0.30000000000000004 +// console.log(0.7 + 0.1); // 0.7999999999999999 +// console.log(0.2 + 0.4); // 0.6000000000000001 +// console.log(2.22 + 0.1); // 2.3200000000000003 + +// // 减法 +// console.log(1.5 - 1.2); // 0.30000000000000004 +// console.log(0.3 - 0.2); // 0.09999999999999998 + +// // 乘法 +// console.log(19.9 * 100); // 1989.9999999999998 +// console.log(19.9 * 10 * 10); // 1990 +// console.log(9.7 * 100); // 969.9999999999999 +// console.log(39.7 * 100); // 3970.0000000000005 + +// // 除法 +// console.log(0.3 / 0.1); // 2.9999999999999996 +// console.log(0.69 / 10); // 0.06899999999999999 + +// console.log(parseInt(0.58 * 100, 10)); + +// console.log((1.335).toFixed(2)); + +console.log(19571992547450991); // 19571992547450990 +console.log(19571992547450991===19571992547450992); // true \ No newline at end of file diff --git a/17. 浮点数精度问题/浮点数精度问题.md b/17. 浮点数精度问题/浮点数精度问题.md new file mode 100644 index 0000000..0275c21 --- /dev/null +++ b/17. 浮点数精度问题/浮点数精度问题.md @@ -0,0 +1,241 @@ +# 浮点数精度问题 + + + +## 经典真题 + + + +- 为什么 *console.log(0.2+0.1==0.3)* 得到的值为 *false* + + + +## 浮点数精度常见问题 + + + +在 *JavaScript* 中整数和浮点数都属于 *number* 数据类型,所有数字都是以 *64* 位浮点数形式储存,即便整数也是如此。 所以我们在打印 *1.00* 这样的浮点数的结果是 *1* 而非 *1.00* 。 + +在一些特殊的数值表示中,例如金额,这样看上去有点别扭,但是至少值是正确了。 + +然而要命的是,当浮点数做数学运算的时候,你经常会发现一些问题,举几个例子: + +**场景一**:进行浮点值运算结果的判断 + +```js +// 加法 +console.log(0.1 + 0.2); // 0.30000000000000004 +console.log(0.7 + 0.1); // 0.7999999999999999 +console.log(0.2 + 0.4); // 0.6000000000000001 +console.log(2.22 + 0.1); // 2.3200000000000003 + +// 减法 +console.log(1.5 - 1.2); // 0.30000000000000004 +console.log(0.3 - 0.2); // 0.09999999999999998 + +// 乘法 +console.log(19.9 * 100); // 1989.9999999999998 +console.log(19.9 * 10 * 10); // 1990 +console.log(9.7 * 100); // 969.9999999999999 +console.log(39.7 * 100); // 3970.0000000000005 + +// 除法 +console.log(0.3 / 0.1); // 2.9999999999999996 +console.log(0.69 / 10); // 0.06899999999999999 +``` + + + +**场景二**:将小数乘以 *10* 的 *n* 次方取整 + +比如将钱币的单位,从元转化成分,经常写出来的是 *parseInt(yuan\*100, 10)* + +```js +console.log(parseInt(0.58 * 100, 10)); // 57 +``` + + + +**场景三**:四舍五入保留 *n* 位小数 + +例如我们会写出 *(number).toFixed(2)*,但是看下面的例子: + +```js +console.log((1.335).toFixed(2)); // 1.33 +``` + +在上面的例子中,我们得出的结果是 *1.33*,而不是预期结果 *1.34*。 + + + +## 为什么会有这样的问题 + + + +似乎是不可思议。小学生都会算的题目,*JavaScript* 不会? + +我们来看看其真正的原因,到底为什么会产生精度丢失的问题呢? + + + +计算机底层只有 *0* 和 *1*, 所以所有的运算最后实际上都是二进制运算。 + +十进制整数利用辗转相除的方法可以准确地转换为二进制数,但浮点数呢? + + + +img + + + + *JavaScript* 里的数字是采用 *IEEE 754* 标准的 *64* 位双精度浮点数。 + +先看下面一张图: + +![preview](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-09-29-060439.png) + +该规范定义了浮点数的格式,对于 *64* 位的浮点数在内存中的表示,最高的 *1* 位是符号位,接着的 *11* 位是指数,剩下的 *52* 位为有效数字,具体如下: + +- 符号位 *S*:第 *1* 位是正负数符号位(*sign*),*0* 代表正数,*1* 代表负数 +- 指数位 *E*:中间的 *11* 位存储指数(*exponent*),用来表示次方数 +- 尾数位 *M*:最后的 *52* 位是尾数(*mantissa*),储存小数部分,超出的部分自动进一舍零 + +也就是说,浮点数最终在运算的时候实际上是一个符合该标准的二进制数 + +符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。 + +*IEEE 754* 规定,有效数字第一位默认总是 *1*,不保存在 *64* 位浮点数之中。也就是说,有效数字总是 *1.xx…xx* 的形式,其中 *xx…xx* 的部分保存在 *64* 位浮点数之中,最长可能为 *52* 位。因此,*JavaScript* 提供的有效数字最长为 *53* 个二进制位(*64* 位浮点的后 *52* 位 + 有效数字第一位的 *1*)。 + +既然限定位数,必然有截断的可能。 + + + +我们可以看一个例子: + +```js +console.log(0.1 + 0.2); // 0.30000000000000004 +``` + +为了验证该例子,我们得先知道怎么将浮点数转换为二进制,整数我们可以用除 *2* 取余的方式,小数我们则可以用乘 *2* 取整的方式。 + + + +*0.1* 转换为二进制: + +*0.1 \* 2*,值为 *0.2*,小数部分 *0.2*,整数部分 *0* + +*0.2 \* 2*,值为 *0.4*,小数部分 *0.4*,整数部分 *0* + +*0.4 \* 2*,值为0.8,小数部分0.8,整数部分0 + +*0.8 \* 2*,值为 *1.6*,小数部分 *0.6*,整数部分 *1* + +*0.6 \* 2*,值为 *1.2*,小数部分 *0.2*,整数部分 *1* + +*0.2 \* 2*,值为 *0.4*,小数部分 *0.4*,整数部分 *0* + +从 *0.2* 开始循环 + + + +*0.2* 转换为二进制可以直接参考上述,肯定最后也是一个循环的情况 + + + +所以最终我们能得到两个循环的二进制数: + +*0.1:0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1100 ...* + +*0.2:0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 ...* + +这两个的和的二进制就是: + +*sum:0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 ...* + +最终我们只能得到和的近似值(按照 *IEEE 754* 标准保留 *52* 位,按 *0* 舍 *1* 入来取值),然后转换为十进制数变成: + +sum ≈ 0.30000000000000004 + + + +再例如: + +```js +console.log((1.335).toFixed(2)); // 1.33 +``` + +因为 *1.335* 其实是 *1.33499999999999996447286321199*,*toFixed* 虽然是四舍五入,但是是对 *1.33499999999999996447286321199* 进行四五入,所以得出 *1.33*。 + + + +在 *Javascript* 中,整数精度同样存在问题,先来看看问题: + +```js +console.log(19571992547450991); // 19571992547450990 +console.log(19571992547450991===19571992547450992); // true +``` + + + +同样的原因,在 *JavaScript* 中 *number* 类型统一按浮点数处理,整数是按最大 *54* 位来算, + +- 最大( *253 - 1*,*Number.MAX_SAFE_INTEGER*、*9007199254740991*) +- 最小( *-(253 - 1)*,*Number.MIN_SAFE_INTEGER*、*-9007199254740991*) + +所以只要超过这个范围,就会存在被舍去的精度问题。 + + + +当然这个问题并不只是在 *Javascript* 中才会出现,几乎所有的编程语言都采用了 *IEEE-754* 浮点数表示法,任何使用二进制浮点数的编程语言都会有这个问题。 + +只不过在很多其他语言中已经封装好了方法来避免精度的问题,而 *JavaScript* 是一门弱类型的语言,从设计思想上就没有对浮点数有个严格的数据类型,所以精度误差的问题就显得格外突出。 + + + +通常这种对精度要求高的计算都应该交给后端去计算和存储,因为后端有成熟的库来解决这种计算问题。 + +前端也有几个不错的类库: + + + +***Math.js*** + +*Math.js* 是专门为 *JavaScript* 和 *Node.js* 提供的一个广泛的数学库。它具有灵活的表达式解析器,支持符号计算,配有大量内置函数和常量,并提供集成解决方案来处理不同的数据类型。 + +像数字,大数字(超出安全数的数字),复数,分数,单位和矩阵。 功能强大,易于使用。 + + + +***decimal.js*** + +为 *JavaScript* 提供十进制类型的任意精度数值。 + + + +***big.js*** + +不仅能够支持处理 *Long* 类型的数据,也能够准确的处理小数的运算。 + + + +## 真题解答 + + + +- 为什么 *console.log(0.2+0.1==0.3)* 得到的值为 *false* + +> 参考答案: +> +> 因为浮点数的计算存在 *round-off* 问题,也就是浮点数不能够进行精确的计算。并且: +> +> - 不仅 *JavaScript*,所有遵循 *IEEE 754* 规范的语言都是如此; +> - 在 *JavaScript* 中,所有的 *Number* 都是以 *64-bit* 的双精度浮点数存储的; +> - 双精度的浮点数在这 *64* 位上划分为 *3* 段,而这 *3* 段也就确定了一个浮点数的值,*64bit* 的划分是“*1-11-52*”的模式,具体来说: +> - 就是 *1* 位最高位(最左边那一位)表示符号位,*0* 表示正,*1* 表示负; +> - *11* 位表示指数部分; +> - *52* 位表示尾数部分,也就是有效域部分 + + + +-*EOF*- + diff --git a/18. 严格模式/严格模式.js b/18. 严格模式/严格模式.js new file mode 100644 index 0000000..a180708 --- /dev/null +++ b/18. 严格模式/严格模式.js @@ -0,0 +1,49 @@ +"use strict" + +// 1. 没有声明的变量不能使用 +// a = 10; +// console.log(a); + +// 2. 删除变量和不存在的属性会报错 + +// var i = 10; +// delete i; + +// delete Object.prototype; + +// 3. 函数中相同的形参名会报错 + +// function test(a, a){ + +// } + +// 4. 对象不能有重名的属性(根据 MDN 的说法ES6已经不存在此问题,有一个 bug 的提案) + +// var o = { +// p: 1, +// p: 2 +// }; +// console.log(o.p); + +// 5. 八进制 +// 以前可以使用 0作为八进制的前缀 +// var i = 010; +// console.log(i); + + +// 6. 函数中的 this 为 undefined +// function test(){ +// console.log(this); +// } +// test(); + +// 7. eval 作用域 + +// var x = 2; +// console.info(eval("var x = 5; x")); // 5 +// console.info(x); // 2 + +// 8. 不能使用保留字作为标识符 + +var public = "hello world"; +console.log(public); \ No newline at end of file diff --git a/18. 严格模式/严格模式.md b/18. 严格模式/严格模式.md new file mode 100644 index 0000000..81e06a9 --- /dev/null +++ b/18. 严格模式/严格模式.md @@ -0,0 +1,294 @@ +# 严格模式 + + + +## 经典真题 + + + +- *use strict* 是什么意思 ? 使用它区别是什么? + + + +## 什么是严格模式 + + + +严格模式是从 *ES5* 开始新增的一种方式,是采用具有限制性 *JavaScript* 变体的一种方式,从而使代码隐式地脱离“马虎模式/稀松模式/懒散模式“(*sloppy*)模式。 + + + +设立"严格模式"的目的,主要有以下几个: + + + +- 消除 *Javascript* 语法的一些不合理、不严谨之处,减少一些怪异行为; +- 消除代码运行的一些不安全之处,保证代码运行的安全; +- 提高编译器效率,增加运行速度; +- 为未来新版本的 *Javascript* 做好铺垫。 + + + +“严格模式”体现了 *Javascript* 更合理、更安全、更严谨的发展方向,支持严格模式的浏览器有:*Internet Explorer 10 +、 Firefox 4+ Chrome 13+、 Safari 5.1+、 Opera 12+*。 + + + +在“严格模式下”,同样的代码,可能会有不一样的运行结果。一些在“正常模式”下可以运行的语句,在“严格模式”下将不能运行。 + + + +掌握这些内容,有助于更细致深入地理解 *Javascript*,让你变成一个更好的程序员。 + + + +## 开启严格模式 + + + +进入“严格模式”的标志,是下面这行语句: + +```js +"use strict"; +``` + +老版本的浏览器会把它当作一行普通字符串,加以忽略。 + + + +“严格模式”有两种调用方法,适用于不同的场合。 + + + +**针对整个脚本文件** + +将 *“use strict”* 放在脚本文件的第一行,则整个脚本都将以“严格模式”运行。 + +如果这行语句不在第一行,则无效,整个脚本以“正常模式”运行。如果不同模式的代码文件合并成一个文件,这一点需要特别注意。 + +```js +"use strict"; +console.log("这是严格模式。"); +``` + +在上面的代码中,我们第一行书写了 *“use strict”*,所以整个代码会进入到严格模式执行。 + + + +```html + + + +``` + +上面的代码表示,一个网页中依次有两段 *Javascript* 代码。前一个 *script* 标签是严格模式,后一个不是。 + + + +**针对单个函数** + +将 *“use strict”* 放在函数体的第一行,则整个函数以“严格模式”运行。 + +```js +function strict(){ + "use strict"; + return "这是严格模式。"; +} + +function notStrict() { + return "这是正常模式。"; +} +``` + + + +**脚本文件的变通写法** + +因为第一种调用方法不利于文件合并,所以更好的做法是,借用第二种方法,将整个脚本文件放在一个立即执行的匿名函数之中。 + +```js +(function (){ + "use strict"; + // some code here +})(); +``` + + + +## 严格模式和普通模式区别 + + + +接下来,我们就来看一下严格模式下对 *Javascript* 的语法和行为,都做了哪些改变。 + + + +**没有使用 *var* 声明的变量不能使用** + +在普通模式下,我们可以使用一个未声明的变量,此时该变量会成为一个全局变量。但是这种使用方式在严格模式下会报错。 + +```js +"use strict" +a=10; // ReferenceError: a is not defined +console.log(a) +function sum(){ + var a=10; + console.log(a) +} +sum() +``` + + + +**删除变量和不存在的属性会报错** + +在普通模式下,删除变量或者不允许删除的属性虽然也会失败,但是是“静默失败”,也就是说虽然失败了,但是不会给出任何提示。这样其实会产生很多隐藏问题,也给程序员的调错带来了难度。 + +在严格模式下则会保存,例如: + +```js +"use strict" +var i = 10; +delete i; // SyntaxError: Delete of an unqualified identifier in strict mode. +console.log(i); // 10 +``` + + + +**函数中相同的形参名会报错** + +在普通模式下,函数中两个形参名相同也不会报错,只不过后面的形参所接收到的值会覆盖前面的同名形参。 + +```js +function a(b,b){ + console.log(b); // 2 +} +a(1, 2) +``` + +但是在严格模式下,相同的形参名会报错。 + +```js +"use strict" +// SyntaxError: Duplicate parameter name not allowed in this context +function a(b,b){ + console.log(b); +} +a(1, 2) +``` + + + +**对象不能有重名的属性** + +正常模式下,如果对象有多个重名属性,最后赋值的那个属性会覆盖前面的值。严格模式下,这属于语法错误。 + +```js +"use strict"; +var o = { + p: 1, + p: 2 +}; // 语法错误 +``` + + + +**禁止八进制表示法** + +正常模式下,整数的第一位如果是 *0*,表示这是八进制数,比如 *010* 等于十进制的 *8*。 + +```js +var i = 010; +console.log(i); // 8 +``` + +严格模式禁止这种表示法,整数第一位为 *0*,将报错。 + +```js +"use strict" +var i = 010; // SyntaxError: Octal literals are not allowed in strict mode. +console.log(i); +``` + + + +**函数内部 *this* 值为 *undefined*** + +在普通模式下,函数中的 *this* 在以函数的形式被调用时,指向全局对象。而在严格模式中,得到的值为 *undefined*。 + +```js +"use strict" +function a(){ + console.log(this); // undefined +} +a(); +``` + + + +**创设 *eval* 作用域** + +正常模式下,*Javascript* 语言有两种变量作用域(*scope*):全局作用域和函数作用域。 + +严格模式创设了第三种作用域:*eval* 作用域。 + +正常模式下,*eval* 语句的作用域,取决于它处于全局作用域,还是处于函数作用域。 + +严格模式下,*eval* 语句本身就是一个作用域,不再能够生成全局变量了,它所生成的变量只能用于 *eval* 内部。 + +```js +"use strict"; +var x = 2; +console.info(eval("var x = 5; x")); // 5 +console.info(x); // 2 +``` + + + +**保留字** + +为了向将来 *Javascript* 的新版本过渡,严格模式新增了一些保留字:*implements, interface, let, package, private, protected, public, static, yield*。使用这些词作为变量名将会报错。 + +```js +"use strict"; +var public = "hello world" // SyntaxError: Unexpected strict mode reserved word +console.log(public); +``` + + + +更多关于严格模式的内容,可以参阅 : + +*MDN*:*https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode* + +《*Javascript* 严格模式详解 *By* 阮一峰》:*http://www.ruanyifeng.com/blog/2013/01/javascript_strict_mode.html* + + + +## 真题解答 + + + +- *use strict* 是什么意思 ? 使用它区别是什么? + +> 参考答案: +> +> *use strict* 代表开启严格模式,这种模式使得 *Javascript* 在更严格的条件下运行,实行更严格解析和错误处理。 +> +> 开启“严格模式”的优点: +> +> - 消除 *Javascript* 语法的一些不合理、不严谨之处,减少一些怪异行为; +> - 消除代码运行的一些不安全之处,保证代码运行的安全; +> - 提高编译器效率,增加运行速度; +> - 为未来新版本的 *Javascript* 做好铺垫。 +> +> 回答一些具体的严格模式下和普通模式之间的区别。 + + + +-*EOF*- + diff --git a/19. 函数防抖和节流/函数防抖和节流.html b/19. 函数防抖和节流/函数防抖和节流.html new file mode 100644 index 0000000..622f0b7 --- /dev/null +++ b/19. 函数防抖和节流/函数防抖和节流.html @@ -0,0 +1,87 @@ + + + + + + + + Document + + + + + + + + \ No newline at end of file diff --git a/19. 函数防抖和节流/函数防抖和节流.md b/19. 函数防抖和节流/函数防抖和节流.md new file mode 100644 index 0000000..161ce14 --- /dev/null +++ b/19. 函数防抖和节流/函数防抖和节流.md @@ -0,0 +1,333 @@ +# 函数防抖和节流 + + + +## 经典真题 + + + +- 防抖,节流是什么,如何实现 (字节) + + + +## 什么是函数防抖和节流 + + + +*JavaScript* 中的函数大多数情况下都是由用户主动调用触发的,除非是函数本身的实现不合理,否则一般不会遇到跟性能相关的问题。 + +但是在一些少数情况下,函数的触发不是由用户直接控制的。在这些场景下,函数有可能被非常频繁地调用,而造成大的性能问题。解决性能问题的处理办法就有**函数防抖**和**函数节流**。 + +下面是函数被频繁调用的常见的几个场景: + + + +- *mousemove* 事件:如果要实现一个拖拽功能,需要一路监听 *mousemove* 事件,在回调中获取元素当前位置,然后重置 *DOM* 的位置来进行样式改变。如果不加以控制,每移动一定像素而触发的回调数量非常惊人,回调中又伴随着 DOM 操作,继而引发浏览器的重排与重绘,性能差的浏览器可能就会直接假死。 + + + +- *window.onresize* 事件:为 *window* 对象绑定了 *resize* 事件,当浏览器窗口大小被拖动而改变的时候,这个事件触发的频率非常之高。如果在 *window.onresize* 事件函数里有一些跟 *DOM* 节点相关的操作,而跟 *DOM* 节点相关的操作往往是非常消耗性能的,这时候浏览器可能就会吃不消而造成卡顿现象。 + + + +- 射击游戏的 *mousedown/keydown* 事件(单位时间只能发射一颗子弹) + + + +- 搜索联想(*keyup* 事件) + + + +- 监听滚动事件判断是否到页面底部自动加载更多(*scroll* 事件) + + + +对于这些情况的解决方案就是函数防抖(*debounce*)或函数节流(*throttle*),**其核心就是限制某一个方法的频繁触发**。 + + + +## 函数防抖 + + + +我们首先来看函数防抖。**函数防抖,是指防止函数在极短的时间内反复调用,造成资源的浪费**。 + +考虑一下电梯关门的场景,现代的大部分电梯都可以通过红外,感知到是否有人进入,为了避免夹到人,同时为了等待后面的人,电梯关门的时间往往有这么一种规则:**始终保证电梯门在最后一个人进入后 *3* 秒后关闭。**如果有人进入后,还没有等到 *3* 秒又有人进来了,电梯门会以最后一次进入的时间为计时起点,重新等待3秒。 + +再考虑一个页面上的场景,页面上的某些事件触发频率非常高,比如滚动条滚动、窗口尺寸变化、鼠标移动等,如果我们需要注册这类事件,不得不考虑效率问题,又特别是事件处理中涉及到了大量的操作,比如: + +```js +window.onresize = function(){ + // 大量的 DOM 操作 +} +``` + +当窗口尺寸发生变化时,哪怕只变化了一点点,都有可能造成成百上千次对处理函数的调用,这对网页性能的影响是极其巨大的。 + +于是,我们可以考虑,每次窗口尺寸变化、滚动条滚动、鼠标移动时,不要立即执行相关操作,而是等一段时间,以窗口尺寸停止变化、滚动条不再滚动、鼠标不再移动为计时起点,一段时间后再去执行操作,就像电梯关门那样。 + +再考虑一个搜索的场景(例如百度),当我在一个文本框中输入文字(键盘按下事件)时,需要将文字发送到服务器,并从服务器得到搜索结果,这样的话,用户直接输入搜索文字就可以了,不用再去点搜索按钮,可以提升用户体验,类似于下面的效果: + +5be978627ac99 + + + +上面的效果,我没有点击搜索按钮,也没有按回车键,只是写了一些搜索的文字而已。 + +可是如何来实现上面的场景呢? + +如果文本框的文字每次被改变(键盘按下事件),我都要把数据发送到服务器,得到搜索结果,这是非常恐怖的! + +想想看,我搜索 *“google”* 这样的单词,至少需要按 *6* 次按键,就这一个词,我需要向服务器请求 *6* 次,并让服务器去搜索 *6* 次,但我只需要最后一次的结果就可以了。如果考虑用户按错的情况,发送请求的次数更加恐怖。这样就造成了大量的带宽被占用,浪费了很多资源。 + + + +如何避免这样的问题呢? + + + +仔细观察,你会发现,真正的搜索行为,并不是每次按键都会触发的,只有当用户停止按键一段事件后才会触发。 + +于是,为了满足这种类型场景,我们可以开发一个通用的函数,这个函数要满足以下功能: + +1. 调用该函数后,不立即做事,而是一段时间后去做事 +2. 如果在等待时间内调用了该函数,重新计时 + + + +这样的功能,就叫做函数防抖,其实就是防止函数短时间内被调用多次。 + +要完成该函数,需要给予两个条件: + +1. 告诉我一段时间后要做什么事(这里应该是一个回调函数,即函数作为参数) +2. 告诉我要等待多长时间(毫秒) + + + +下面我们就来封装这么一个函数防抖的通用函数: + +```js +/** + * 函数防抖 + * @param {function} func 一段时间后,要调用的函数 + * @param {number} wait 等待的时间,单位毫秒 + */ +function debounce(func, wait) { + // 设置变量,记录 setTimeout 得到的 id + var timerId = null; + return function (...args) { + if (timerId) { + // 如果有值,说明目前正在等待中,清除它 + clearTimeout(timerId); + } + // 重新开始计时 + timerId = setTimeout(() => { + func(...args); + }, wait); + } +} +``` + + + +下面来进行一个测试,测试如下: + +```html + +``` + +```js +var txt = document.getElementById("txt"); +// 调用 debounce 函数来将事件处理函数变为一个防抖函数 +var debounceHandle = debounce(function(event){ + console.log(event.target.value); +}, 500) +txt.onkeyup = (event)=>{ + debounceHandle(event); +} +``` + + + +效果如下: + +2021-10-01 10.06.44 + + + +## 函数节流 + + + +函数节流的目的,也是为了防止一个函数短时间内被频繁的触发。 + +和函数防抖的原理不同,函数节流的核心思想是让连续的函数执行,变为固定时间段间断地执行。 + +这里做一个形象的的比喻: + +前面我们所介绍的函数防抖,是指频繁触发的情况下,只有足够的空闲时间,才执行代码一次。比如生活中的坐公交,就是一定时间内,如果有人陆续刷卡上车,司机就不会开车。只有别人没刷卡了,司机才开车。 + +而这里我们要介绍的函数节流,指一定时间内函数只执行一次。比如人的眨眼睛,就是一定时间内眨一次。这是函数节流最形象的解释。 + + + +关于节流的实现,有 *2* 种主流的实现方式,一种是**使用时间戳**,一种是**设置定时器**。 + + + +**(1)使用时间戳** + +触发事件时,取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 *0*),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。 + +下面是封装使用时间戳的函数节流的通用函数: + +```js +/** + * + * @param {要进行节流的函数} func + * @param {间隔时间} wait + * @returns + */ +function throttle(func, wait) { + var args; // 存储函数参数 + var previous = 0; // 一开始的默认时间 + return function () { + var now = new Date(); // 获取最新的时间戳 + args = arguments; // 获取参数 + // 进行时间戳的判断,如果超出规定时间,则执行 + if (now - previous > wait) { + func.apply(null, args); + previous = now; + } + } +} +``` + +下面来实际使用测试一下: + +```html + +``` + +```js +var txt = document.getElementById("txt"); +// 调用 throttle 函数来将事件处理函数变为一个节流函数 +var throttleHandle = throttle(function (event) { + console.log(event.target.value); +}, 1000) +txt.onkeyup = (event) => { + throttleHandle(event); +} +``` + + + +效果如下: + +2021-10-01 10.27.15 + + + +**(2)设置定时器** + +第二种方式是设置定时器,触发事件时设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。 + +下面是封装使用定时器的函数节流的通用函数: + +```js +/** + * + * @param {要节流执行的函数} func + * @param {节流的时间间隔} wait + * @returns + */ +function throttle(func, wait) { + // timeout 存储计时器返回值 + // args 存储参数 + var timeout, args; + return function () { + args = arguments; + // 如果 timeout 有值,说明上一次的执行间隔时间还没过 + if (!timeout) { + // 进入此 if 说明时间间隔已经过了 + // 先执行一次要执行的函数 + func.apply(null, args) + // 然后重新设置时间间隔 + timeout = setTimeout(function () { + timeout = null; + }, wait); + } + } +} +``` + + + +## 真题解答 + + + +- 防抖,节流是什么,如何实现 (字节) + +> 参考答案: +> +> 我们在平时开发的时候,会有很多场景会频繁触发事件,比如说搜索框实时发请求,*onmousemove、resize、onscroll* 等,有些时候,我们并不能或者不想频繁触发事件,这时候就应该用到函数防抖和函数节流。 +> +> 函数防抖(*debounce*),指的是短时间内多次触发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行。 +> +> 具体实现如下: +> +> ```js +> /** +> * 函数防抖 +> * @param {function} func 一段时间后,要调用的函数 +> * @param {number} wait 等待的时间,单位毫秒 +> */ +> function debounce(func, wait) { +> // 设置变量,记录 setTimeout 得到的 id +> var timerId = null; +> return function (...args) { +> if (timerId) { +> // 如果有值,说明目前正在等待中,清除它 +> clearTimeout(timerId); +> } +> // 重新开始计时 +> timerId = setTimeout(() => { +> func(...args); +> }, wait); +> } +> } +> ``` +> +> 函数节流(*throttle*),指连续触发事件但是在 *n* 秒中只执行一次函数。即 *2n* 秒内执行 *2* 次... 。 +> +> 节流如字面意思,会稀释函数的执行频率。 +> +> 下面是使用时间戳方式的具体实现: +> +> ```js +> /** +> * +> * @param {要进行节流的函数} func +> * @param {间隔时间} wait +> * @returns +> */ +> function throttle(func, wait) { +> var args; // 存储函数参数 +> var previous = 0; // 一开始的默认时间 +> return function () { +> var now = new Date(); // 获取最新的时间戳 +> args = arguments; // 获取参数 +> // 进行时间戳的判断,如果超出规定时间,则执行 +> if (now - previous > wait) { +> func.apply(null, args); +> previous = now; +> } +> } +> } +> ``` + + + +-*EOF*- + diff --git a/20. 垃圾回收与内存泄漏/垃圾回收与内存泄漏.md b/20. 垃圾回收与内存泄漏/垃圾回收与内存泄漏.md new file mode 100644 index 0000000..f2acbf3 --- /dev/null +++ b/20. 垃圾回收与内存泄漏/垃圾回收与内存泄漏.md @@ -0,0 +1,182 @@ +# 垃圾回收与内存泄漏 + + + +## 经典真题 + + + +- 请介绍一下 *JavaScript* 中的垃圾回收站机制 + + + +## 什么是内存泄露 + + + +程序的运行需要内存。只要程序提出要求,操作系统或者运行时(*runtime*)就必须供给内存。 + +对于持续运行的服务进程(*daemon*),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。 + +也就是说,不再用到的内存,如果没有及时释放,就叫做内存泄漏(*memory leak*)。 + + + +## *JavaScript* 中的垃圾回收 + + + +浏览器的 *Javascript* 具有自动垃圾回收机制(*GC*:*Garbage Collecation*),也就是说,执行环境会负责管理代码执行过程中使用的内存。其原理是:**垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存**。 + +但是这个过程不是实时的,因为其开销比较大并且 *GC* 时停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行。 + +不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后在函数中使用这些变量,直至函数结束,而闭包中由于内部函数的原因,外部函数并不能算是结束。 + + + +下面是一段示例代码: + +```js +function fn1() { + var obj = {name: 'zhangsan', age: 10}; +} +function fn2() { + var obj = {name:'zhangsan', age: 10}; + return obj; +} + +var a = fn1(); +var b = fn2(); +``` + +在上面的代码中,我们首先声明了两个函数,分别叫做 *fn1* 和 *fn2*。 + +当 *fn1* 被调用时,进入 *fn1* 的环境,会开辟一块内存存放对象 *{name: 'zhangsan', age: 10}*。而当调用结束后,出了 *fn1* 的环境,那么该块内存会被 *JavaScript* 引擎中的垃圾回收器自动释放; + +在 *fn2* 被调用的过程中,返回的对象被全局变量 *b* 所指向,所以该块内存并不会被释放。 + +这里问题就出现了:到底哪个变量是没有用的? + +所以垃圾收集器必须跟踪到底哪个变量没用,对于不再有用的变量打上标记,以备将来收回其内存。 + +用于标记的无用变量的策略可能因实现而有所区别,通常情况下有两种实现方式:**标记清除**和**引用计数**。 + +引用计数不太常用,标记清除较为常用。 + + + +## 标记清除 + + + +*JavaScript* 中最常用的垃圾回收方式就是标记清除。 + +当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。 + +从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。 + +而当变量离开环境时,则将其标记为“离开环境”。 + +```js +function test(){ + var a = 10 ; // 被标记 ,进入环境 + var b = 20 ; // 被标记 ,进入环境 +} +test(); // 执行完毕 之后 a、b 又被标离开环境,被回收。 +``` + +垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。 + +然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包)。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。 + +最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。 + +到目前为止,*IE9+、Firefox、Opera、Chrome、Safari* 的 *JS* 实现使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同。 + + + +## 引用计数 + + + +引用计数的含义是跟踪记录每个值被引用的次数。 + +当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 *1*。如果同一个值又被赋给另一个变量,则该值的引用次数加 *1*。 + +相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 *1*。当这个值的引用次数变成 *0* 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。 + +这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为 *0* 的值所占用的内存。 + +```js +function test() { + var a = {}; // a 指向对象的引用次数为 1 + var b = a; // a 指向对象的引用次数加 1,为 2 + var c = a; // a 指向对象的引用次数再加 1,为 3 + var b = {}; // a 指向对象的引用次数减 1,为 2 +} +``` + +*Netscape Navigator3* 是最早使用引用计数策略的浏览器,但很快它就遇到一个严重的问题:**循环引用**。 + +循环引用指的是对象 *A* 中包含一个指向对象B的指针,而对象 *B* 中也包含一个指向对象 *A* 的引用。 + +```js +function fn() { + var a = {}; + var b = {}; + a.pro = b; + b.pro = a; +} +fn(); +``` + +以上代码 *a* 和 *b* 的引用次数都是 *2*,*fn* 执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为 *a* 和 *b* 的引用次数不为 *0*,所以不会被垃圾回收器回收内存,如果 *fn* 函数被大量调用,就会造成内存泄露。在 *IE7* 与 *IE8* 上,内存直线上升。 + + + +## 真题解答 + + + +- 请介绍一下 *JavaScript* 中的垃圾回收站机制 + +> 参考答案: +> +> *JavaScript* 具有自动垃圾回收机制。垃圾收集器会按照固定的时间间隔周期性的执行。 +> +> *JavaScript* 常见的垃圾回收方式:**标记清除**、**引用计数**方式。 +> +> 1、标记清除方式: +> +> - 工作原理:当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。 +> +> - 工作流程: +> +> - 垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记; +> +> - 去掉环境中的变量以及被环境中的变量引用的变量的标记; +> +> - 被加上标记的会被视为准备删除的变量; +> +> - 垃圾回收器完成内存清理工作,销毁那些带标记的值并回收他们所占用的内存空间。 +> +> 2、引用计数方式: +> +> - 工作原理:跟踪记录每个值被引用的次数。 +> +> - 工作流程: +> +> - 声明了一个变量并将一个引用类型的值赋值给这个变量,这个引用类型值的引用次数就是 *1*; +> +> - 同一个值又被赋值给另一个变量,这个引用类型值的引用次数加 *1*; +> +> - 当包含这个引用类型值的变量又被赋值成另一个值了,那么这个引用类型值的引用次数减 *1*; +> +> - 当引用次数变成 *0* 时,说明没办法访问这个值了; +> +> - 当垃圾收集器下一次运行时,它就会释放引用次数是 *0* 的值所占的内存。 + + + +-*EOF*- + diff --git a/21. WeakSet和WeakMap/WeakSet 和 WeakMap.js b/21. WeakSet和WeakMap/WeakSet 和 WeakMap.js new file mode 100644 index 0000000..7b12f7f --- /dev/null +++ b/21. WeakSet和WeakMap/WeakSet 和 WeakMap.js @@ -0,0 +1,101 @@ +// 创建普通对象,添加属性 +// var obj = {}; // ===> new Object(); +// obj.name = "zhangsan"; + +// var m = new Map(); +// console.log(m); +// 添加键 +// m.set("name", "xiejie"); +// console.log(m); +// m.set("name","zhangsan"); +// console.log(m); + +// 通过一个二维数组,可以快速的初始化一个 map +// var arr = [ +// [true,"zhangsan"], +// [1,18], +// [function(){},"male"], +// ]; +// var m2 = new Map(arr); +// console.log(m2); +// console.log(m2.size); +// console.log(m2.get('name')); + +// 使用 has 方法查询某个键是否存在 +// console.log(m.has("name")); + +// var obj = { +// name : "zhangsan", +// age : 18, +// gender : "male" +// } + +// for(var i in obj){ +// console.log(obj[i]); +// } + +// console.log(m2); +// for(var i of m2){ +// console.log(i); +// } + + +// var wm = new WeakMap(); +// wm.set({"name":"xiejie"},2); +// // console.log(wm); +// wm.forEach((item)=>{ +// console.log(item); +// }) + +// const map = new Map(); +// map.set('name', 'john'); +// map.set('phone', 'iPhone'); +// map.forEach(item=>{ +// console.log(item); +// }) +// // john +// // iPhone + + +// set 基本用法 +// var s = new Set(); +// console.log(s); +// s.add(123); +// console.log(s); + +// set 不允许添加相同的值 + +// s.add(123); +// console.log(s); + +// var arr = [1,2,3,4,5,6,7]; +// var s = new Set(arr); +// console.log(s); + +// 利用 set 快速来为数组去重 +// var arr = [1,2,2,4,3,3,5,2,1,4,5,2,6] +// arr = [...new Set(arr)]; + +// var arr = [1,2,3,4,5,6,7]; +// var s = new Set(arr); +// console.log(s.delete(1)); +// console.log(s); +// s.clear(); +// console.log(s); + +// 并集 +// var arr1 = [1, 2, 3] +// var arr2 = [2, 3, 4] +// var newArr = [...new Set([...arr1, ...arr2])] +// console.log(newArr); + +// 交集 +var arr1 = [1, 2, 3] +var arr2 = [2, 3, 4] +var set1 = new Set(arr1) +var set2 = new Set(arr2) +var newArr = [] +set1.forEach(item => { + set2.has(item) ? newArr.push(item) : '' +}) +console.log(newArr) \ No newline at end of file diff --git a/21. WeakSet和WeakMap/WeakSet 和 WeakMap.md b/21. WeakSet和WeakMap/WeakSet 和 WeakMap.md new file mode 100644 index 0000000..3bcedc2 --- /dev/null +++ b/21. WeakSet和WeakMap/WeakSet 和 WeakMap.md @@ -0,0 +1,552 @@ +# *WeakSet* 和 *WeakMap* + + + +## 经典真题 + + + +- 是否了解 *WeakMap、WeakSet*(美团 *19* 年) + + + +## 从对象开始说起 + + + +首先我们从大家都熟悉的对象开始说起。 + +对于对象的使用,大家其实是非常熟悉的,所以我们这里仅简单的过一遍。 + +```js +const algorithm = { site: "leetcode" }; +console.log(algorithm.site); // leetcode + +for (const key in algorithm) { + console.log(key, algorithm[key]); +} + +// site leetcode +delete algorithm.site; +console.log(algorithm.site); // undefined +``` + +在上面的代码中,我们有一个 *algorithm* 对象,它的 *key* 和 *value* 是一个字符串类型的值,之后通过点( . )进行值的访问。 + +另外,*for-in* 循环也很适合在对象中循环。可以使用中括号( [ ] )访问其键对应的值。但是不能使用 *for-of* 循环,因为对象是不可迭代的。 + +对象的属性可以用 *delete* 关键字来删除。 + + + +好的,我们已经快速讨论了有关对象的一些事项: + +- 如何添加属性 +- 如何遍历对象 +- 如何删除属性 + + + +关于对象的讨论暂时就到这儿。 + + + +## *Map* + + + +*Map* 是 *JavaScript* 中新的集合对象,其功能类似于对象。但是,与常规对象相比,存在一些主要差异。 + +首先,让我们看一个创建 *Map* 对象的简单示例。 + + + +### 添加属性 + + + +首先,通过 *Map* 构造函数,我们可以创建一个 *Map* 实例对象出来,如下: + +```js +const map = new Map(); +// Map(0) {} +``` + +*Map* 有一种特殊的方法可在其中添加称为 *set* 的属性。它有两个参数:键是第一个参数,值是第二个参数。 + +```js +map.set('name', 'john'); +// Map(1) {"name" => "john"} +``` + +但是,它不允许你在其中添加现有数据。如果 *Map* 对象中已经存在与新数据的键对应的值,则不会添加新数据。 + +```js +map.set('phone', 'iPhone'); +// Map(2) {"name" => "john", "phone" => "iPhone"} +map.set('phone', 'iPhone'); +// Map(2) {"name" => "john", "phone" => "iPhone"} +``` + +但是可以用其他值覆盖现有数据。 + +```js +map.set('phone', 'Galaxy'); +// Map(2) {"name" => "john", "phone" => "Galaxy"} +``` + +二维数组和 *Map* 对象之间可以很方便的相互转换。例如: + +```js +var arr = [ + [1, 2], + [3, 4], + [5, 6], +]; + +var map = new Map(arr); +console.log(map); //Map { 1 => 2, 3 => 4, 5 => 6 } +console.log(Array.from(map)); //[ [ 1, 2 ], [ 3, 4 ], [ 5, 6 ] ] +``` + + + +### 获取属性和长度 + + + +可以通过 *get* 方法或者 *Map* 对象某一条属性的值: + +```js +const map = new Map(); +map.set('name', 'john'); +map.set('phone', 'iPhone'); +console.log(map.get('phone')); // iPhone +``` + + + +可以通过 *has* 方法来查询是否具有某一条属性: + +```js +const map = new Map(); +map.set('name', 'john'); +map.set('phone', 'iPhone'); +console.log(map.has('phone')); // true +``` + + + +可以通过 *size* 属性获取 *Map* 对象的长度: + +```js +const map = new Map(); +map.set('name', 'john'); +map.set('phone', 'iPhone'); +console.log(map.size); // 2 +``` + + + +### 遍历 *Map* 对象 + + + +*Map* 是一个可迭代的对象,这意味着可以使用 *for-of* 语句将其映射。 + +*Map* 以数组形式提供数据,要获取键或值则需要解构数组或以索引的方式来进行访问。 + +```js +for (const item of map) { + console.dir(item); +} +// Array(2) ["name", "john"] +// Array(2) ["phone", "Galaxy"] +``` + + + +要仅获取键或值,还有一些方法可供使用。 + +```js +map.keys(); +// MapIterator {"name", "phone"} +map.values(); +// MapIterator {"john", "Galaxy"} +map.entries(); +// MapIterator {"name" => "john", "phone" => "Galaxy"} +``` + + + +也可以使用 *forEach* 方法,例如: + +```js +const map = new Map(); +map.set('name', 'john'); +map.set('phone', 'iPhone'); +map.forEach(item=>{ + console.log(item); +}) +// john +// iPhone +``` + + + +可以使用展开操作符( ... )来获取 *Map* 的全部数据,因为展开操作符还可以在幕后与可迭代对象一起工作。 + +```js +const simpleSpreadedMap = [...map]; +// [Array(2), Array(2)] +``` + + + +### 删除属性 + + + +从 *Map* 对象中删除数据也很容易,你所需要做的就是调用 *delete*。 + +```js +map.delete('phone'); +// true +map.delete('fake'); +// false +``` + +*delete* 返回布尔值,该布尔值指示 *delete* 函数是否成功删除了数据。如果是,则返回 *true*,否则返回 *false*。 + + + +如果要清空整个 *Map* 对象,可以使用 *clear* 方法,如下: + +```js +const map = new Map(); +map.set('name', 'john'); +map.set('phone', 'iPhone'); +console.log(map); // Map(2) { 'name' => 'john', 'phone' => 'iPhone' } +map.clear(); +console.log(map); // Map(0) {} +``` + + + +### *Map* 和 *Object* 的区别 + + + +关于 *Map* 和 *Object* 的区别,可以参阅下表: + +![image-20210930183632548](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-09-30-103632.png) + + + +## *WeakMap* + + + +*WeakMap* 起源于 *Map*,因此它们彼此非常相似。但是,*WeakMap* 具有很大的不同。 + +*WeakMap* 的名字是怎么来的呢? + +嗯,是因为它与它的引用链接所指向的数据对象的连接或关系没有 *Map* 的连接或关系那么强,所以它是弱的。 + +那么,这到底是什么意思呢? + + + +**差异 *1*:*key* 必须是对象** + +可以将任何值作为键传入 *Map* 对象,但 *WeakMap* 不同,它只接受一个对象作为键,否则,它将返回一个错误。 + +```js +const John = { name: 'John' }; +const weakMap = new WeakMap(); +weakMap.set(John, 'student'); +// WeakMap {{...} => "student"} +weakMap.set('john', 'student'); +// Uncaught TypeError: Invalid value used as weak map key +``` + + + +**差异 *2*:并非 *Map* 中的所有方法都支持** + +*WeakMap* 可以使用的方法如下: + +- *delete* +- *get* +- *has* +- *set* + +还有一个最大的不同是 *WeakMap* 不支持迭代对象的方法。 + + + +**差异 *3*:当 *GC* 清理引用时,数据会被删除** + +这是和 *Map* 相比最大的不同。 + +例如: + +```js +let John = { major: "math" }; + +const map = new Map(); +const weakMap = new WeakMap(); + +map.set(John, 'John'); +weakMap.set(John, 'John'); + +John = null; +/* John 被垃圾收集 */ +``` + +当 *John* 对象被垃圾回收时,*Map* 对象将保持引用链接,而 *WeakMap* 对象将丢失链接。 + +所以当你使用 *WeakMap* 时,你应该考虑这个特点。 + + + +## *Set* + + + +*Set* 也非常类似于 *Map*,但是 *Set* 对于单个值更有用。 + + + +### 添加属性 + + + +使用 *add* 方法可以添加属性。 + +```js +const set = new Set(); + +set.add(1); +set.add('john'); +set.add(BigInt(10)); +// Set(4) {1, "john", 10n} +``` + +与 *Map* 一样,*Set* 也不允许添加相同的值。 + +```js +set.add(5); +// Set(1) {5} + +set.add(5); +// Set(1) {5} +``` + +对于原始数据类型(*boolean、number、string、null、undefined*),如果储存相同值则只保存一个,对于引用类型,引用地址完全相同则只会存一个。 + +- *+0* 与 *-0* 在存储判断唯一性的时候是恒等的,所以不可以重复。 +- *undefined* 和 *undefined* 是恒等的,所以不可以重复。 +- *NaN* 与 *NaN* 是不恒等的,但是在 *Set* 中只能存一个不能重复。 + + + +### 遍历对象 + + + +由于 *Set* 是一个可迭代的对象,因此可以使用 *for-of* 或 *forEach* 语句。 + +```js +for (const val of set) { + console.dir(val); +} +// 1 +// 'John' +// 10n +// 5 + +set.forEach(val => console.dir(val)); +// 1 +// 'John' +// 10n +// 5 +``` + + + +### 删除属性 + + + +这一部分和 *Map* 的删除完全一样。如果数据被成功删除,它返回 *true*,否则返回 *false*。 + +当然也可以使用 clear 方法清空 *Set* 集合。 + +```js +set.delete(5); +// true +set.delete(function(){}); +// false; + +set.clear(); +``` + +如果你不想将相同的值添加到数组表单中,则 *Set* 可能会非常有用。 + +```js +/* With Set */ +const set = new Set(); +set.add(1); +set.add(2); +set.add(2); +set.add(3); +set.add(3); +// Set {1, 2, 3} + +// Converting to Array +const arr = [ ...set ]; +// [1, 2, 3] + +Object.prototype.toString.call(arr); +// [object Array] + +/* Without Set */ +const hasSameVal = val => ar.some(v === val); +const ar = []; + +if (!hasSameVal(1)) ar.push(1); +if (!hasSameVal(2)) ar.push(2); +if (!hasSameVal(3)) ar.push(3); +``` + + + +### 应用场景 + + + +接下来来看一下 *Set* 常见的应用场景: + +```js +//数组去重 +...new Set([1,1,2,2,3]) + +//并集 +var arr1 = [1, 2, 3] +var arr2 = [2, 3, 4] +var newArr = [...new Set([...arr1, ...arr2])] +//交集 +var arr1 = [1, 2, 3] +var arr2 = [2, 3, 4] +var set1 = new Set(arr1) +var set2 = new Set(arr2) +var newArr = [] +set1.forEach(item => { + set2.has(item) ? newArr.push(item) : '' +}) +console.log(newArr) +//差集 +var arr1 = [1, 2, 3] +var arr2 = [2, 3, 4] +var set1 = new Set(arr1) +var set2 = new Set(arr2) +var newArr = [] +set1.forEach(item => { + set2.has(item) ? '' : newArr.push(item) +}) +set2.forEach(item => { + set1.has(item) ? '' : newArr.push(item) +}) +console.log(newArr) +``` + + + +## *WeakSet* + + + +*WeakSet* 和 *Set* 区别如下: + +- *WeakSet* 只能储存对象引用,不能存放值,而 *Set* 对象都可以 +- *WeakSet* 对象中储存的对象值都是被弱引用的,即垃圾回收机制不考虑 *WeakSet* 对该对象的引用,如果没有其他的变量或者属性引用这个对象值,则这个对象将会被垃圾回收掉。(不考虑该对象还存在与 *WeakSet* 中),所以 *WeakSet* 对象里有多少个成员元素,取决于垃圾回收机制有没有运行,运行前后成员个数可能不一致,遍历结束之后,有的成员可能取不到,被垃圾回收了。因此 *ES6* 规定,*WeakSet* 对象是无法被遍历的,也没有办法拿到它包含的所有元素。 + + + +*WeakSet* 能够使用的方法如下: + +- *add(value)* 方法:在 *WeakSet* 中添加一个元素。如果添加的元素已存在,则不会进行操作。 +- *delete(value)* 方法:删除元素 *value* +- *has(value)* 方法:判断 *WeakSet* 对象中是否包含 *value* +- *clear( )* 方法:清空所有元素 + + + +下面来看一下 *WeakSet* 的代码示例,与 *WeakMap* 一样,*WeakSet* 也将丢失对内部数据的访问链接(如果内部数据已被垃圾收集)。 + +```js +let John = { major: "math" }; + +const set = new Set(); +const weakSet = new WeakSet(); + +set.add(John); +// Set {{...}} +weakSet.add(John); +// WeakSet {{...}} + +John = null; +/* John 被垃圾收集 */ +``` + +一旦对象 *John* 被垃圾回收,*WeakSet* 就无法访问其引用 *John* 的数据。而且 *WeakSet* 不支持 *for-of* 或 *forEach*,因为它不可迭代。 + + + +## 比较总结 + + + +- *Map* + - 键名唯一不可重复 + - 类似于集合,键值对的集合,任何值都可以作为一个键或者一个值 + - 可以遍历,可以转换各种数据格式,方法 *get、set、has、delete* +- *WeakMap* + - 只接受对象为键名,不接受其他类型的值作为键名,键值可以是任意 + - 键名是拖引用,键名所指向的对象,会被垃圾回收机制回收 + - 不能遍历,方法 *get、set、has、delete* + +- *Set* + - 成员唯一,无序且不会重复 + - 类似于数组集合,键值和键名是一致的(只有键值。没有键名) + - 可以遍历,方法有 *add、delete、has* +- *WeakSet* + - 只能存储对应引用,不能存放值 + - 成员都是弱引用,会被垃圾回收机制回收 + - 不能遍历,方法有 *add、delete、has* + + + +## 真题解答 + + + +- 是否了解 *WeakMap、WeakSet*(美团 *19* 年) + +> 参考答案: +> +> *WeakSet* 对象是一些对象值的集合, 并且其中的每个对象值都只能出现一次。在 *WeakSet* 的集合中是唯一的 +> +> 它和 *Set* 对象的区别有两点: +> +> - 与 *Set* 相比,*WeakSet* 只能是**对象的集合**,而不能是任何类型的任意值。 +> - *WeakSet* 持弱引用:集合中对象的引用为弱引用。 如果没有其他的对 *WeakSet* 中对象的引用,那么这些对象会被当成垃圾回收掉。 这也意味着 *WeakSet* 中没有存储当前对象的列表。 正因为这样,*WeakSet* 是不可枚举的。 +> +> *WeakMap* 对象也是键值对的集合。它的**键必须是对象类型**,值可以是任意类型。它的键被弱保持,也就是说,当其键所指对象没有其他地方引用的时候,它会被 *GC* 回收掉。*WeakMap* 提供的接口与 *Map* 相同。 +> +> 与 *Map* 对象不同的是,*WeakMap* 的键是不可枚举的。不提供列出其键的方法。列表是否存在取决于垃圾回收器的状态,是不可预知的。 + + + +-*EOF*- \ No newline at end of file diff --git a/22. 深浅拷贝/深浅拷贝.html b/22. 深浅拷贝/深浅拷贝.html new file mode 100644 index 0000000..def37c3 --- /dev/null +++ b/22. 深浅拷贝/深浅拷贝.html @@ -0,0 +1,231 @@ + + + + + + + + Document + + + + + + + + \ No newline at end of file diff --git a/22. 深浅拷贝/深浅拷贝.md b/22. 深浅拷贝/深浅拷贝.md new file mode 100644 index 0000000..bf3c5f9 --- /dev/null +++ b/22. 深浅拷贝/深浅拷贝.md @@ -0,0 +1,532 @@ +# 深浅拷贝 + + + +## 经典真题 + + + +- 深拷贝和浅拷贝的区别?如何实现 + + + +## 深拷贝和浅拷贝概念 + + + +首先,我们需要明确深拷贝和浅拷贝的概念。 + + + +- **浅拷贝**:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝(浅复制)。浅拷贝只复制指向某个对象的指针(引用地址),而不复制对象本身,新旧对象还是共享同一块内存。 + + + +- **深拷贝**:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。 + + + +## 浅拷贝方法 + + + +接下来我们来看一下对象有哪些浅拷贝方法。 + + + +**1. 直接赋值** + +直接赋值是最常见的一种浅拷贝方式。例如: + + + +```js +var stu = { + name: 'xiejie', + age: 18 +} +// 直接赋值 +var stu2 = stu; +stu2.name = "zhangsan"; +console.log(stu); // { name: 'zhangsan', age: 18 } +console.log(stu2); // { name: 'zhangsan', age: 18 } +``` + + + +**2. *Object.assign* 方法** + +我们先来看一下 *Object.assign* 方法的基本用法。 + +该方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。 + +如下: + +```js +var stu = { + name: 'xiejie' +} +var stu2 = Object.assign(stu, { age: 18 }, { gender: 'male' }) +console.log(stu2); // { name: 'xiejie', age: 18, gender: 'male' } +``` + +在上面的代码中,我们有一个对象 *stu*,然后使用 *Object.assign* 方法将后面两个对象的属性值分配到 *stu* 目标对象上面。 + +最终得到 *{ name: 'xiejie', age: 18, gender: 'male' }* 这个对象。 + + + +通过这个方法,我们就可以实现一个对象的拷贝。例如: + +```js +const stu = { + name: 'xiejie', + age: 18 +} +const stu2 = Object.assign({}, stu) +stu2.name = 'zhangsan'; +console.log(stu); // { name: 'xiejie', age: 18 } +console.log(stu2); // { name: 'zhangsan', age: 18 } +``` + +在上面的代码中,我们使用 *Object.assign* 方法来对 *stu* 方法进行拷贝,并且可以看到修改拷贝后对象的值,并没有影响原来的对象,这仿佛实现了一个深拷贝。 + + + +然而,*Object.assign* 方法事实上是一个浅拷贝。 + +当对象的属性值对应的是一个对象时,该方法拷贝的是对象的属性的引用,而不是对象本身。 + +例如: + +```js +const stu = { + name: 'xiejie', + age: 18, + stuInfo: { + No: 1, + score: 100 + } +} +const stu2 = Object.assign({}, stu) +stu2.name = 'zhangsan'; +stu2.stuInfo.score = 90; +console.log(stu); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 90 } } +console.log(stu2); // { name: 'zhangsan', age: 18, stuInfo: { No: 1, score: 90 } } +``` + + + +**3. *ES6* 扩展运算符** + +首先我们还是来回顾一下 *ES6* 扩展运算符的基本用法。 + +ES6 扩展运算符可以将数组表达式或者 *string* 在语法层面展开,还可以在构造字面量对象时,将对象表达式按 *key-value* 的方式展开。 + +例如: + +```js +var arr = [1, 2, 3]; +var arr2 = [3, 5, 8, 1, ...arr]; // 展开数组 +console.log(arr2); // [3, 5, 8, 1, 1, 2, 3] + +var stu = { + name: 'xiejie', + age: 18 +} +var stu2 = { ...stu, score: 100 }; // 展开对象 +console.log(stu2); // { name: 'xiejie', age: 18, score: 100 } +``` + + + +接下来我们来使用扩展运算符来实现对象的拷贝,如下: + +```js +const stu = { + name: 'xiejie', + age: 18 +} +const stu2 = {...stu} +stu2.name = 'zhangsan'; +console.log(stu); // { name: 'xiejie', age: 18 } +console.log(stu2); // { name: 'zhangsan', age: 18 } +``` + + + +但是和 *Object.assign* 方法一样,如果对象中某个属性对应的值为引用类型,那么直接拷贝的是引用地址。如下: + +```js +const stu = { + name: 'xiejie', + age: 18, + stuInfo: { + No: 1, + score: 100 + } +} +const stu2 = {...stu} +stu2.name = 'zhangsan'; +stu2.stuInfo.score = 90; +console.log(stu); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 90 } } +console.log(stu2); // { name: 'zhangsan', age: 18, stuInfo: { No: 1, score: 90 } } +``` + + + +**4. 数组的 *slice* 和 *concat* 方法** + +在 *javascript* 中,数组也是一种对象,所以也会涉及到深浅拷贝的问题。 + +在 *Array* 中的 *slice* 和 *concat* 方法,不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。 + +例如: + +```js +// concat 拷贝数组 +var arr1 = [1, true, 'Hello']; +var arr2 = arr1.concat(); +console.log(arr1); // [ 1, true, 'Hello' ] +console.log(arr2); // [ 1, true, 'Hello' ] + +arr2[0] = 2; +console.log(arr1); // [ 1, true, 'Hello' ] +console.log(arr2); // [ 2, true, 'Hello' ] +``` + +```js +// slice 拷贝数组 +var arr1 = [1, true, 'Hello']; +var arr2 = arr1.slice(); +console.log(arr1); // [ 1, true, 'Hello' ] +console.log(arr2); // [ 1, true, 'Hello' ] + +arr2[0] = 2; +console.log(arr1); // [ 1, true, 'Hello' ] +console.log(arr2); // [ 2, true, 'Hello' ] +``` + + + +但是,这两个方法仍然是浅拷贝。如果一旦涉及到数组里面的元素是引用类型,那么这两个方法是直接拷贝的引用地址。如下: + +```js +// concat 拷贝数组 +var arr1 = [1, true, 'Hello', { name: 'xiejie', age: 18 }]; +var arr2 = arr1.concat(); +console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ] +console.log(arr2); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ] + +arr2[0] = 2; +arr2[3].age = 19; +console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 19 } ] +console.log(arr2); // [ 2, true, 'Hello', { name: 'xiejie', age: 19 } ] +``` + +```js +// concat 拷贝数组 +var arr1 = [1, true, 'Hello', { name: 'xiejie', age: 18 }]; +var arr2 = arr1.slice(); +console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ] +console.log(arr2); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ] + +arr2[0] = 2; +arr2[3].age = 19; +console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 19 } ] +console.log(arr2); // [ 2, true, 'Hello', { name: 'xiejie', age: 19 } ] +``` + + + +**5. *jQuery* 中的 *$.extend*** + +在 *jQuery* 中,*$.extend(deep,target,object1,objectN)* 方法可以进行深浅拷贝。各参数说明如下: + +- *deep*:如过设为 *true* 为深拷贝,默认是 *false* 浅拷贝 +- *target*:要拷贝的目标对象 +- *object1*:待拷贝到第一个对象的对象 +- *objectN*:待拷贝到第N个对象的对象 + +来看一个具体的示例: + +```js + + + + +``` + + + +效果: + + + +image-20210831133219541 + + + + + +## 深拷贝方法 + + + +说完了浅拷贝,接下来我们来看如何实现深拷贝。 + +总结一下,大致有如下的方式。 + + + +**1. *JSON.parse(JSON.stringify)*** + +这是一个广为流传的深拷贝方式,用 *JSON.stringify* 将对象转成 *JSON* 字符串,再用 *JSON.parse* 方法把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。 + +示例如下: + +```js +const stu = { + name: 'xiejie', + age: 18, + stuInfo: { + No: 1, + score: 100 + } +} +const stu2 = JSON.parse(JSON.stringify(stu)); +stu2.name = 'zhangsan'; +stu2.stuInfo.score = 90; +console.log(stu); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 100 } } +console.log(stu2); // { name: 'zhangsan', age: 18, stuInfo: { No: 1, score: 90 } } +``` + + + +这种方式看似能够解决问题,但是这种方法也有一个缺点,那就是不能处理函数。 + +这是因为 *JSON.stringify* 方法是将一个 *javascript* 值(对象或者数组)转换为一个 *JSON* 字符串,而 *JSON* 字符串是不能够接受函数的。同样,正则对象也一样,在 *JSON.parse* 解析时会发生错误。 + +例如: + +```js +const stu = { + name: 'xiejie', + age: 18, + stuInfo: { + No: 1, + score: 100, + saySth: function () { + console.log('我是一个学生'); + } + } +} +const stu2 = JSON.parse(JSON.stringify(stu)); +stu2.name = 'zhangsan'; +stu2.stuInfo.score = 90; +console.log(stu); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 100, saySth: [Function: saySth] }} +console.log(stu2); // { name: 'zhangsan', age: 18, stuInfo: { No: 1, score: 90 } } +``` + +可以看到,在原对象中有方法,拷贝之后,新对象中没有方法了。 + + + +**2. *$.extend(deep,target,object1,objectN)*** + +前面在介绍浅拷贝时提到了 *jQuery* 的这个方法,该方法既能实现浅拷贝,也能实现深拷贝。要实现深拷贝,只需要将第一个参数设置为 *true* 即可。例如: + +```js + + + + +``` + +效果: + + + +image-20210831134114926 + + + + + +**3. 手写递归方法** + +最终,还是只有靠我们自己手写递归方法来实现深拷贝。 + +示例如下: + +```js +function deepClone(target) { + var result; + // 判断是否是对象类型 + if (typeof target === 'object') { + // 判断是否是数组类型 + if (Array.isArray(target)) { + result = []; // 如果是数组,创建一个空数组 + // 遍历数组的键 + for (var i in target) { + // 递归调用 + result.push(deepClone(target[i])) + } + } else if (target === null) { + // 再判断是否是 null + // 如果是,直接等于 null + result = null; + } else if (target.constructor === RegExp) { + // 判断是否是正则对象 + // 如果是,直接赋值拷贝 + result = target; + } else if (target.constructor === Date) { + // 判断是否是日期对象 + // 如果是,直接赋值拷贝 + result = target; + } else { + // 则是对象 + // 创建一个空对象 + result = {}; + // 遍历该对象的每一个键 + for (var i in target) { + // 递归调用 + result[i] = deepClone(target[i]); + } + } + } else { + // 表示不是对象类型,则是简单数据类型 直接赋值 + result = target; + } + // 返回结果 + return result; +} +``` + +在上面的代码中,我们封装了一个名为 *deepClone* 的方法,在该方法中,通过递归调用的形式来深度拷贝一个对象。 + +下面是 *2* 段测试代码: + +```js +// 测试1 +const stu = { + name: 'xiejie', + age: 18, + stuInfo: { + No: 1, + score: 100, + saySth: function () { + console.log('我是一个学生'); + } + } +} +const stu2 = deepClone(stu) +stu2.name = 'zhangsan'; +stu2.stuInfo.score = 90; +console.log(stu); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 100, saySth: [Function: saySth] }} +console.log(stu2); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 90, saySth: [Function: saySth] }} +``` + +```js +// 测试2 +var arr1 = [1, true, 'Hello', { name: 'xiejie', age: 18 }]; +var arr2 = deepClone(arr1) +console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ] +console.log(arr2); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ] + +arr2[0] = 2; +arr2[3].age = 19; +console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ] +console.log(arr2); // [ 2, true, 'Hello', { name: 'xiejie', age: 19 } ] +``` + + + +## 真题解答 + + + +- 深拷贝和浅拷贝的区别?如何实现 + +> 参考答案: +> +> - **浅拷贝**:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝(浅复制) +> +> 浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。 +> +> - **深拷贝**:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。 +> +> **浅拷贝方法** +> +> 1. 直接赋值 +> 2. *Object.assign* 方法:可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。当拷贝的 *object* 只有一层的时候,是深拷贝,但是当拷贝的对象属性值又是一个引用时,换句话说有多层时,就是一个浅拷贝。 +> 3. *ES6* 扩展运算符,当 *object* 只有一层的时候,也是深拷贝。有多层时是浅拷贝。 +> 4. *Array.prototype.concat* 方法 +> 5. *Array.prototype.slice* 方法 +> 6. *jQuery* 中的 *$.extend*:在 *jQuery* 中,*$.extend(deep,target,object1,objectN)* 方法可以进行深浅拷贝。*deep* 如过设为 *true* 为深拷贝,默认是 *false* 浅拷贝。 +> +> **深拷贝方法** +> +> 1. *$.extend(deep,target,object1,objectN)*,将 *deep* 设置为 *true* +> 2. *JSON.parse(JSON.stringify)*:用 *JSON.stringify* 将对象转成 *JSON* 字符串,再用 *JSON.parse* 方法把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。这种方法虽然可以实现数组或对象深拷贝,但不能处理函数。 +> 3. 手写递归 +> +> 示例代码如下: +> +> ```js +> function deepCopy(oldObj, newobj) { +> for (var key in oldObj) { +> var item = oldObj[key]; +> // 判断是否是对象 +> if (item instanceof Object) { +> if (item instanceof Function) { +> newobj[key] = oldObj[key]; +> } else { +> newobj[key] = {}; //定义一个空的对象来接收拷贝的内容 +> deepCopy(item, newobj[key]); //递归调用 +> } +> +> // 判断是否是数组 +> } else if (item instanceof Array) { +> newobj[key] = []; //定义一个空的数组来接收拷贝的内容 +> deepCopy(item, newobj[key]); //递归调用 +> } else { +> newobj[key] = oldObj[key]; +> } +> } +> } +> ``` + + + +-*EOF*- \ No newline at end of file diff --git a/23. 函数柯里化/函数柯里化.js b/23. 函数柯里化/函数柯里化.js new file mode 100644 index 0000000..5ed962a --- /dev/null +++ b/23. 函数柯里化/函数柯里化.js @@ -0,0 +1,116 @@ +// 该函数就是接受不了多个参数 +// function add(x, y) { +// return x + y; +// } +// console.log(add(1, 2)); // 3 +// console.log(add(5, 7)); // 12 + + +// 接下来我们要将其进行柯里化 + +// function add(x){ +// return function(y){ +// return x + y; +// } +// } + +// console.log(add(1)(2)); + +// 固定参数 + +// function check(reg, txt) { +// return reg.test(txt) +// } + +// // 即使是相同的正则表达式,也需要重新传递一次 +// console.log(check(/\d+/g, 'test1')); // true +// console.log(check(/\d+/g, 'testtest')); // false + +// function check(reg){ +// return function(str){ +// return reg.test(str); +// } +// } + +// var func = check(/\d+/g); +// console.log(func('test1')); +// console.log(func('testtest')); +// console.log(func('abc')); + + +// console.log(check(/[a-z]+/g)('test')); + + +// 封装一个通用的柯里化函数 + +// function curry() { +// var fn = arguments[0]; // 拿到要执行的函数 +// var args = Array.prototype.slice.call(arguments, 1); +// // 接下来,我们就需要判断这个参数是否足够 +// if(args.length === fn.length){ +// // 进入此 if,说明第一次参数就是传够了的 +// // 直接执行 fn 函数 +// return fn.apply(this, args); +// } +// // 下面是处理参数不够的情况 +// function _curry(){ +// args.push(...arguments); +// if(args.length === fn.length){ +// return fn.apply(this, args); +// } +// return _curry; +// } +// return _curry; +// } + +// // 测试 1 +// function add(a, b, c) { +// return a + b + c; +// } + +// console.log(curry(add)(1)(2)(3)); // 6 +// console.log(curry(add, 1)(2)(3)); // 6 +// console.log(curry(add, 1, 2, 3)); // 6 +// console.log(curry(add, 1)(3, 4)); // 8 + +// var addCurrying = curry(add)(2); +// console.log(addCurrying(7)(8)); // 17 + +// // 测试 2 +// function check(reg, txt) { +// return reg.test(txt) +// } +// var hasNumber = curry(check)(/\d+/g); +// console.log(hasNumber('test1'));// true + + +// 一道面试题 + +// add(1)(2)(3) = 6; +// add(1, 2, 3)(4) = 10; +// add(1)(2)(3)(4)(5) = 15; + + +function add(){ + // 拿到第一次调用的所有的参数 + var args = Array.prototype.slice.call(arguments); + + // 该函数会被返回,该函数的作用是继续收集参数 + function _adder(){ + args.push(...arguments); + return _adder; + } + + // 当调用 toString 方法的时候,说明我不要再接收参数了 + // 执行计算操作 + _adder.toString = function(){ + return args.reduce((a,b)=>a+b); + } + + return _adder; +} + +console.log(add(1)(2)(3).toString()) +console.log(add(1, 2, 3)(4).toString()) +console.log(add(1)(2)(3,4,5)(6,7).toString()) + diff --git a/23. 函数柯里化/函数柯里化.md b/23. 函数柯里化/函数柯里化.md new file mode 100644 index 0000000..f57ebe4 --- /dev/null +++ b/23. 函数柯里化/函数柯里化.md @@ -0,0 +1,318 @@ +# 函数柯里化 + + + +## 经典真题 + + + +- 什么是函数柯里化? + + + +## 什么是函数柯里化 + + + +在计算机科学中,柯里化(英语:*Currying*),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。 + +这个技术由克里斯托弗·斯特雷奇以逻辑学家哈斯凯尔·加里命名的,尽管它是 *Moses Schönfinkel* 和戈特洛布·弗雷格发明的。 + + + +在直觉上,柯里化声称如果你固定某些参数,你将得到接受余下参数的一个函数。 + +我们姑且叫它返回函数,在调用返回函数的时候,它将判断当前的参数和之前被柯里化函数固定的参数拼起来之后,是否达到了原本函数的参数个数。 + +如果是,则执行原本的函数,得到结果;如果没有达到,则要继续调用柯里化函数来固定目前的参数。 + + + +在理论计算机科学中,柯里化提供了在简单的理论模型中,比如:只接受一个单一参数的 *lambda* 演算中,研究带有多个参数的函数的方式。 + +函数柯里化的对偶是*Uncurrying*,一种使用匿名单参数函数来实现多参数函数的方法。 + + + +## 柯里化快速入门 + + + +接下来,我们来通过一个简单的示例,让大家快速体会函数柯里化。 + +假设我们有一个求取两个数之和的函数: + +```js +function add(x, y) { + return x + y; +} +console.log(add(1, 2)); // 3 +console.log(add(5, 7)); // 12 +``` + +在上面的示例中,我们有一个 *add* 函数,接收两个形参,返回两形参的和。 + +在调用的时候,我们每次也需要传递两个参数。 + + + +现在,我们对其进行柯里化,如下: + +```js +function add(x) { + return function (y) { + return x + y; + } +} +console.log(add(1)(2)); // 3 +console.log(add(5)(7)); // 3 +``` + +在上面的代码中,我们对 *add* 函数进行了柯里化改造,只接受一个参数,但是返回的也不是值了,而是返回一个函数,这个函数也接收一个参数,然后利用闭包的特性,可以访问到最开始传入的 *x* 的值,最终返回 *x* 和 *y* 的和。 + + + +所以,通过上面的这个示例,我们能够体会到前面所说的柯里化函数的特点: + +一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。 + + + +## 函数柯里化实际应用 + + + +通过上面的例子,我们体验到了什么是柯里化函数。 + +但是问题来了,费这么大劲封装一层,到底有什么用处呢? + +没有好处想让我们程序员多干事情是不可能滴,这辈子都不可能。 + + + +所以接下来我们就来看一下函数柯里化的一个实际应用。 + + + +**参数复用** + +就是将相同的参数固定下来。 + +```js +// 正常正则验证字符串 reg.test(txt) + +// 函数封装后 +function check(reg, txt) { + return reg.test(txt) +} + +// 即使是相同的正则表达式,也需要重新传递一次 +console.log(check(/\d+/g, 'test1')); // true +console.log(check(/\d+/g, 'testtest')); // false +console.log(check(/[a-z]+/g, 'test')); // true + +// Currying后 +function curryingCheck(reg) { + return function (txt) { + return reg.test(txt) + } +} + +// 正则表达式通过闭包保存了起来 +var hasNumber = curryingCheck(/\d+/g) +var hasLetter = curryingCheck(/[a-z]+/g) + +console.log(hasNumber('test1')); // true +console.log(hasNumber('testtest')); // false +console.log(hasLetter('21212')); // false +``` + +上面的示例是一个正则的校验,正常来说直接调用 *check* 函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数 *reg* 进行复用,这样别的地方就能够直接调用 *hasNumber、hasLetter* 等函数,让参数能够复用,调用起来也更方便。 + + + +**提前确认** + +```js +/** + * + * @param {要绑定事件的 DOM 元素} element + * @param {绑定什么事件} event + * @param {事件处理函数} handler + */ +var on = function (element, event, handler) { + if (document.addEventListener) { + if (element && event && handler) { + element.addEventListener(event, handler, false); + } + } else { + if (element && event && handler) { + element.attachEvent('on' + event, handler); + } + } +} + +on(div, 'click', function(){}) + + +var on = (function () { + if (document.addEventListener) { + return function (element, event, handler) { + if (element && event && handler) { + element.addEventListener(event, handler, false); + } + }; + } else { + return function (element, event, handler) { + if (element && event && handler) { + element.attachEvent('on' + event, handler); + } + }; + } +})(); + +on(div, 'click', function(){}) + +//换一种写法可能比较好理解一点,上面就是把 isSupport 这个参数给先确定下来了 +var on = function (isSupport, element, event, handler) { + isSupport = isSupport || document.addEventListener; + if (isSupport) { + return element.addEventListener(event, handler, false); + } else { + return element.attachEvent('on' + event, handler); + } +} +on(true, div, 'click', function(){}) +on(true, div, 'click', function(){}) +on(true, div, 'click', function(){}) +``` + +我们在做项目的过程中,封装一些 *DOM* 操作可以说再常见不过,上面第一种写法也是比较常见,但是我们看看第二种写法,它相对于第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断。 + + + +## 封装通用柯里化函数 + + + +接下来我们来封装一个通用的柯里化函数。 + +```js +function curry() { + var fn = arguments[0]; // 获取要执行的函数 + var args = [].slice.call(arguments, 1); // 获取传递的参数,构成一个参数数组 + // 如果传递的参数已经等于执行函数所需的参数数量 + if (args.length === fn.length) { + return fn.apply(this, args) + } + // 参数不够向外界返回的函数 + function _curry(){ + // 推入之前判断 + // 将新接收到的参数推入到参数数组中 + args.push(...arguments); + if(args.length === fn.length){ + return fn.apply(this, args) + } + return _curry; + } + return _curry; +} +``` + +对上面的代码进行测试: + +```js +// 测试 1 +function add(a, b, c) { + return a + b + c; +} + +console.log(curry(add)(1)(2)(3)); // 6 +console.log(curry(add, 1)(2)(3)); // 6 +console.log(curry(add, 1, 2, 3)); // 6 +console.log(curry(add, 1)(3, 4)); // 8 + +var addCurrying = curry(add)(2); +console.log(addCurrying(7)(8)); // 17 + +// 测试 2 +function check(reg, txt) { + return reg.test(txt) +} +var hasNumber = curry(check)(/\d+/g); +console.log(hasNumber('test1'));// true +``` + + + +## 一道经典的柯里化面试题 + + + +实现一个 *add* 方法,使计算结果能够满足如下预期: + +```js +add(1)(2)(3) = 6; +add(1, 2, 3)(4) = 10; +add(1)(2)(3)(4)(5) = 15; +``` + + + +要完成上面的需求,我们就可以使用柯里化函数: + +```js +function add() { + // 第一次执行时,定义一个数组专门用来存储所有的参数 + var _args = Array.prototype.slice.call(arguments); + + // 在内部声明一个函数,利用闭包的特性保存 _args 并收集所有的参数值 + var _adder = function () { + _args.push(...arguments); + return _adder; + }; + + // 这个是最后输出的时候被调用的,return 后面如果是函数体, + // 为了输出函数体字符串会自动调用 toString 方法 + // 利用 toString 隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回 + _adder.toString = function () { + return _args.reduce(function (a, b) { + return a + b; + }); + } + + // 这个 return 是第一次调用的时候返回上面的函数体, + // 这样后面所有的括号再执行的时候就是执行 _adder 函数体 + return _adder; +} +console.log(add(1)(2)(3).toString()); // 6 +console.log(add(1, 2, 3)(4).toString()); // 10 +console.log(add(1)(2)(3)(4)(5).toString()); // 15 +console.log(add(2, 6)(1).toString()); // 9 +``` + + + +## 真题详解 + + + +- 什么是函数柯里化? + +> 参考答案: +> +> 柯里化(*currying*)又称部分求值。一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。 +> +> 举个例子,就是把原本: +> +> *function(arg1,arg2)* 变成 *function(arg1)(arg2)* +> *function(arg1,arg2,arg3)* 变成 *function(arg1)(arg2)(arg3)* +> *function(arg1,arg2,arg3,arg4)* 变成 *function(arg1)(arg2)(arg3)(arg4)* +> +> 总而言之,就是将: +> +> *function(arg1,arg2,…,argn)* 变成 *function(arg1)(arg2)…(argn)* + + + +-*EOF*- \ No newline at end of file diff --git a/24. Node的事件循环/Node的事件循环.html b/24. Node的事件循环/Node的事件循环.html new file mode 100644 index 0000000..72fa70d --- /dev/null +++ b/24. Node的事件循环/Node的事件循环.html @@ -0,0 +1,61 @@ + + + + + + + + Document + + + + + + + + \ No newline at end of file diff --git a/24. Node的事件循环/Node的事件循环.js b/24. Node的事件循环/Node的事件循环.js new file mode 100644 index 0000000..e1a1d71 --- /dev/null +++ b/24. Node的事件循环/Node的事件循环.js @@ -0,0 +1,131 @@ +// const start = Date.now(); // 获取当前的时间戳 + +// setTimeout(function fn1() { +// console.log('setTimeout', Date.now() - start); +// }, 200) + +// const fs = require('fs'); +// fs.readFile('./index.js', 'utf-8', function f2() { +// console.log('文件读取结束'); +// const start = Date.now(); +// // 强行延时 500 毫秒 +// while (Date.now() - start < 500) { } +// }) +// fs.readFile('./index.js', 'utf-8', function f2() { +// console.log('文件读取结束2'); +// const start = Date.now(); +// // 强行延时 500 毫秒 +// while (Date.now() - start < 500) { } +// }) + +// timer 队列:setTimeout +// poll 队列:readFile + +// check 阶段 + +// console.log('start'); +// setTimeout(()=>{ +// console.log('timer1'); +// Promise.resolve().then(function(){ +// console.log('promise1'); +// }) +// },0) +// setTimeout(()=>{ +// console.log('timer2'); +// Promise.resolve().then(function(){ +// console.log('promise2'); +// }) +// },0) +// Promise.resolve().then(function(){ +// console.log('promise3'); +// }) +// console.log('end'); + +// Promise 会被放入到微任务队列 +// 会先清空微任务队列,再执行其他任务队列的回调任务 +// start +// end +// promise3 +// timer1 +// promise1 +// timer2 +// promise2 + +// timer : +// 微任务: + +// setTimeout(function timeout() { +// console.log('timeout'); +// }, 0); +// setImmediate(function immediate() { +// console.log('immediate'); +// }); + +// const fs = require('fs'); +// fs.readFile('./index.js', 'utf-8', function () { +// setTimeout(() => { +// console.log('timeout'); +// }, 0) +// setImmediate(function immediate() { +// console.log('immediate'); +// }); +// }) + +// setTimeout(() => { +// console.log('timer1') +// Promise.resolve().then(function () { +// console.log('promise1') +// }) +// }, 0) +// process.nextTick(() => { +// console.log('nextTick') +// process.nextTick(() => { +// console.log('nextTick') +// process.nextTick(() => { +// console.log('nextTick') +// process.nextTick(() => { +// console.log('nextTick') +// }) +// }) +// }) +// }) + +// nextTick +// nextTick +// nextTick +// nextTick +// timer1 +// promise1 + + +// timers : setTimeout +// 微任务队列: + +setTimeout(()=>{ + console.log('timer1') + Promise.resolve().then(function() { + console.log('promise1') + }) + process.nextTick(() => { + console.log('nexttick'); + }) +}, 0) +setTimeout(()=>{ + console.log('timer2') + Promise.resolve().then(function() { + console.log('promise2') + }) +}, 0) + +// 证明 nextTick 和 Promise 都是放入微任务队列 +// 但是前者比后者的优先级高 + + +// timer:setTimeout2 +// 微任务: + +// timer1 +// nexttick +// promise1 +// timer2 +// promise2 diff --git a/24. Node的事件循环/Node的事件循环.md b/24. Node的事件循环/Node的事件循环.md new file mode 100644 index 0000000..bb94dd3 --- /dev/null +++ b/24. Node的事件循环/Node的事件循环.md @@ -0,0 +1,549 @@ +# *Node* 事件循环 + + + +## 经典真题 + + + +- 请简述一下 *Node.js* 中的事件循环,和浏览器环境的事件循环有何不同? + + + +## *Node.js* 与浏览器的事件循环有何区别? + + + +### 进程与线程 + + + +我们经常说 *JavaScript* 是一门单线程语言,指的是一个进程里只有一个主线程,那到底什么是线程?什么是进程? + +首先需要把这个问题搞明白。 + +进程是 *CPU* 资源分配的最小单位,而线程是 *CPU* 调度的最小单位。举个例子,看下面的图: + +image-20211015112136231 + + + +- 进程好比图中的工厂,有单独的专属自己的工厂资源。 + +- 线程好比图中的工人,多个工人在一个工厂中协作工作,工厂与工人是 *1:n* 的关系。也就是说**一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线**。 + +- 工厂的空间是工人们共享的,这象征**一个进程的内存空间是共享的,每个线程都可用这些共享内存**。 + +- 多个工厂之间独立存在。 + + + +接下来我们回过头来看多进程和多线程的概念: + +- 多进程:在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态。多进程带来的好处是明显的,比如你可以听歌的同时,打开编辑器敲代码,编辑器和听歌软件的进程之间丝毫不会相互干扰。 + + + +- 多线程:程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。 + + + +以 *Chrome* 浏览器中为例,当你打开一个 *Tab* 页时,其实就是创建了一个进程。 + + + +image-20211015112546949 + + + +一个进程中可以有多个线程,比如渲染线程、*JS* 引擎线程、*HTTP* 请求线程等等。当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。 + + + +### 浏览器内核 + + + +简单来说浏览器内核是通过取得页面内容、整理信息(应用 *CSS* )、计算和组合最终输出可视化的图像结果,通常也被称为渲染引擎。 + +浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成: + +- *GUI* 渲染线程 +- *JavaScript* 引擎线程 +- 定时触发器线程 +- 事件触发线程 +- 异步 *http* 请求线程 + + + +#### *GUI* 渲染线程 + + + +- 主要负责页面的渲染,解析 *HTML*、*CSS*,构建 *DOM* 树,布局和绘制等。 +- 当界面需要重绘或者由于某种操作引发回流时,将执行该线程。 +- 该线程与 *JS* 引擎线程互斥,当执行 *JS* 引擎线程时,*GUI* 渲染会被挂起,当任务队列空闲时,主线程才会去执行 *GUI* 渲染。 + + + +#### *JavaScript* 引擎线程 + + + +- 该线程当然是主要负责处理 *JavaScript* 脚本,执行代码。 +- 也是主要负责执行准备好待执行的事件,即定时器计数结束,或者异步请求成功并正确返回时,将依次进入任务队列,等待 *JS* 引擎线程的执行。 +- 当然,该线程与 *GUI* 渲染线程互斥,当 *JS* 引擎线程执行 *JavaScript* 脚本时间过长,将导致页面渲染的阻塞。 + + + +#### 定时触发器线程 + + + +- 负责执行异步定时器一类的函数的线程,如:*setTimeout、setInterval*。 +- 主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待 *JS* 引擎线程执行。 + + + +#### 事件触发线程 + + + +- 主要负责将准备好的事件交给 *JS* 引擎线程执行。 + + + +比如 *setTimeout* 定时器计数结束, *ajax* 等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件依次加入到任务队列的队尾,等待 *JS* 引擎线程的执行。 + + + +#### 异步 *http* 请求线程 + + + +- 负责执行异步请求一类的函数的线程,如:*Promise、fetch、ajax* 等。 +- 主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待 *JS* 引擎线程执行。 + + + +### 浏览器中的事件循环 + + + +#### 宏任务和微任务 + + + +事件循环中的异步队列有两种:宏任务( *macro* )队列和微任务( *micro* )队列。 + +**宏任务队列有一个,微任务队列只有一个**。 + +- 常见的宏任务有:*setTimeout、setInterval、requestAnimationFrame、script*等。 +- 常见的微任务有:*new Promise( ).then(回调)、MutationObserver* 等。 + + + +#### 事件循环流程 + + + +一个完整的事件循环过程,可以概括为以下阶段: + +image-20211015121213384 + + + +- 一开始执行栈空,我们可以把**执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则**。微任务队列空,宏任务队列里有且只有一个 *script* 脚本(整体代码)。 + + + +- 全局上下文( *script* 标签)被推入执行栈,同步代码执行。在执行的过程中,会判断是同步任务还是异步任务,通过对一些接口的调用,可以产生新的宏任务与微任务,它们会分别被推入各自的任务队列里。同步代码执行完了,*script* 脚本会被移出宏任务队列,这个过程本质上是队列的宏任务的执行和出队的过程。 + + + +- 上一步我们出队的是一个宏任务,这一步我们处理的是微任务。但需要注意的是:当一个宏任务执行完毕后,会执行所有的微任务,也就是将整个微任务队列清空。 + + + +- 执行渲染操作,更新界面 + + + +- 检查是否存在 *Web worker* 任务,如果有,则对其进行处理 + + + +- 上述过程循环往复,直到两个队列都清空 + + + +宏任务和微任务的执行流程,总结起来就是: + +**当某个宏任务执行完后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏任务队列中排在最前的任务,执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,依次类推。** + +执行流程如下图所示: + + + +image-20211015114206131 + + + +这里我们可以来看两道具体的代码题目加深理解: + +```js +console.log('script start'); +setTimeout(function() { + console.log('setTimeout'); +}, 0); + +Promise.resolve().then(function() { + console.log('promise1'); +}).then(function() { + console.log('promise2'); +}); + +console.log('script end'); +``` + +上面的代码输出的结果为: + +```js +script start +script end +promise1 +promise2 +setTimeout +``` + +原因很简单,首先会执行同步的任务,输出 *script start* 以及 *script end*。接下来是处理异步任务,异步任务分为宏任务队列和微任务队列,在执行宏任务队列中的每个宏任务之前先把微任务清空一遍,由于 *promise* 是微任务,所以会先被执行,而 *setTimeout* 由于是一个宏任务,会在微任务队列被清空后再执行。 + + + +```js +Promise.resolve().then(()=>{ + console.log('Promise1') + setTimeout(()=>{ + console.log('setTimeout2') + },0) +}) +setTimeout(()=>{ + console.log('setTimeout1') + Promise.resolve().then(()=>{ + console.log('Promise2') + }) +},0) +``` + +上面的代码输出的结果为: + +```js +Promise1 +setTimeout1 +Promise2 +setTimeout2 +``` + +一开始执行栈的同步任务(这属于宏任务)执行完毕,会去查看是否有微任务队列,上题中存在(有且只有一个),然后执行微任务队列中的所有任务输出 *Promise1*,同时会生成一个宏任务 *setTimeout2*。 + +然后去查看宏任务队列,宏任务 *setTimeout1* 在 *setTimeout2* 之前,先执行宏任务 *setTimeout1*,输出 *setTimeout1*。在执行宏任务 *setTimeout1* 时会生成微任务 *Promise2* ,放入微任务队列中,接着先去清空微任务队列中的所有任务,输出 *Promise2*。 + +清空完微任务队列中的所有任务后,就又会去宏任务队列取一个,这回执行的是 *setTimeout2*。 + + + +### *Node.js* 中的事件循环 + + + +#### *Node.js* 事件循环介绍 + + + +*Node.js* 中的事件循环和浏览器中的是完全不相同的东西。 + +*Node.js* 采用 *V8* 作为 *JS* 的解析引擎,而 *I/O* 处理方面使用了自己设计的 *libuv*,*libuv* 是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的 *API*,事件循环机制也是它里面的实现。 + +image-20211015121258759 + + + +可以看出 *Node.JS* 的事件循环比浏览器端复杂很多。*Node.js* 的运行机制如下: + +- *V8* 引擎解析 *JavaScript* 脚本。 +- 解析后的代码,调用 *Node API*。 +- *libuv* 库负责 *Node API* 的执行。它将不同的任务分配给不同的线程,形成一个事件循环,以异步的方式将任务的执行结果返回给 *V8* 引擎。 +- *V8* 引擎再将结果返回给用户。 + + + +整个架构图如下所示: + +image-20211029160543365 + + + + + +#### 事件循环的 *6* 个阶段 + + + +其中 *libuv* 引擎中的事件循环分为 *6* 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。 + +node + + + +从上图中,大致看出 *Node.js* 中的事件循环的顺序: + +外部输入数据 –-> 轮询阶段( *poll* )-–> 检查阶段( *check* )-–> 关闭事件回调阶段( *close callback* )–-> 定时器检测阶段( *timer* )–-> *I/O* 事件回调阶段( *I/O callbacks* )-–>闲置阶段( *idle、prepare* )–->轮询阶段(按照该顺序反复运行)... + +以上 *6* 个阶段所做的事情如下: + +- *timers* 阶段:这个阶段执行 *timer*( *setTimeout、setInterval* )的回调 +- *I/O callbacks* 阶段:处理一些上一轮循环中的少数未执行的 *I/O* 回调 +- *idle、prepare* 阶段:仅 *Node.js* 内部使用 +- *poll* 阶段:获取新的 *I/O* 事件, 适当的条件下 *Node.js* 将阻塞在这里 +- *check* 阶段:执行 *setImmediate( )* 的回调 +- *close callbacks* 阶段:执行 *socket* 的 *close* 事件回调 + +注意:**上面六个阶段都不包括 *process.nextTick( )*** + +接下去我们详细介绍 *timers、poll、check* 这 *3* 个阶段,因为日常开发中的绝大部分异步任务都是在这 *3* 个阶段处理的。 + + + +***timer* 阶段** + + + +*timers* 阶段会执行 *setTimeout* 和 *setInterval* 回调,并且是由 *poll* 阶段控制的。同样,**在 *Node.js* 中定时器指定的时间也不是准确时间,只能是尽快执行**。 + + + +***poll* 阶段** + + + +*poll* 是一个至关重要的阶段,这一阶段中,系统会做两件事情: + +- 回到 *timer* 阶段执行回调 +- 执行 *I/O* 回调 + +并且在进入该阶段时如果没有设定了 *timer* 的话,会发生以下两件事情: + +- 如果 *poll* 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制 +- 如果 *poll* 队列为空时,会有两件事发生: + - 如果有 *setImmediate* 回调需要执行,*poll* 阶段会停止并且进入到 *check* 阶段执行回调 + - 如果没有 *setImmediate* 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去 + +当然设定了 *timer* 的话且 *poll* 队列为空,则会判断是否有 *timer* 超时,如果有的话会回到 *timer* 阶段执行回调。 + +假设 *poll* 被堵塞,那么即使 *timer* 已经到时间了也只能等着,这也是为什么上面说定时器指定的时间并不是准确的时间。例如: + +```js +const start = Date.now(); +setTimeout(function f1() { + console.log("setTimeout", Date.now() - start); +}, 200); + +const fs = require('fs'); + +fs.readFile('./index.js', 'utf-8', function f2() { + console.log('readFile'); + const start = Date.now(); + // 强行延时 500 毫秒 + while (Date.now() - start < 500) { } +}) +``` + + + +***check* 阶段** + + + +*setImmediate( )* 的回调会被加入 *check* 队列中,从事件循环的阶段图可以知道,*check* 阶段的执行顺序在 *poll* 阶段之后。 + +我们先来看个例子: + +```js +console.log('start') +setTimeout(() => { + console.log('timer1') + Promise.resolve().then(function() { + console.log('promise1') + }) +}, 0) +setTimeout(() => { + console.log('timer2') + Promise.resolve().then(function() { + console.log('promise2') + }) +}, 0) +Promise.resolve().then(function() { + console.log('promise3') +}) +console.log('end') +// 输出结果:start => end => promise3 => timer1 => promise1 => timer2 => promise2 +``` + +一开始执行同步任务,依次打印出 *start end*,并将 *2* 个 *timer* 依次放入 *timer* 队列,之后会立即执行微任务队列,所以打印出 *promise3*。 + +然后进入 *timers* 阶段,执行 *timer1* 的回调函数,打印 *timer1*,发现有一个 *promise.then* 回调将其加入到微任务队列并且立即执行,之后同样的步骤执行 *timer2*,打印 *timer2* 以及 *promise2*。 + + + +#### 一些注意点 + + + +***setTimeout* 和 *setImmediate* 区别** + + + +二者非常相似,区别主要在于调用时机不同。 + +- *setImmediate* 设计在 *poll* 阶段完成时执行,即 *check* 阶段 +- *setTimeout* 设计在 *poll* 阶段为空闲时,且设定时间到达后执行,但它在 *timer* 阶段执行 + +来看一个具体的示例: + +```js +setTimeout(function timeout () { + console.log('timeout'); +},0); +setImmediate(function immediate () { + console.log('immediate'); +}); +``` + +对于以上代码来说,*setTimeout* 可能执行在前,也可能执行在后。首先 *setTimeout(fn, 0) === setTimeout(fn, 1)*,这是由源码决定的,进入事件循环也是需要成本的,如果在准备时候花费了大于 *1ms* 的时间,那么在 *timer* 阶段就会直接执行 *setTimeout* 回调。如果准备时间花费小于 *1ms*,那么就是 *setImmediate* 回调先执行了。 + + + +但当二者在异步 *I/O callback* 内部调用时,总是先执行 *setImmediate*,再执行 *setTimeout*,例如: + +```js +const fs = require('fs') +fs.readFile(__filename, () => { + setTimeout(() => { + console.log('timeout'); + }, 0) + setImmediate(() => { + console.log('immediate') + }) +}) +// immediate +// timeout +``` + +在上述代码中,*setImmediate* 永远先执行。因为两个代码写在 *I/O* 回调中,*I/O* 回调是在 *poll* 阶段执行,当回调执行完毕后队列为空,发现存在 *setImmediate* 回调,所以就直接跳转到 *check* 阶段去执行回调了。 + + + +***process.nextTick*** + + + +这个函数其实是独立于事件循环之外的,它有一个自己的队列。当每个阶段完成后,如果存在 *nextTick* 队列,就会清空队列中的所有回调函数,并且优先于其他 *microtask* 执行。 + + + +```js +setTimeout(() => { + console.log('timer1') + Promise.resolve().then(function() { + console.log('promise1') + }) +}, 0) +process.nextTick(() => { + console.log('nextTick') + process.nextTick(() => { + console.log('nextTick') + process.nextTick(() => { + console.log('nextTick') + process.nextTick(() => { + console.log('nextTick') + }) + }) + }) +}) +// nextTick => nextTick => nextTick => nextTick => timer1 => promise1 +``` + + + +***Promise.then*** + + + +*Promise.then* 也是独立于事件循环之外的,有一个自己的队列,但是优先级要比 *process.nextTick* 要低,所以当微任务中同时存在 *process.nextTick* 和 *Promise.then* 时,会优先执行前者。 + + + +```js +setTimeout(()=>{ + console.log('timer1') + Promise.resolve().then(function() { + console.log('promise1') + }) + process.nextTick(() => { + console.log('nexttick'); + }) +}, 0) +setTimeout(()=>{ + console.log('timer2') + Promise.resolve().then(function() { + console.log('promise2') + }) +}, 0) +// timer1、nexttick、promise1、timer2、promise2 +``` + + + +#### *Node.js* 与浏览器的事件队列的差异 + + + +浏览器环境下,就两个队列,一个宏任务队列,一个微任务队列。微任务的任务队列是每个宏任务执行完之后执行。 + +在 *Node.js* 中,每个任务队列的每个任务执行完毕之后,就会清空这个微任务队列。 + + + +![eventloop](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-10-15-060748.png) + + + +## 真题解答 + + + +- 请简述一下 *Node.js* 中的事件循环,和浏览器环境的事件循环有何不同? + +> 参考答案: +> +> *Node.JS* 的事件循环分为 *6* 个阶段: +> +> - *timers* 阶段:这个阶段执行 *timer*( *setTimeout、setInterval* )的回调 +> - *I/O callbacks* 阶段:处理一些上一轮循环中的少数未执行的 *I/O* 回调 +> - *idle、prepare* 阶段:仅 *Node.js* 内部使用 +> - *poll* 阶段:获取新的 *I/O* 事件, 适当的条件下 *Node.js* 将阻塞在这里 +> - *check* 阶段:执行 *setImmediate( )* 的回调 +> - *close callbacks* 阶段:执行 *socket* 的 *close* 事件回调 +> +> 事件循环的执行顺序为: +> +> 外部输入数据 –-> 轮询阶段( *poll* )-–> 检查阶段( *check* )-–> 关闭事件回调阶段( *close callback* )–-> 定时器检测阶段( *timer* )–-> *I/O* 事件回调阶段( *I/O callbacks* )-–>闲置阶段( *idle、prepare* )–->轮询阶段(按照该顺序反复运行)... +> +> 浏览器和 *Node.js* 环境下,微任务任务队列的执行时机不同 +> +> - 在 *Node.js* 中,每个任务队列的每个任务执行完毕之后,就会清空这个微任务队列。 +> - 浏览器环境下,就两个队列,一个宏任务队列,一个微任务队列。微任务的任务队列是每个宏任务执行完之后执行。 + + + +-*EOF*- \ No newline at end of file diff --git a/25. eval/eval.md b/25. eval/eval.md new file mode 100644 index 0000000..598887c --- /dev/null +++ b/25. eval/eval.md @@ -0,0 +1,45 @@ +# *eval* + + + +## 经典真题 + + + +- *JavaScript* 中的 *eval* 方法是啥?一般什么场景下使用? + + + +## 关于 *eval* 你所需要知道的内容 + + + +### *eval* 的基本用法 + + + + + + + +### *eval* 作用域 + + + +### *eval* 应用场景 + + + + + + + +## 真题解答 + + + +- *JavaScript* 中的 *eval* 方法是啥?一般什么场景下使用? + +> 参考答案: +> +> \ No newline at end of file diff --git a/javascript 面试题汇总.md b/javascript 面试题汇总.md new file mode 100644 index 0000000..34d8c2a --- /dev/null +++ b/javascript 面试题汇总.md @@ -0,0 +1,8026 @@ +# *JavaScript* 面试题汇总 + +### 1. 根据下面 *ES6* 构造函数的书写方式,要求写出 *ES5* 的 + +```js +class Example { + constructor(name) { + this.name = name; + } + init() { + const fun = () => { console.log(this.name) } + fun(); + } +} +const e = new Example('Hello'); +e.init(); +``` + +> 参考答案: +> +> ```js +> function Example(name) { +> 'use strict'; +> if (!new.target) { +> throw new TypeError('Class constructor cannot be invoked without new'); +> } +> this.name = name; +> } +> +> Object.defineProperty(Example.prototype, 'init', { +> enumerable: false, +> value: function () { +> 'use strict'; +> if (new.target) { +> throw new TypeError('init is not a constructor'); +> } +> var fun = function () { +> console.log(this.name); +> } +> fun.call(this); +> } +> }) +> ``` +> + +> 解析: +> +> 此题的关键在于是否清楚 *ES6* 的 *class* 和普通构造函数的区别,记住它们有以下区别,就不会有遗漏: +> +> 1. *ES6* 中的 *class* 必须通过 *new* 来调用,不能当做普通函数调用,否则报错 +> +> 因此,在答案中,加入了 *new.target* 来判断调用方式 +> +> 2. *ES6* 的 *class* 中的所有代码均处于严格模式之下 +> +> 因此,在答案中,无论是构造函数本身,还是原型方法,都使用了严格模式 +> +> 3. *ES6* 中的原型方法是不可被枚举的 +> +> 因此,在答案中,定义原型方法使用了属性描述符,让其不可枚举 +> +> 4. 原型上的方法不允许通过 *new* 来调用 +> +> 因此,在答案中,原型方法中加入了 *new.target* 来判断调用方式 + + + +### 2. 数组去重有哪些方法?(美团 *19* 年) + +> 参考答案: +> +> ```js +> // 数字或字符串数组去重,效率高 +> function unique(arr) { +> var result = {}; // 利用对象属性名的唯一性来保证不重复 +> for (var i = 0; i < arr.length; i++) { +> if (!result[arr[i]]) { +> result[arr[i]] = true; +> } +> } +> return Object.keys(result); // 获取对象所有属性名的数组 +> } +> +> // 任意数组去重,适配范围光,效率低 +> function unique(arr) { +> var result = []; // 结果数组 +> for (var i = 0; i < arr.length; i++) { +> if (!result.includes(arr[i])) { +> result.push(arr[i]); +> } +> } +> return result; +> } +> +> // 利用ES6的Set去重,适配范围广,效率一般,书写简单 +> function unique(arr) { +> return [...new Set(arr)] +> } +> ``` + + + +### 3. 描述下列代码的执行结果 + +```js +foo(typeof a); +function foo(p) { + console.log(this); + console.log(p); + console.log(typeof b); + let b = 0; +} +``` + +> 参考答案: +> +> 报错,报错的位置在 `console.log(typeof b);` +> +> 报错原因:*ReferenceError: Cannot access 'b' before initialization* + +> 解析: +> +> 这道题考查的是 *ES6* 新增的声明变量关键字 *let* 以及暂时性死区的知识。*let* 和以前的 *var* 关键字不一样,无法在 *let* 声明变量之前访问到该变量,所以在 *typeof b* 的地方就会报错。 + + + +### 4. 描述下列代码的执行结果 + +```js +class Foo { + constructor(arr) { + this.arr = arr; + } + bar(n) { + return this.arr.slice(0, n); + } +} +var f = new Foo([0, 1, 2, 3]); +console.log(f.bar(1)); +console.log(f.bar(2).splice(1, 1)); +console.log(f.arr); +``` + +> 参考答案: +> +> [ 0 ] +> [ 1 ] +> [ 0, 1, 2, 3 ] + +> 解析: +> +> 主要考察的是数组相关的知识。 *f* 对象上面有一个属性 *arr*,*arr* 的值在初始化的时候会被初始化为 *[0, 1, 2, 3]*,之后就完全是考察数组以及数组方法的使用了。 + + + +### 5. 描述下列代码的执行结果 + +```js +01 function f(count) { +02 console.log(`foo${count}`); +03 setTimeout(() => { console.log(`bar${count}`); }); +04 } +05 f(1); +06 f(2); +07 setTimeout(() => { f(3); }); +``` + +> 参考答案: +> +> foo1 +> foo2 +> bar1 +> bar2 +> foo3 +> bar3 + +> 解析: +> +> 这个完全是考察的异步的知识。调用 *f(1)* 的时候,会执行同步代码,打印出 *foo1*,然后 *03* 行的 *setTimeout* 被放入到异步执行队列,接下来调用 *f(2)* 的时候,打印出 *foo2*,后面 *03* 行的 *setTimeout* 又被放入到异步执行队列。然后执行 *07* 行的语句,被放入到异步执行队列。至此,所有同步代码就都执行完毕了。 +> +> 接下来开始执行异步代码,那么大家时间没写,就都是相同的,所以谁先被放入到异步队列,谁就先执行,所以先打印出 *bar1*、然后是 *bar2*,接下来执行之前 *07* 行放入到异步队列里面的 *setTimeout*,先执行 *f* 函数里面的同步代码,打印出 *foo3*,然后是最后一个异步,打印出 *bar3* + + + +### 6. 描述下列代码的执行结果 + +```js +var a = 2; +var b = 5; +console.log(a === 2 || 1 && b === 3 || 4); +``` + +> 参考答案: +> +> *true* +> +> 考察的是逻辑运算符。在 || 里面,只要有一个为真,后面的直接短路,都不用去计算。所以 *a === 2* 得到 *true* 之后直接短路了,返回 *true*。 + + + +### 7. 描述下列代码的执行结果 + +```js +export class ButtonWrapper { + constructor(domBtnEl, hash) { + this.domBtnEl = domBtnEl; + this.hash = hash; + this.bindEvent(); + } + bindEvent() { + this.domBtnEl.addEventListener('click', this.clickEvent, false); + } + detachEvent() { + this.domBtnEl.removeEventListener('click', this.clickEvent); + } + clickEvent() { + console.log(`The hash of the button is: ${this.hash}`); + } +} +``` + +> 参考答案: +> +> 上面的代码导出了一个 *ButtonWrapper* 类,该类在被实例化的时候,实例化对象上面有两个属性,分别是 *domBtnEl* 和 *hash*,*domBtnEl* 是一个 *DOM* 节点,之后为这个 *domBtnEl* 绑定了点击事件,点击后打印出 *The hash of the button is: hash* 那句话。*detachEvent* 是移除点击事件,当调用实例化对象的 *detachEvent* 方法时,点击事件就会被移除。 + + + +### 8. 箭头函数有哪些特点 + +> 参考答案: +> +> 1. 更简洁的语法,例如 +> - 只有一个形参就不需要用括号括起来 +> - 如果函数体只有一行,就不需要放到一个块中 +> - 如果 *return* 语句是函数体内唯一的语句,就不需要 *return* 关键字 +> 2. 箭头函数没有自己的 *this*,*arguments*,*super* +> 3. 箭头函数 *this* 只会从自己的作用域链的上一层继承 *this*。 + + + +### 9. 说一说类的继承 + +> 参考答案: +> +> 继承是面向对象编程中的三大特性之一。 +> +> *JavaScript* 中的继承经过不断的发展,从最初的对象冒充慢慢发展到了今天的圣杯模式继承。 +> +> 其中最需要掌握的就是**伪经典继承**和**圣杯模式**的继承。 +> +> 很长一段时间,JS 继承使用的都是**组合继承**。这种继承也被称之为伪经典继承,该继承方式综合了原型链和盗用构造函数的方式,将两者的优点集中了起来。 +> +> 组合继承弥补了之前原型链和盗用构造函数这两种方式各自的不足,是 *JavaScript* 中使用最多的继承方式。 +> +> 组合继承最大的问题就是效率问题。最主要就是父类的构造函数始终会被调用两次:一次是在创建子类原型时调用,另一次是在子类构造函数中调用。 +> +> 本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。 +> +> 圣杯模式的继承解决了这一问题,其基本思路就是不通过调用父类构造函数来给子类原型赋值,而是取得父类原型的一个副本,然后将返回的新对象赋值给子类原型。 + +> 解析:该题主要考察就是对 *js* 中的继承是否了解,以及常见的继承的形式有哪些。最常用的继承就是**组合继承**(伪经典继承)和圣杯模式继承。下面附上 *js* 中这两种继承模式的详细解析。 +> +> 下面是一个组合继承的例子: +> +> ```js +> // 基类 +> var Person = function (name, age) { +> this.name = name; +> this.age = age; +> } +> Person.prototype.test = "this is a test"; +> Person.prototype.testFunc = function () { +> console.log('this is a testFunc'); +> } +> +> // 子类 +> var Student = function (name, age, gender, score) { +> Person.apply(this, [name, age]); // 盗用构造函数 +> this.gender = gender; +> this.score = score; +> } +> Student.prototype = new Person(); // 改变 Student 构造函数的原型对象 +> Student.prototype.testStuFunc = function () { +> console.log('this is a testStuFunc'); +> } +> +> // 测试 +> var zhangsan = new Student("张三", 18, "男", 100); +> console.log(zhangsan.name); // 张三 +> console.log(zhangsan.age); // 18 +> console.log(zhangsan.gender); // 男 +> console.log(zhangsan.score); // 100 +> console.log(zhangsan.test); // this is a test +> zhangsan.testFunc(); // this is a testFunc +> zhangsan.testStuFunc(); // this is a testStuFunc +> ``` +> +> +> +> 在上面的例子中,我们使用了组合继承的方式来实现继承,可以看到无论是基类上面的属性和方法,还是子类自己的属性和方法,都得到了很好的实现。 +> +> +> +> 但是在组合继承中存在效率问题,比如在上面的代码中,我们其实调用了两次 *Person*,产生了两组 *name* 和 *age* 属性,一组在原型上,一组在实例上。 +> +> +> +> 也就是说,我们在执行 *Student.prototype = new Person( )* 的时候,我们是想要 *Person* 原型上面的方法,属性是不需要的,因为属性之后可以通过 *Person.apply(this, [name, age])* 拿到,但是当你 *new Person( )* 的时候,会实例化一个 *Person* 对象出来,这个对象上面,属性和方法都有。 +> +> +> +> 圣杯模式的继承解决了这一问题,其基本思路就是不通过调用父类构造函数来给子类原型赋值,而是取得父类原型的一个副本,然后将返回的新对象赋值给子类原型。 +> +> +> +> 下面是一个圣杯模式的示例: +> +> +> +> ```js +> // target 是子类,origin 是基类 +> // target ---> Student, origin ---> Person +> function inherit(target, origin) { +> function F() { }; // 没有任何多余的属性 +> +> // origin.prototype === Person.prototype, origin.prototype.constructor === Person 构造函数 +> F.prototype = origin.prototype; +> +> // 假设 new F() 出来的对象叫小 f +> // 那么这个 f 的原型对象 === F.prototype === Person.prototype +> // 那么 f.constructor === Person.prototype.constructor === Person 的构造函数 +> target.prototype = new F(); +> +> // 而 f 这个对象又是 target 对象的原型对象 +> // 这意味着 target.prototype.constructor === f.constructor +> // 所以 target 的 constructor 会指向 Person 构造函数 +> +> // 我们要让子类的 constructor 重新指向自己 +> // 若不修改则会发现 constructor 指向的是父类的构造函数 +> target.prototype.constructor = target; +> } +> +> +> // 基类 +> var Person = function (name, age) { +> this.name = name; +> this.age = age; +> } +> Person.prototype.test = "this is a test"; +> Person.prototype.testFunc = function () { +> console.log('this is a testFunc'); +> } +> +> +> // 子类 +> var Student = function (name, age, gender, score) { +> Person.apply(this, [name, age]); +> this.gender = gender; +> this.score = score; +> } +> inherit(Student, Person); // 使用圣杯模式实现继承 +> // 在子类上面添加方法 +> Student.prototype.testStuFunc = function () { +> console.log('this is a testStuFunc'); +> } +> +> // 测试 +> var zhangsan = new Student("张三", 18, "男", 100); +> +> console.log(zhangsan.name); // 张三 +> console.log(zhangsan.age); // 18 +> console.log(zhangsan.gender); // 男 +> console.log(zhangsan.score); // 100 +> console.log(zhangsan.test); // this is a test +> zhangsan.testFunc(); // this is a testFunc +> zhangsan.testStuFunc(); // this is a testStuFunc +> ``` +> +> 在上面的代码中,我们在 *inherit* 方法中创建了一个中间层,之后让 *F* 的原型和父类的原型指向同一地址,再让子类的原型指向这个 *F* 的实例化对象来实现了继承。 +> +> +> +> 这样我们的继承,属性就不会像之前那样实例对象上一份,原型对象上一份,拥有两份。圣杯模式继承是目前 *js* 继承的最优解。 +> +> +> +> 最后我再画个图帮助大家理解,如下图: +> +> +> +> 组合模式(伪经典模式)下的继承示意图: +> +> image-20210808102111003 +> +> 圣杯模式下的继承示意图: +> +> +> +> image-20210808101303180 +> +> + + + +### 10. *new* 操作符都做了哪些事? + +> 参考答案: +> +> *new* 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。 +> +> *new* 关键字会进行如下的操作: +> 步骤 *1*:创建一个空的简单 *JavaScript* 对象,即 { } ; +> 步骤 *2*:链接该对象到另一个对象(即设置该对象的原型对象); +> 步骤 *3*:将步骤 *1* 新创建的对象作为 *this* 的上下文; +> 步骤 *4*:如果该函数没有返回对象,则返回 *this*。 + + + +### 11. *call、apply、bind* 的区别 ? + +> 参考答案: +> +> *call* 和 *apply* 的功能相同,区别在于传参的方式不一样: +> +> - *fn.call(obj, arg1, arg2, ...)* 调用一个函数, 具有一个指定的 *this* 值和分别地提供的参数(参数的列表)。 +> - *fn.apply(obj, [argsArray])* 调用一个函数,具有一个指定的 *this* 值,以及作为一个数组(或类数组对象)提供的参数。 +> +> *bind* 和 *call/apply* 有一个很重要的区别,一个函数被 *call/apply* 的时候,会直接调用,但是 *bind* 会创建一个新函数。当这个新函数被调用时,*bind( )* 的第一个参数将作为它运行时的 *this*,之后的一序列参数将会在传递的实参前传入作为它的参数。 + + + +### 12. 事件循环机制(宏任务、微任务) + +> 参考答案: +> +> 在 *js* 中任务会分为同步任务和异步任务。 +> +> 如果是同步任务,则会在主线程(也就是 *js* 引擎线程)上进行执行,形成一个执行栈。但是一旦遇到异步任务,则会将这些异步任务交给异步模块去处理,然后主线程继续执行后面的同步代码。 +> +> 当异步任务有了运行结果以后,就会在任务队列里面放置一个事件,这个任务队列由事件触发线程来进行管理。 +> +> 一旦执行栈中所有的同步任务执行完毕,就代表着当前的主线程(*js* 引擎线程)空闲了,系统就会读取任务队列,将可以运行的异步任务添加到执行栈中,开始执行。 +> +> 在 *js* 中,任务队列中的任务又可以被分为 *2* 种类型:宏任务(*macrotask*)与微任务(*microtask*) +> +> 宏任务可以理解为每次执行栈所执行的代码就是一个宏任务,包括每次从事件队列中获取一个事件回调并放到执行栈中所执行的任务。 +> +> 微任务可以理解为当前宏任务执行结束后立即执行的任务。 + + + +### 13. 你了解 *node* 中的事件循环机制吗?*node11* 版本以后有什么改变 + +> 参考答案: +> +> *Node.js* 在主线程里维护了一个**事件队列,**当接到请求后,就将该请求作为一个事件放入这个队列中,然后继续接收其他请求。当主线程空闲时(没有请求接入时),就开始循环事件队列,检查队列中是否有要处理的事件,这时要分两种情况:如果是非 *I/O* 任务,就亲自处理,并通过回调函数返回到上层调用;如果是 *I/O* 任务,就从**线程池**中拿出一个线程来处理这个事件,并指定回调函数,然后继续循环队列中的其他事件。 +> +> 当线程中的 *I/O* 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环,当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 **事件循环** (*Event Loop*)。 +> +> 无论是 *Linux* 平台还是 *Windows* 平台,*Node.js* 内部都是通过**线程池**来完成异步 *I/O* 操作的,而 *LIBUV* 针对不同平台的差异性实现了统一调用。因此,***Node.js* 的单线程仅仅是指 *JavaScript* 运行在单线程中,而并非 *Node.js* 是单线程。** +> +> *Node.JS* 的事件循环分为 *6* 个阶段: +> +> - *timers* 阶段:这个阶段执行 *timer*( *setTimeout、setInterval* )的回调 +> - *I/O callbacks* 阶段:处理一些上一轮循环中的少数未执行的 *I/O* 回调 +> - *idle、prepare* 阶段:仅 *Node.js* 内部使用 +> - *poll* 阶段:获取新的 *I/O* 事件, 适当的条件下 *Node.js* 将阻塞在这里 +> - *check* 阶段:执行 *setImmediate( )* 的回调 +> - *close callbacks* 阶段:执行 *socket* 的 *close* 事件回调 +> +> 事件循环的执行顺序为: +> +> 外部输入数据 –-> 轮询阶段( *poll* )-–> 检查阶段( *check* )-–> 关闭事件回调阶段( *close callback* )–-> 定时器检测阶段( *timer* )–-> *I/O* 事件回调阶段( *I/O callbacks* )-–>闲置阶段( *idle、prepare* )–->轮询阶段(按照该顺序反复运行)... +> +> 浏览器和 *Node.js* 环境下,微任务任务队列的执行时机不同 +> +> - *Node.js* 端,微任务在事件循环的各个阶段之间执行 +> - 浏览器端,微任务在事件循环的宏任务执行完之后执行 +> +> *Node.js v11.0.0* 版本于 *2018* 年 *10* 月,主要有以下变化: +> +> 1. *V8* 引擎更新至版本 *7.0* +> 2. *http、https* 和 *tls* 模块默认使用 *WHESWG URL* 解析器。 +> 3. 隐藏子进程的控制台窗口默认改为了 *true*。 +> 4. *FreeBSD 10*不再支持。 +> 5. 增加了多线程 *Worker Threads* + + + +### 14. 什么是函数柯里化? + +> 参考答案: +> +> 柯里化(*currying*)又称部分求值。一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。 +> +> 举个例子,就是把原本: +> +> *function(arg1,arg2)* 变成 *function(arg1)(arg2)* +> *function(arg1,arg2,arg3)* 变成 *function(arg1)(arg2)(arg3)* +> *function(arg1,arg2,arg3,arg4)* 变成 *function(arg1)(arg2)(arg3)(arg4)* +> +> 总而言之,就是将: +> +> *function(arg1,arg2,…,argn)* 变成 *function(arg1)(arg2)…(argn)* + + + +### 15. *promise.all* 方法的使用场景?数组中必须每一项都是 *promise* 对象吗?不是 *promise* 对象会如何处理 ? + +> 参考答案: +> +> ***promise.all(promiseArray)*** 方法是 *promise* 对象上的静态方法,该方法的作用是将多个 *promise* 对象实例包装,生成并返回一个新的 *promise* 实例。 +> +> 此方法在集合多个 *promise* 的返回结果时很有用。 +> +> 返回值将会按照参数内的 *promise* 顺序排列,而不是由调用 *promise* 的完成顺序决定。 +> +> ***promise.all* 的特点** +> +> 接收一个*Promise*实例的数组或具有*Iterator*接口的对象 +> +> 如果元素不是*Promise*对象,则使用*Promise.resolve*转成*Promise*对象 +> +> 如果全部成功,状态变为*resolved*,返回值将组成一个数组传给回调 +> +> 只有有一个失败,状态就变为 *rejected*,返回值将直接传递给回调 *all( )*的返回值,也是新的 *promise* 对象 + + + +### 16. *this* 的指向哪几种 ? + +> 参考答案: +> +> 总结起来,*this* 的指向规律有如下几条: +> +> - 在函数体中,非显式或隐式地简单调用函数时,在严格模式下,函数内的 *this* 会被绑定到 *undefined* 上,在非严格模式下则会被绑定到全局对象 *window/global* 上。 +> - 一般使用 *new* 方法调用构造函数时,构造函数内的 *this* 会被绑定到新创建的对象上。 +> - 一般通过 *call/apply/bind* 方法显式调用函数时,函数体内的 *this* 会被绑定到指定参数的对象上。 +> - 一般通过上下文对象调用函数时,函数体内的 *this* 会被绑定到该对象上。 +> - 在箭头函数中,*this* 的指向是由外层(函数或全局)作用域来决定的。 + + + +### 17. *JS* 中继承实现的几种方式 + +> 参考答案: +> +> *JS* 的继承随着语言的发展,从最早的对象冒充到现在的圣杯模式,涌现出了很多不同的继承方式。每一种新的继承方式都是对前一种继承方式不足的一种补充。 +> +> 1. 原型链继承 +> +> - 重点:让新实例的原型等于父类的实例。 +> - 特点:实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。(新实例不会继承父类实例的属性!) +> - 缺点: +> - 1、新实例无法向父类构造函数传参。 +> - 2、继承单一。 +> - 3、所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!) +> +> 2. 借用构造函数继承 +> +> - 重点:用 *call( )* 和 *apply( )* 将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制)) +> - 特点: +>    - 1、只继承了父类构造函数的属性,没有继承父类原型的属性。 +> - 2、解决了原型链继承缺点1、2、3。 +> - 3、可以继承多个构造函数属性(call多个)。 +> - 4、在子实例中可向父实例传参。 +> - 缺点: +>    - 1、只能继承父类构造函数的属性。 +> - 2、无法实现构造函数的复用。(每次用每次都要重新调用) +> - 3、每个新实例都有父类构造函数的副本,臃肿。 +> +> 3. 组合模式(又被称之为伪经典模式) +> +> - 重点:结合了两种模式的优点,传参和复用 +> - 特点: +>    - 1、可以继承父类原型上的属性,可以传参,可复用。 +>    - 2、每个新实例引入的构造函数属性是私有的。 +> - 缺点:调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。 +> +> 4. 寄生组合式继承(圣杯模式) +> +> - 重点:修复了组合继承的问题 + + + +### 18. 什么是事件监听 + +> 参考答案: +> +> 首先需要区别清楚事件监听和事件监听器。 +> +> 在绑定事件的时候,我们需要对应的书写一个事件处理程序,来应对事件发生时的具体行为。 +> +> 这个事件处理程序我们也称之为事件监听器。 +> +> 当事件绑定好后,程序就会对事件进行监听,当用户触发事件时,就会执行对应的事件处理程序。 +> +> 关于事件监听,*W3C* 规范中定义了 *3* 个事件阶段,依次是捕获阶段、目标阶段、冒泡阶段。 +> +> - **捕获**阶段:在事件对象到达事件目标之前,事件对象必须从 *window* 经过目标的祖先节点传播到事件目标。 这个阶段被我们称之为捕获阶段。在这个阶段注册的事件监听器在事件到达其目标前必须先处理事件。 +> +> - **目标** 阶段:事件对象到达其事件目标。 这个阶段被我们称为目标阶段。一旦事件对象到达事件目标,该阶段的事件监听器就要对它进行处理。如果一个事件对象类型被标志为不能冒泡。那么对应的事件对象在到达此阶段时就会终止传播。 +> - **冒泡** 阶段:事件对象以一个与捕获阶段相反的方向从事件目标传播经过其祖先节点传播到 *window*。这个阶段被称之为冒泡阶段。在此阶段注册的事件监听器会对相应的冒泡事件进行处理。 + + + +### 19. 什么是 *js* 的闭包?有什么作用? + +> 参考答案: +> +> 一个函数和对其周围状态(*lexical environment*,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是**闭包**(*closure*)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 *JavaScript* 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。 +> +> 闭包的用处: +> +> 1. 匿名自执行函数 +> 2. 结果缓存 +> 3. 封装 +> 4. 实现类和继承 + + + +### 20. 事件委托以及冒泡原理 + +> 参考答案: +> +> 事件委托,又被称之为事件代理。在 *JavaScript* 中,添加到页面上的事件处理程序数量将直接关系到页面整体的运行性能。导致这一问题的原因是多方面的。 +> +> 首先,每个函数都是对象,都会占用内存。内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 *DOM* 访问次数,会延迟整个页面的交互就绪时间。 +> +> 对事件处理程序过多问题的解决方案就是事件委托。 +> +> 事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,*click* 事件会一直冒泡到 *document* 层次。也就是说,我们可以为整个页面指定一个 *onclick* 事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。 +> +> 事件冒泡(*event bubbling*),是指事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。 + + + +### 21. *let const var* 的区别?什么是块级作用域?如何用? + +> 参考答案: +> +> 1. *var* 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问,有变量提升。 +> 2. *let* 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,无变量提升,不可以重复声明。 +> 3. *const* 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改,无变量提升,不可以重复声明。 +> +> 最初在 *JS* 中作用域有:全局作用域、函数作用域。没有块作用域的概念。 +> +> *ES6* 中新增了块级作用域。块作用域由 { } 包括,*if* 语句和 *for* 语句里面的 { } 也属于块作用域。 +> +> 在以前没有块作用域的时候,在 if 或者 for 循环中声明的变量会泄露成全局变量,其次就是 { } 中的内层变量可能会覆盖外层变量。块级作用域的出现解决了这些问题。 + + + +### 22. *ES5* 的方法实现块级作用域(立即执行函数) *ES6* 呢? + +> 参考答案: +> +> *ES6* 原生支持块级作用域。块作用域由 { } 包括,*if* 语句和 *for* 语句里面的 { } 也属于块作用域。 +> +> 使用 *let* 声明的变量或者使用 *const* 声明的常量,只能在块作用域里访问,不能跨块访问。 + + + +### 23. *ES6* 箭头函数的特性 + +> 参考答案: +> +> 1. 更简洁的语法,例如 +> - 只有一个形参就不需要用括号括起来 +> - 如果函数体只有一行,就不需要放到一个块中 +> - 如果 *return* 语句是函数体内唯一的语句,就不需要 *return* 关键字 +> 2. 箭头函数没有自己的 *this*,*arguments*,*super* +> 3. 箭头函数 *this* 只会从自己的作用域链的上一层继承 *this*。 + + + +### 24. 箭头函数与普通函数的区别 ? + +> 参考答案: +> +> 1. 外形不同。箭头函数使用箭头定义,普通函数中没有 +> +> 2. 普通函数可以有匿名函数,也可以有具体名函数,但是箭头函数都是匿名函数。 +> +> 3. **箭头函数不能用于构造函数,不能使用 *new*,**普通函数可以用于构造函数,以此创建对象实例。 +> +> 4. **箭头函数中 *this* 的指向不同,**在普通函数中,*this* 总是指向调用它的对象,如果用作构造函数,*this* 指向创建的对象实例。 +> 箭头函数本身不创建 *this*,也可以说箭头函数本身没有 *this*,但是它在声明时可以捕获其所在上下文的 *this* 供自己使用。 +> +> 5. 每一个普通函数调用后都具有一个 *arguments* 对象,用来存储实际传递的参数。 +> +> 但是箭头函数并没有此对象。**取而代之用rest参数来解决**。 +> +> 6. 箭头函数不能用于 *Generator* 函数,不能使用 *yeild* 关键字。 +> +> 7. 箭头函数不具有 *prototype* 原型对象。而普通函数具有 *prototype* 原型对象。 +> +> 8. 箭头函数不具有 *super*。 +> +> 9. 箭头函数不具有 *new.target*。 + + + +### 25. *JS* 的基本数据类型有哪些?基本数据类型和引用数据类型的区别 + +> 参考答案: +> +> 在 *JavaScript* 中,数据类型整体上来讲可以分为两大类:**基本类型**和**引用数据类型** +> +> 基本数据类型,一共有 *6* 种: +> +> ```text +> string,symbol,number,boolean,undefined,null +> ``` +> +> 其中 *symbol* 类型是在 *ES6* 里面新添加的基本数据类型。 +> +> 引用数据类型,就只有 *1* 种: +> +> ```js +> object +> ``` +> +> 基本数据类型的值又被称之为原始值或简单值,而引用数据类型的值又被称之为复杂值或引用值。 +> +> 两者的区别在于: +> +> **原始值是表示 *JavaScript* 中可用的数据或信息的最底层形式或最简单形式。**简单类型的值被称为原始值,是因为它们是**不可细化**的。 +> +> 也就是说,数字是数字,字符是字符,布尔值是 *true* 或 *false*,*null* 和 *undefined* 就是 *null* 和 *undefined*。这些值本身很简单,不能够再进行拆分。由于原始值的数据大小是固定的,所以**原始值的数据是存储于内存中的栈区里面的。** +> +> 在 *JavaScript* 中,对象就是一个引用值。因为对象可以向下拆分,拆分成多个简单值或者复杂值。**引用值在内存中的大小是未知的,因为引用值可以包含任何值,而不是一个特定的已知值,所以引用值的数据都是存储于堆区里面。** +> +> 最后总结一下两者的区别: +> +> 1. 访问方式 +> - 原始值:访问到的是值 +> - 引用值:访问到的是引用地址 +> 2. 比较方式 +> - 原始值:比较的是值 +> - 引用值:比较的是地址 +> +> 3. 动态属性 +> - 原始值:无法添加动态属性 +> - 引用值:可以添加动态属性 +> 4. 变量赋值 +> - 原始值:赋值的是值 +> - 引用值:赋值的是地址 + + + +### 26. *NaN* 是什么的缩写 + +> 参考答案: +> +> *NaN* 的全称为 *Not a Number*,表示非数,或者说不是一个数。虽然 NaN 表示非数,但是它却属于 *number* 类型。 +> +> *NaN* 有两个特点: +> +> 1. 任何涉及 *NaN* 的操作都会返回 *NaN* +> 2. *NaN* 和任何值都不相等,包括它自己本身 + + + +### 27. *JS* 的作用域类型 + +> 参考答案: +> +> 在 *JavaScript* 里面,作用域一共有 4 种:全局作用域,局部作用域、函数作用域以及 *eval* 作用域。 +> +> **全局作用域:**这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。 +> +> **局部作用域:**当使用 *let* 或者 *const* 声明变量时,这些变量在一对花括号中存在局部作用域,只能够在花括号内部进行访问使用。 +> +> **函数作用域:**当进入到一个函数的时候,就会产生一个函数作用域。函数作用域里面所声明的变量只在函数中提供访问使用。 +> +> ***eval* 作用域:**当调用 *eval( )* 函数的时候,就会产生一个 *eval* 作用域。 + + + +### 28. *undefined==null* 返回的结果是什么?*undefined* 与 *null* 的区别在哪? + +> 参考答案: +> +> 返回 *true*。 +> +> 这两个值都表示“无”的意思。 +> +> 通常情况下, 当我们试图访问某个不存在的或者没有赋值的变量时,就会得到一个 *undefined* 值。*Javascript* 会自动将声明是没有进行初始化的变量设为 *undifined*。 +> +> 而 *null* 值表示空,*null* 不能通过 *Javascript* 来自动赋值,也就是说必须要我们自己手动来给某个变量赋值为 *null*。 + +> 解析: +> +> 那么为什么 *JavaScript* 要设置两个表示"无"的值呢?这其实是历史原因。 +> +> *1995* 年 *JavaScript* 诞生时,最初像 *Java* 一样,只设置了 *null* 作为表示"无"的值。根据 *C* 语言的传统,*null* 被设计成可以自动转为*0*。 +> +> 但是,*JavaScript* 的设计者,觉得这样做还不够,主要有以下两个原因。 +> +> 1. *null* 像在 *Java* 里一样,被当成一个对象。但是,*JavaScript* 的数据类型分成原始类型(*primitive*)和合成类型(*complex*)两大类,作者觉得表示"无"的值最好不是对象。 +> 2. *JavaScript* 的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败。作者觉得,如果 *null* 自动转为 *0*,很不容易发现错误。 +> +> 因此,作者又设计了一个 *undefined*。 +> +> **这里注意:先有 *null* 后有 *undefined* 出来,*undefined* 是为了填补之前的坑。** +> +> *JavaScript* 的最初版本是这样区分的: +> +> *null* 是一个表示"无"的对象(空对象指针),转为数值时为 *0*; +> +> 典型用法是: +> +> - 作为函数的参数,表示该函数的参数不是对象。 +> +> - 作为对象原型链的终点。 +> +> *undefined* 是一个表示"无"的原始值,转为数值时为 *NaN*。 +> +> 典型用法是: +> +> - 变量被声明了,但没有赋值时,就等于 *undefined*。 +> - 调用函数时,应该提供的参数没有提供,该参数等于 *undefined*。 +> - 对象没有赋值的属性,该属性的值为 *undefined*。 +> - 函数没有返回值时,默认返回 *undefined*。 + + + +### 29. 写一个函数判断变量类型 + +> 参考答案: +> +> ```js +> function getType(data){ +> let type = typeof data; +> if(type !== "object"){ +> return type +> } +> return Object.prototype.toString.call(data).replace(/^\[object (\S+)\]$/,'$1') +> } +> function Person(){} +> console.log(getType(1)); // number +> console.log(getType(true)); // boolean +> console.log(getType([1,2,3])); // Array +> console.log(getType(/abc/)); // RegExp +> console.log(getType(new Date)); // Date +> console.log(getType(new Person)); // Object +> console.log(getType({})); // Object +> ``` + + + +### 30. *js* 的异步处理函数 + +> 参考答案: +> +> 在最早期的时候,*JavaScript* 中要实现异步操作,使用的就是 *Callback* 回调函数。 +> +> 但是回调函数会产生回调地狱(*Callback Hell*) +> +> 之后 ES6 推出了 *Promise* 解决方案来解决回调地狱的问题。不过,虽然 *Promise* 作为 *ES6* 中提供的一种新的异步编程解决方案,但是它也有问题。比如,代码并没有因为新方法的出现而减少,反而变得更加复杂,同时理解难度也加大。 +> +> 之后,就出现了基于 *Generator* 的异步解决方案。不过,这种方式需要编写外部的执行器,而执行器的代码写起来一点也不简单。当然也可以使用一些插件,比如 *co* 模块来简化执行器的编写。 +> +> *ES7* 提出的 *async* 函数,终于让 *JavaScript* 对于异步操作有了终极解决方案。 +> +> 实际上,*async* 只是生成器的一种语法糖而已,简化了外部执行器的代码,同时利用 *await* 替代 *yield*,*async* 替代生成器的`*`号。 + + + +### 31. *defer* 与 *async* 的区别 + +> 参考答案: +> +> 按照惯例,所有 *script* 元素都应该放在页面的 *head* 元素中。这种做法的目的就是把**所有外部文件(*CSS* 文件和 *JavaScript* 文件)的引用都放在相同的地方**。可是,在文档的 *head* 元素中包含所有 *JavaScript* 文件,意味着必须等到全部 *JavaScript* 代码都被下载、解析和执行完成以后,才能开始呈现页面的内容(浏览器在遇到 *body* 标签时才开始呈现内容)。 +> +> 对于那些需要很多 *JavaScript* 代码的页面来说,这无疑会导致浏览器在呈现页面时出现明显的延迟,而延迟期间的浏览器窗口中将是一片空白。为了避免这个问题,现在 ***Web* 应用程序一般都全部 *JavaScript* 引用放在 *body* 元素中页面的内容后面**。这样一来,在解析包含的 *JavaScript* 代码之前,页面的内容将完全呈现在浏览器中。而用户也会因为浏览器窗口显示空白页面的时间缩短而感到打开页面的速度加快了。 +> +> 有了 *defer* 和 *async* 后,这种局面得到了改善。 +> +> ***defer* (延迟脚本)** +> +> 延迟脚本:*defer* 属性只适用于外部脚本文件。 +> +> 如果给 *script* 标签定义了*defer* 属性,这个属性的作用是表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,如果 *script* 元素中设置了 *defer* 属性,相当于告诉浏览器立即下载,但延迟执行。 +> +> ***async*(异步脚本)** +> +> 异步脚本:*async* 属性也只适用于外部脚本文件,并告诉浏览器立即下载文件。 +> +> **但与 *defer* 不同的是:标记为 *async* 的脚本并不保证按照指定它们的先后顺序执行。** +> +> 所以总结起来,两者之间最大的差异就是在于脚本下载完之后何时执行,显然 *defer* 是最接近我们对于应用脚本加载和执行的要求的。 +> +> *defer* 是立即下载但延迟执行,加载后续文档元素的过程将和脚本的加载并行进行(异步),但是脚本的执行要在所有元素解析完成之后,*DOMContentLoaded* 事件触发之前完成。*async* 是立即下载并执行,加载和渲染后续文档元素的过程将和 *js* 脚本的加载与执行并行进行(异步)。 + + + +### 32. 浏览器事件循环和任务队列 + +> 参考答案: +> +> *JavaScript* 的异步机制由事件循环和任务队列构成。 +> +> *JavaScript* 本身是单线程语言,所谓异步依赖于浏览器或者操作系统等完成。*JavaScript* 主线程拥有一个执行栈以及一个任务队列,主线程会依次执行代码,当遇到函数时,会先将函数入栈,函数运行完毕后再将该函数出栈,直到所有代码执行完毕。 +> +> 遇到异步操作(例如:*setTimeout、Ajax*)时,异步操作会由浏览器(*OS*)执行,浏览器会在这些任务完成后,将事先定义的回调函数推入主线程的任务队列(*task queue*)中,当主线程的执行栈清空之后会读取任务队列中的回调函数,当任务队列被读取完毕之后,主线程接着执行,从而进入一个无限的循环,这就是事件循环。 + + + +### 33. 原型与原型链 (美团 19年) + +> 参考答案: +> +> - 每个对象都有一个 `__proto__ ` 属性,该属性指向自己的原型对象 +> - 每个构造函数都有一个 `prototype ` 属性,该属性指向实例对象的原型对象 +> - 原型对象里的 `constructor` 指向构造函数本身 +> +> 如下图: +> +> image-20210812161401493 +> +> 每个对象都有自己的原型对象,而原型对象本身,也有自己的原型对象,从而形成了一条原型链条。 +> +> 当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。 + + + +### 34. 作用域与作用域链 (美团 19年) + +> 参考答案: +> +> 作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。*ES6* 之前 *JavaScript* 没有块级作用域,只有全局作用域和函数作用域。*ES6* 的到来,为我们提供了块级作用域。 +> +> 作用域链指的是作用域与作用域之间形成的链条。当我们查找一个当前作用域没有定义的变量(自由变量)的时候,就会向上一级作用域寻找,如果上一级也没有,就再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链 。 + + + +### 35. 闭包及应用场景以及闭包缺点 (美团 19年) + +> 参考答案: +> +> 闭包的应用场景: +> +> 1. 匿名自执行函数 +> 2. 结果缓存 +> 3. 封装 +> 4. 实现类和继承 +> +> 闭包的缺点: +> +> 因为闭包的作用域链会引用包含它的函数的活动对象,导致这些活动对象不会被销毁,因此会占用更多的内存。 + + + +### 36. 继承方式 (美团 19年) + +> 参考答案: +> +> 参阅前面第 *9* 题以及第 *18* 题答案。 + + + +### 37. 原始值与引用值 (美团 19年) + +> 参考答案: +> +> **原始值是表示 *JavaScript* 中可用的数据或信息的最底层形式或最简单形式。**简单类型的值被称为原始值,是因为它们是**不可细化**的。 +> +> 也就是说,数字是数字,字符是字符,布尔值是 *true* 或 *false*,*null* 和 *undefined* 就是 *null* 和 *undefined*。这些值本身很简单,不能够再进行拆分。由于原始值的数据大小是固定的,所以**原始值的数据是存储于内存中的栈区里面的。** +> +> 在 *JavaScript* 中,对象就是一个引用值。因为对象可以向下拆分,拆分成多个简单值或者复杂值。**引用值在内存中的大小是未知的,因为引用值可以包含任何值,而不是一个特定的已知值,所以引用值的数据都是存储于堆区里面。** +> +> 最后总结一下两者的区别: +> +> 1. 访问方式 +> - 原始值:访问到的是值 +> - 引用值:访问到的是引用地址 +> 2. 比较方式 +> - 原始值:比较的是值 +> - 引用值:比较的是地址 +> +> 3. 动态属性 +> - 原始值:无法添加动态属性 +> - 引用值:可以添加动态属性 +> 4. 变量赋值 +> - 原始值:赋值的是值 +> - 引用值:赋值的是地址 + + + +### 38. 描述下列代码的执行结果 + +```js +const first = () => (new Promise((resolve, reject) => { + console.log(3); + let p = new Promise((resolve, reject) => { + console.log(7); + setTimeout(() => { + console.log(1); + }, 0); + setTimeout(() => { + console.log(2); + resolve(3); + }, 0) + resolve(4); + }); + resolve(2); + p.then((arg) => { + console.log(arg, 5); // 1 bb + }); + setTimeout(() => { + console.log(6); + }, 0); +})) +first().then((arg) => { + console.log(arg, 7); // 2 aa + setTimeout(() => { + console.log(8); + }, 0); +}); +setTimeout(() => { + console.log(9); +}, 0); +console.log(10); +``` + +> 参考答案: +> +> 3 +> 7 +> 10 +> 4 5 +> 2 7 +> 1 +> 2 +> 6 +> 9 +> 8 + + + +### 39. 如何判断数组或对象(美团 19年) + +> 参考答案: +> +> 1. 通过 *instanceof* 进行判断 +> +> ```js +> var arr = [1,2,3,1]; +> console.log(arr instanceof Array) // true +> ``` +> +> 2. 通过对象的 *constructor* 属性 +> +> ```js +> var arr = [1,2,3,1]; +> console.log(arr.constructor === Array) // true +> ``` +> +> 3. *Object.prototype.toString.call(arr)* +> +> ```js +> console.log(Object.prototype.toString.call({name: "jerry"}));//[object Object] +> console.log(Object.prototype.toString.call([]));//[object Array] +> ``` +> +> 4. 可以通过 *ES6* 新提供的方法 *Array.isArray( )* +> +> ```js +> Array.isArray([]) //true +> ``` + + + +### 40. 对象深拷贝与浅拷贝,单独问了 *Object.assign*(美团 19年) + +> 参考答案: +> +> - **浅拷贝**:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝(浅复制) +> +> 浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。 +> +> - **深拷贝**:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。 +> +> +> +> *Object.assign* 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 *Object.assign* 方法进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。 + + + +### 42. 说说 *instanceof* 原理,并回答下面的题目(美团 19年) + +```js +function A(){} +function B(){} +A.prototype = new B(); +let a = new A(); +console.log(a instanceof B) // true of false ? +``` + +> 参考答案: +> +> 答案为 *true*。 +> +> *instanceof* 原理: +> +> *instanceof* 用于检测一个对象是否为某个构造函数的实例。 +> +> 例如:*A instanceof B* +> *instanceof* 用于检测对象 *A* 是不是 *B* 的实例,而检测是基于原型链进行查找的,也就是说 *B* 的 *prototype* 有没有在对象 *A* 的\__*proto*__ 原型链上,如果有就返回 *true*,否则返回 *false* + +### + +### 43. 内存泄漏(美团 19 年) + +> 参考答案: +> +> 内存泄漏(*Memory Leak*)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。 +> +> *Javascript* 是一种高级语言,它不像 *C* 语言那样要手动申请内存,然后手动释放,*Javascript* 在声明变量的时候自动会分配内存,普通的类型比如 *number*,一般放在栈内存里,对象放在堆内存里,声明一个变量,就分配一些内存,然后定时进行垃圾回收。垃圾回收的任务由 *JavaScript* 引擎中的垃圾回收器来完成,它监视所有对象,并删除那些不可访问的对象。 +> +> 基本的垃圾回收算法称为**“标记-清除”**,定期执行以下“垃圾回收”步骤: +> +> - 垃圾回收器获取根并**“标记”**(记住)它们。 +> - 然后它访问并“标记”所有来自它们的引用。 +> - 然后它访问标记的对象并标记它们的引用。所有被访问的对象都被记住,以便以后不再访问同一个对象两次。 +> - 以此类推,直到有未访问的引用(可以从根访问)为止。 +> - 除标记的对象外,所有对象都被删除。 + + + +### 44. *ES6* 新增哪些东西?让你自己说(美团 19 年) + +> 参考答案: +> +> *ES6* 新增内容众多,这里列举出一些关键的以及平时常用的新增内容: +> +> 1. 箭头函数 +> 2. 字符串模板 +> 3. 支持模块化(*import、export*) +> 4. 类(*class、constructor、extends*) +> 5. *let、const* 关键字 +> 6. 新增一些数组、字符串等内置构造函数方法,例如 *Array.from*、*Array.of* 、*Math.sign*、*Math.trunc* 等 +> 7. 新增一些语法,例如扩展操作符、解构、函数默认参数等 +> 8. 新增一种基本数据类型 *Symbol* +> 9. 新增元编程相关,例如 *proxy*、*Reflect* +> 10. *Set* 和 *Map* 数据结构 +> 11. *Promise* +> 12. *Generator* 生成器 + + + +### 45. *weakmap、weakset*(美团 *19* 年) + +> 参考答案: +> +> *WeakSet* 对象是一些对象值的集合, 并且其中的每个对象值都只能出现一次。在 *WeakSet* 的集合中是唯一的 +> +> 它和 *Set* 对象的区别有两点: +> +> - 与 *Set* 相比,*WeakSet* 只能是**对象的集合**,而不能是任何类型的任意值。 +> - *WeakSet* 持弱引用:集合中对象的引用为弱引用。 如果没有其他的对 *WeakSet* 中对象的引用,那么这些对象会被当成垃圾回收掉。 这也意味着 *WeakSet* 中没有存储当前对象的列表。 正因为这样,*WeakSet* 是不可枚举的。 +> +> *WeakMap* 对象也是键值对的集合。它的**键必须是对象类型**,值可以是任意类型。它的键被弱保持,也就是说,当其键所指对象没有其他地方引用的时候,它会被 *GC* 回收掉。*WeakMap* 提供的接口与 *Map* 相同。 +> +> 与 *Map* 对象不同的是,*WeakMap* 的键是不可枚举的。不提供列出其键的方法。列表是否存在取决于垃圾回收器的状态,是不可预知的。 + + + +### 46. 为什么 *ES6* 会新增 *Promise*(美团 19年) + +> 参考答案: +> +> 在 *ES6* 以前,解决异步的方法是回调函数。但是回调函数有一个最大的问题就是回调地狱(*callback hell*),当我们的回调函数嵌套的层数过多时,就会导致代码横向发展。 +> +> *Promise* 的出现就是为了解决回调地狱的问题。 + + + +### 47. *ES5* 实现继承?(虾皮) + +> 参考答案: +> +> 1. 借用构造函数实现继承 +> +> ```js +> function Parent1(){ +> this.name = "parent1" +> } +> function Child1(){ +> Parent1.call(this); +> this.type = "child1"; +> } +> ``` +> +> 缺点:*Child1* 无法继承 *Parent1* 的原型对象,并没有真正的实现继承 (部分继承)。 +> +> 2. 借用原型链实现继承 +> +> ```js +> function Parent2(){ +> this.name = "parent2"; +> this.play = [1,2,3]; +> } +> function Child2(){ +> this.type = "child2"; +> } +> Child2.prototype = new Parent2(); +> ``` +> +> 缺点:原型对象的属性是共享的。 +> +> 3. 组合式继承 +> +> ```js +> function Parent3(){ +> this.name = "parent3"; +> this.play = [1,2,3]; +> } +> function Child3(){ +> Parent3.call(this); +> this.type = "child3"; +> } +> Child3.prototype = Object.create(Parent3.prototype); +> Child3.prototype.constructor = Child3; +> ``` + + + +### 48. 科里化?(搜狗) + +> 参考答案: +> +> 柯里化,英语全称 *Currying*,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。 +> +> 举个例子,就是把原本: +> +> *function(arg1,arg2)* 变成 *function(arg1)(arg2)* +> *function(arg1,arg2,arg3)* 变成 *function(arg1)(arg2)(arg3)* +> *function(arg1,arg2,arg3,arg4)* 变成 *function(arg1)(arg2)(arg3)(arg4)* +> +> 总而言之,就是将: +> +> *function(arg1,arg2,…,argn)* 变成 *function(arg1)(arg2)…(argn)* + + + +### 49. 防抖和节流?(虾皮) + +> 参考答案: +> +> 我们在平时开发的时候,会有很多场景会频繁触发事件,比如说搜索框实时发请求,*onmousemove、resize、onscroll* 等,有些时候,我们并不能或者不想频繁触发事件,这时候就应该用到函数防抖和函数节流。 +> +> 函数防抖(*debounce*),指的是短时间内多次触发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行。 +> +> 函数节流(*throttle*),指连续触发事件但是在 *n* 秒中只执行一次函数。即 *2n* 秒内执行 *2* 次... 。节流如字面意思,会稀释函数的执行频率。 + + + +### 50. 闭包?(好未来---探讨了 *40* 分钟) + +> 参考答案: +> +> 请参阅前面第 *20* 题以及第 *36* 题答案。 + + + +### 51. 原型和原型链?(字节) + +> 参考答案: +> +> 请参阅前面第 *34* 题答案。 + + + +### 52. 排序算法---(时间复杂度、空间复杂度) + +> 参考答案: +> +> 算法(*Algorithm*)是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。 +> +> 主要还是从算法所占用的「时间」和「空间」两个维度去考量。 +> +> - 时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。 +> - 空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。 +> +> 因此,评价一个算法的效率主要是看它的时间复杂度和空间复杂度情况。然而,有的时候时间和空间却又是「鱼和熊掌」,不可兼得的,那么我们就需要从中去取一个平衡点。 +> +> 排序也称排序算法(*Sort Algorithm*),排序是将**一组数据**,依**指定的顺序**进行**排列的过程**。 +> +> 排序的分类分为**内部排序**和**外部排序法**。 +> +> - 内部排序:指将需要处理的所有数据都加载到**内部存储器(内存)**中进行排序。 +> - 外部排序:**数据量过大**,无法全部加载到内存中,需要借助**外部存储(文件等)**进行排序。 +> +> image-20210813134746501 + + + +### 53. 浏览器事件循环和 *node* 事件循环(搜狗) + +> 参考答案: +> +> 1. 浏览器中的 *Event Loop* +> +> 事件循环中的异步队列有两种:*macro*(宏任务)队列和 *micro*(微任务)队列。**宏任务队列可以有多个,微任务队列只有一个**。 +> +> - 常见的 *macro-task* 比如:*setTimeout、setInterval、 setImmediate、script*(整体代码)、 *I/O* 操作、*UI* 渲染等。 +> - 常见的 *micro-task* 比如: *process.nextTick、new Promise( ).then*(回调)、*MutationObserver*(*html5* 新特性) 等。 +> +> 当某个宏任务执行完后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏任务队列中排在最前的任务,执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,依次类推。 +> +> 2. *Node* 中的事件循环 +> +> *Node* 中的 *Event Loop* 和浏览器中的是完全不相同的东西。*Node.js* 采用 *V8* 作为 *js* 的解析引擎,而 *I/O* 处理方面使用了自己设计的 *libuv*,*libuv* 是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的 *API*,事件循环机制也是它里面的实现。 +> +> *Node.JS* 的事件循环分为 *6* 个阶段: +> +> - *timers* 阶段:这个阶段执行 *timer*( *setTimeout、setInterval* )的回调 +> - *I/O callbacks* 阶段:处理一些上一轮循环中的少数未执行的 *I/O* 回调 +> - *idle、prepare* 阶段:仅 *Node.js* 内部使用 +> - *poll* 阶段:获取新的 *I/O* 事件, 适当的条件下 *Node.js* 将阻塞在这里 +> - *check* 阶段:执行 *setImmediate( )* 的回调 +> - *close callbacks* 阶段:执行 *socket* 的 *close* 事件回调 +> +> *Node.js* 的运行机制如下: +> +> - *V8* 引擎解析 *JavaScript* 脚本。 +> - 解析后的代码,调用 *Node API*。 +> - *libuv* 库负责 *Node API* 的执行。它将不同的任务分配给不同的线程,形成一个 *Event Loop*(事件循环),以异步的方式将任务的执行结果返回给 *V8* 引擎。 +> - *V8* 引擎再将结果返回给用户。 + + + +### 54. 闭包的好处 + +> 参考答案: +> +> 请参阅前面第 *20* 题以及第 *36* 题答案。 + + + +### 55. *let、const、var* 的区别 + +> 参考答案: +> +> 1. *var* 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问,有变量提升。 +> 2. *let* 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,无变量提升,不可以重复声明。 +> 3. *const* 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改,无变量提升,不可以重复声明。 + + + +### 56. 闭包、作用域(可以扩充到作用域链) + +> 参考答案: +> +> **什么是作业域?** +> +> ES5 中只存在两种作用域:全局作用域和函数作用域。在 JavaScript 中,我们将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套子作用域中根据标识符名称进行变量(变量名或者函数名)查找。 +> +> **什么是作用域链?** +> +> 当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止,,而作用域链,就是有当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域对符合访问权限的变量和函数的有序访问。 +> +> **闭包产生的本质** +> +> 当前环境中存在指向父级作用域的引用 +> +> **什么是闭包** +> +> 闭包是一种特殊的对象,它由两部分组成:执行上下文(代号 A),以及在该执行上下文中创建的函数 (代号 B),当 B 执行时,如果访问了 A 中变量对象的值,那么闭包就会产生,且在 Chrome 中使用这个执行上下文 A 的函数名代指闭包。 +> +> **一般如何产生闭包** +> +> - 返回函数 +> - 函数当做参数传递 +> +> **闭包的应用场景** +> +> - 柯里化 bind +> - 模块 + + + +### 57. *Promise* + +> 参考答案: +> +> *Promise* 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理且更强大。它最早由社区提出并实现,*ES6*将其写进了语言标准,统一了用法,并原生提供了*Promise*对象。 +> +> **特点** +> +> 1. 对象的状态不受外界影响 (*3* 种状态) +> +> - *Pending* 状态(进行中) +> +> - *Fulfilled* 状态(已成功) +> - *Rejected* 状态(已失败) +> +> 2. 一旦状态改变就不会再变 (两种状态改变:成功或失败) +> +> - *Pending* -> *Fulfilled* +> - *Pending* -> *Rejected* +> +> **用法** +> +> ```js +> var promise = new Promise(function(resolve, reject){ +> // ... some code +> +> if (/* 异步操作成功 */) { +> resolve(value); +> } else { +> reject(error); +> } +> }) +> ``` + + + +### 58. 实现一个函数,对一个url进行请求,失败就再次请求,超过最大次数就走失败回调,任何一次成功都走成功回调 + +> 参考答案: +> +> 示例代码如下: +> +> ```js +> /** +> @params url: 请求接口地址; +> @params body: 设置的请求体; +> @params succ: 请求成功后的回调 +> @params error: 请求失败后的回调 +> @params maxCount: 设置请求的数量 +> */ +> function request(url, body, succ, error, maxCount = 5) { +> return fetch(url, body) +> .then(res => succ(res)) +> .catch(err => { +> if (maxCount <= 0) return error('请求超时'); +> return request(url, body, succ, error, --maxCount); +> }); +> } +> +> // 调用请求函数 +> request('https://java.some.com/pc/reqCount', { method: 'GET', headers: {} }, +> (res) => { +> console.log(res.data); +> }, +> (err) => { +> console.log(err); +> }) +> +> ``` + + + +### 59. 冒泡排序 + +> 参考答案: +> +> 冒泡排序的核心思想是: +> +> 1. 比较相邻的两个元素,如果前一个比后一个大或者小(取决于排序的顺序是小到大还是大到小),则交换位置。 +> 2. 比较完第一轮的时候,最后一个元素是最大或最小的元素。 +> 3. 这时候最后一个元素已经是最大或最小的了,所以下一次冒泡的时候最后一个元素不需要参与比较。 +> +> 示例代码: +> +> ```js +> function bSort(arr) { +> var len = arr.length; +> // 外层 for 循环控制冒泡的次数 +> for (var i = 0; i < len - 1; i++) { +> for (var j = 0; j < len - 1 - i; j++) { +> // 内层 for 循环控制每一次冒泡需要比较的次数 +> // 因为之后每一次冒泡的两两比较次数会越来越少,所以 -i +> if (arr[j] > arr[j + 1]) { +> var temp = arr[j]; +> arr[j] = arr[j + 1]; +> arr[j + 1] = temp; +> } +> } +> } +> return arr; +> } +> +> //举个数组 +> myArr = [20, -1, 27, -7, 35]; +> //使用函数 +> console.log(bSort(myArr)); // [ -7, -1, 20, 27, 35 ] +> ``` + + + +### 60. 数组降维 + +> 参考答案: +> +> 数组降维就是将一个嵌套多层的数组进行降维操作,也就是对数组进行扁平化。在 *ES5* 时代我们需要自己手写方法或者借助函数库来完成,但是现在可以使用 *ES6* 新提供的数组方法 *flat* 来完成数组降维操作。 + +> 解析:使用 *flat* 方法会接收一个参数,这个参数是数值类型,是要处理扁平化数组的深度,生成后的新数组是独立存在的,不会对原数组产生影响。 +> +> *flat* 方法的语法如下: +> +> ```js +> var newArray = arr.flat([depth]) +> ``` +> +> 其中 *depth* 指定要提取嵌套数组结构的深度,默认值为 *1*。 +> +> 示例如下: +> +> ```js +> var arr = [1, 2, [3, 4, [5, 6]]]; +> console.log(arr.flat()); // [1, 2, 3, 4, [5, 6]] +> console.log(arr.flat(2)); // [1, 2, 3, 4, 5, 6] +> ``` +> +> 上面的代码定义了一个层嵌套的数组,默认情况下只会拍平一层数组,也就是把原来的三维数组降低到了二维数组。在传入的参数为 *2* 时,则会降低两维,成为一个一维数组。 +> +> 使用 *Infinity*,可展开任意深度的嵌套数组,示例如下: +> +> ```js +> var arr = [1, 2, [3, 4, [5, 6, [7, 8]]]]; +> console.log(arr.flat(Infinity)); // [1, 2, 3, 4, 5, 6, 7, 8] +> ``` +> +> 在数组中有空项的时候,使用 *flat* 方法会将中的空项进行移除。 +> +> ```js +> var arr = [1, 2, , 4, 5]; +> console.log(arr.flat()); // [1, 2, 4, 5] +> ``` +> +> 上面的代码中,数组中第三项是空值,在使用 *flat* 后会对空项进行移除。 + + + +### 61. *call apply bind* + +> 参考答案: +> +> 请参阅前面第 *11* 题答案。 + + + +### 62. promise 代码题 + +```js +new Promise((resolve, reject) => { + reject(1); + console.log(2); + resolve(3); + console.log(4); +}).then((res) => { console.log(res) }) + .catch(res => { console.log('reject1') }) +try { + new Promise((resolve, reject) => { + throw 'error' + }).then((res) => { console.log(res) }) + .catch(res => { console.log('reject2') }) +} catch (err) { + console.log(err) +} +``` + +>参考答案: +> +>2 +>4 +>reject1 +>reject2 +> +>直播课或者录播课进行解析。 + + + +### 63. *proxy* 是实现代理,可以改变 *js* 底层的实现方式, 然后说了一下和 *Object.defineProperty* 的区别 + +>参考答案: +> +>两者的区别总结如下: +> +>- 代理原理:Object.defineProperty的原理是通过将数据属性转变为存取器属性的方式实现的属性读写代理。而Proxy则是因为这个内置的Proxy对象内部有一套监听机制,在传入handler对象作为参数构造代理对象后,一旦代理对象的某个操作触发,就会进入handler中对应注册的处理函数,此时我们就可以有选择的使用Reflect将操作转发被代理对象上。 +>- 代理局限性:Object.defineProperty始终还是局限于属性层面的读写代理,对于对象层面以及属性的其它操作代理它都无法实现。鉴于此,由于数组对象push、pop等方法的存在,它对于数组元素的读写代理实现的并不完全。而使用Proxy则可以很方便的监视数组操作。 +>- 自我代理:Object.defineProperty方式可以代理到自身(代理之后使用对象本身即可),也可以代理到别的对象身上(代理之后需要使用代理对象)。Proxy方式只能代理到Proxy实例对象上。这一点在其它说法中是Proxy对象不需要侵入对象就可以实现代理,实际上Object.defineProperty方式也可以不侵入。 + + + +### 64. 使用 *ES5* 与 *ES6* 分别实现继承 + +>参考答案: +> +>如果是使用 *ES5* 来实现继承,那么现在的最优解是使用圣杯模式。圣杯模式的核心思想就是不通过调用父类构造函数来给子类原型赋值,而是取得父类原型的一个副本,然后将返回的新对象赋值给子类原型。具体代码可以参阅前面第 *9* 题的解析。 +> +>*ES6* 新增了 *extends* 关键字,直接使用该关键字就能够实现继承。 + + + +### 65. 深拷贝 + +> 参考答案: +> +> 有深拷贝就有浅拷贝。 +> +> 浅拷贝就是只拷贝对象的引用,而不深层次的拷贝对象的值,多个对象指向堆内存中的同一对象,任何一个修改都会使得所有对象的值修改,因为它们共用一条数据。 +> +> 深拷贝不是单纯的拷贝一份引用数据类型的引用地址,而是将引用类型的值全部拷贝一份,形成一个新的引用类型,这样就不会发生引用错乱的问题,使得我们可以多次使用同样的数据,而不用担心数据之间会起冲突。 + +> 解析: +> +> 「深拷贝」就是在拷贝数据的时候,将数据的所有**引用结构**都拷贝一份。简单的说就是,在内存中存在两个数据结构完全相同又相互独立的数据,将引用型类型进行复制,而不是只复制其引用关系。 +> +> 分析下怎么做「深拷贝」: +> +> 1. 首先假设深拷贝这个方法已经完成,为 deepClone +> 2. 要拷贝一个数据,我们肯定要去遍历它的属性,如果这个对象的属性仍是对象,继续使用这个方法,如此往复 +> +> ``` +> function deepClone(o1, o2) { +> for (let k in o2) { +> if (typeof o2[k] === 'object') { +> o1[k] = {}; +> deepClone(o1[k], o2[k]); +> } else { +> o1[k] = o2[k]; +> } +> } +> } +> // 测试用例 +> let obj = { +> a: 1, +> b: [1, 2, 3], +> c: {} +> }; +> let emptyObj = Object.create(null); +> deepClone(emptyObj, obj); +> console.log(emptyObj.a == obj.a); +> console.log(emptyObj.b == obj.b); +> ``` +> +> 递归容易造成爆栈,尾部调用可以解决递归的这个问题,*Chrome* 的 *V8* 引擎做了尾部调用优化,我们在写代码的时候也要注意尾部调用写法。递归的爆栈问题可以通过将递归改写成枚举的方式来解决,就是通过 *for* 或者 *while* 来代替递归。 + + + +### 66. *async* 与 *await* 的作用 + +>参考答案: +> +> *async* 是一个修饰符,*async* 定义的函数会默认的返回一个 *Promise* 对象 *resolve* 的值,因此对 *async* 函数可以直接进行 *then* 操作,返回的值即为 *then* 方法的传入函数。 +> +>*await* 关键字只能放在 *async* 函数内部, *await* 关键字的作用就是获取 *Promise* 中返回的内容, 获取的是 *Promise* 函数中 *resolve* 或者 *reject* 的值。 + + + +### 67. 数据的基础类型(原始类型)有哪些 + +>参考答案: +> +>*JavaScript* 中的基础数据类型,一共有 *6* 种: +> +>*string,symbol,number,boolean,undefined,null* +> +>其中 *symbol* 类型是在 *ES6* 里面新添加的基本数据类型。 + + + +### 68. *typeof null* 返回结果 + +> 参考答案: +> +> 返回 *object* + +> 解析:至于为什么会返回 *object*,这实际上是来源于 *JavaScript* 从第一个版本开始时的一个 *bug*,并且这个 *bug* 无法被修复。修复会破坏现有的代码。 +> +> 原理这是这样的,不同的对象在底层都表现为二进制,在 *JavaScript* 中二进制前三位都为 *0* 的话会被判断为 *object* 类型,*null* 的二进制全部为 *0*,自然前三位也是 *0*,所以执行 *typeof* 值会返回 *object*。 + + + +### 69. 对变量进行类型判断的方式有哪些 + +> 参考答案: +> +> 常用的方法有 *4* 种: +> +> 1. *typeof* +> +> *typeof* 是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 *7* 种:*number、boolean、symbol、string、object、undefined、function* 等。 +> +> 2. *instanceof* +> +> *instanceof* 是用来判断 *A* 是否为 *B* 的实例,表达式为:*A instanceof B*,如果 *A* 是 *B* 的实例,则返回 *true*,否则返回 *false*。 在这里需要特别注意的是:*instanceof* 检测的是原型。 +> +> 3. *constructor* +> +> 当一个函数 *F* 被定义时,*JS* 引擎会为 *F* 添加 *prototype* 原型,然后再在 *prototype* 上添加一个 *constructor* 属性,并让其指向 *F* 的引用。 +> +> 4. *toString* +> +> *toString( )* 是 *Object* 的原型方法,调用该方法,默认返回当前对象的 *[[Class]]* 。这是一个内部属性,其格式为 *[object Xxx]* ,其中 *Xxx* 就是对象的类型。 +> +> 对于 *Object* 对象,直接调用 *toString( )* 就能返回 *[object Object]* 。而对于其他对象,则需要通过 *call / apply* 来调用才能返回正确的类型信息。例如: +> +> ```js +> Object.prototype.toString.call('') ; // [object String] +> Object.prototype.toString.call(1) ; // [object Number] +> Object.prototype.toString.call(true) ;// [object Boolean] +> Object.prototype.toString.call(Symbol());//[object Symbol] +> Object.prototype.toString.call(undefined) ;// [object Undefined] +> Object.prototype.toString.call(null) ;// [object Null] +> ``` + + + +### 70. *typeof* 与 *instanceof* 的区别? *instanceof* 是如何实现? + +> 参考答案: +> +> 1. *typeof* +> +> *typeof* 是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 *7* 种:*number、boolean、symbol、string、object、undefined、function* 等。 +> +> 2. *instanceof* +> +> *instanceof* 是用来判断 *A* 是否为 *B* 的实例,表达式为:*A instanceof B*,如果 *A* 是 *B* 的实例,则返回 *true*,否则返回 *false*。 在这里需要特别注意的是:*instanceof* 检测的是原型。 +> +> 用一段伪代码来模拟其内部执行过程: +> +> ```js +> instanceof (A,B) = { +> varL = A.__proto__; +> varR = B.prototype; +> if(L === R) { +> // A的内部属性 __proto__ 指向 B 的原型对象 +> return true; +> } +> return false; +> } +> ``` +> +> 从上述过程可以看出,当 *A* 的 \__*proto*__ 指向 *B* 的 *prototype* 时,就认为 *A* 就是 *B* 的实例。 +> +> 需要注意的是,*instanceof* 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。 +> +> 例如:*[ ] instanceof Object* 返回的也会是 *true*。 + + + +### 71. 引用类型有哪些,有什么特点 + +>参考答案: +> +>JS 中七种内置类型(*null,undefined,boolean,number,string,symbol,object*)又分为两大类型 +> +>两大类型: +> +>- 基本类型: `null`,`undefined`,`boolean`,`number`,`string`,`symbol` +>- 引用类型Object: `Array` ,`Function`, `Date`, `RegExp`等 +> +>image-20210813153833385 +> +>基本类型和引用类型的主要区别有以下几点: +> +>**存放位置:** +> +>- 基本数据类型:基本类型值在内存中占据固定大小,直接存储在**栈内存**中的数据 +>- 引用数据类型:引用类型在栈中存储了指针,这个指针指向堆内存中的地址,真实的数据存放在**堆内存**里。 +> +>image-20210813154040287 +> +>**值的可变性:** +> +>- 基本数据类型: 值不可变,*javascript* 中的原始值(*undefined、null*、布尔值、数字和字符串)是不可更改的 +> +>- 引用数据类型:引用类型是可以直接改变其值的 +> +>**比较:** +> +>- 基本数据类型: 基本类型的比较是值的比较,只要它们的值相等就认为他们是相等的 +> +>- 引用数据类型: 引用数据类型的比较是引用的比较,看其的引用是否指向同一个对象 + + + +### 72. 如何得到一个变量的类型---指函数封装实现 + +>参考答案: +> +>请参阅前面第 *30* 题答案。 + + + +### 73. 什么是作用域、闭包 + +>参考答案: +> +>请参阅前面第 *56* 题。 + + + +### 74. 闭包的缺点是什么?闭包的应用场景有哪些?怎么销毁闭包? + +>参考答案: +> +>闭包是指有权访问另外一个函数作用域中的变量的函数。 +> +>因为闭包引用着另一个函数的变量,导致另一个函数已经不使用了也无法销毁,所以**闭包使用过多,会占用较多的内存,这也是一个副作用,内存泄漏。** +> +>如果要销毁一个闭包,可以 把被引用的变量设置为*null*,即手动清除变量,这样下次 *js* 垃圾回收机制回收时,就会把设为 *null* 的量给回收了。 +> +>闭包的应用场景: +> +>1. 匿名自执行函数 +>2. 结果缓存 +>3. 封装 +>4. 实现类和继承 + + + +### 75. *JS*的垃圾回收站机制 + +>参考答案: +> +>*JS* 具有自动垃圾回收机制。垃圾收集器会按照固定的时间间隔周期性的执行。 +> +>*JS* 常见的垃圾回收方式:标记清除、引用计数方式。 +> +>1、标记清除方式: +> +>- 工作原理:当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。 +> +>- 工作流程: +> +> - 垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记; +> +> - 去掉环境中的变量以及被环境中的变量引用的变量的标记; +> +> - 被加上标记的会被视为准备删除的变量; +> +> - 垃圾回收器完成内存清理工作,销毁那些带标记的值并回收他们所占用的内存空间。 +> +>2、引用计数方式: +> +>- 工作原理:跟踪记录每个值被引用的次数。 +> +>- 工作流程: +> +> - 声明了一个变量并将一个引用类型的值赋值给这个变量,这个引用类型值的引用次数就是 *1*; +> +> - 同一个值又被赋值给另一个变量,这个引用类型值的引用次数加1; +> +> - 当包含这个引用类型值的变量又被赋值成另一个值了,那么这个引用类型值的引用次数减 *1*; +> +> - 当引用次数变成 *0* 时,说明没办法访问这个值了; +> +> - 当垃圾收集器下一次运行时,它就会释放引用次数是0的值所占的内存。 + + + +### 76. 什么是作用域链、原型链 + +>参考答案: +> +>**什么是作用域链?** +> +>当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止,,而作用域链,就是有当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域对符合访问权限的变量和函数的有序访问。 +> +>**什么原型链?** +> +>每个对象都可以有一个原型\__*proto*__,这个原型还可以有它自己的原型,以此类推,形成一个原型链。查找特定属性的时候,我们先去这个对象里去找,如果没有的话就去它的原型对象里面去,如果还是没有的话再去向原型对象的原型对象里去寻找。这个操作被委托在整个原型链上,这个就是我们说的原型链。 + + + +### 77. *new* 一个构造函数发生了什么 + +>参考答案: +> +>*new* 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。 +> +>*new* 关键字会进行如下的操作: +>步骤 *1*:创建一个空的简单 *JavaScript* 对象,即 { } ; +>步骤 *2*:链接该对象到另一个对象(即设置该对象的原型对象); +>步骤 *3*:将步骤 *1* 新创建的对象作为 *this* 的上下文; +>步骤 *4*:如果该函数没有返回对象,则返回 *this*。 + + + +### 78. 对一个构造函数实例化后. 它的原型链指向什么 + +>参考答案: +> +>指向该构造函数实例化出来对象的原型对象。 +> +>对于构造函数来讲,可以通过 *prototype* 访问到该对象。 +> +>对于实例对象来讲,可以通过隐式属性 \__*proto*__ 来访问到。 + + + +### 79. 什么是变量提升 + +>参考答案: +> +>当 *JavaScript* 编译所有代码时,所有使用 *var* 的变量声明都被提升到它们的函数/局部作用域的顶部(如果在函数内部声明的话),或者提升到它们的全局作用域的顶部(如果在函数外部声明的话),而不管实际的声明是在哪里进行的。这就是我们所说的“提升”。 +> +>请记住,这种“提升”实际上并不发生在你的代码中,而只是一种比喻,与 *JavaScript* 编译器如何读取你的代码有关。记住当我们想到“提升”的时候,我们可以想象任何被提升的东西都会被移动到顶部,但是实际上你的代码并不会被修改。 +> +>函数声明也会被提升,但是被提升到了最顶端,所以将位于所有变量声明之上。 +> +>在编译阶段变量和函数声明会被放入内存中,但是你在代码中编写它们的位置会保持不变。 + + + +### 80. == 和 === 的区别是什么 + +> 参考答案: +> +> 简单来说: == 代表相同, === 代表严格相同(数据类型和值都相等)。 +> +> 当进行双等号比较时候,先检查两个操作数数据类型,如果相同,则进行===比较,如果不同,则愿意为你进行一次类型转换,转换成相同类型后再进行比较,而 === 比较时,如果类型不同,直接就是false。 +> +> 从这个过程来看,大家也能发现,某些情况下我们使用 === 进行比较效率要高些,因此,没有歧义的情况下,不会影响结果的情况下,在 *JS* 中首选 === 进行逻辑比较。 + + + +### 81. *Object.is* 方法比较的是什么 + +>参考答案: +> +>*Object.is* 方法是 *ES6* 新增的用来比较两个值是否严格相等的方法,与 === (严格相等)的行为基本一致。不过有两处不同: +> +>- +0 不等于 -0。 +>- *NaN* 等于自身。 +> +>所以可以将*Object.is* 方法看作是加强版的严格相等。 + + + +### 82. 基础数据类型和引用数据类型,哪个是保存在栈内存中?哪个是在堆内存中? + +> 参考答案: +> +> 在 *ECMAScript* 规范中,共定义了 *7* 种数据类型,分为 **基本类型** 和 **引用类型** 两大类,如下所示: +> +> - **基本类型**:*String、Number、Boolean、Symbol、Undefined、Null* +> +> - **引用类型**:*Object* +> +> 基本类型也称为简单类型,由于其占据空间固定,是简单的数据段,为了便于提升变量查询速度,将其存储在栈中,即按值访问。 +> +> 引用类型也称为复杂类型,由于其值的大小会改变,所以不能将其存放在栈中,否则会降低变量查询速度,因此,其值存储在堆(*heap*)中,而存储在变量处的值,是一个指针,指向存储对象的内存处,即按址访问。引用类型除 *Object* 外,还包括 *Function 、Array、RegExp、Date* 等等。 + + + +### 83. 箭头函数解决了什么问题? + +> 参考答案: +> +> 箭头函数主要解决了 *this* 的指向问题。 + +> 解析: +> +> 在 *ES5* 时代,一旦对象的方法里面又存在函数,则 *this* 的指向往往会让开发人员抓狂。 +> +> 例如: +> +> ```js +> //错误案例,this 指向会指向 Windows 或者 undefined +> var obj = { +> age: 18, +> getAge: function () { +> var a = this.age; // 18 +> var fn = function () { +> return new Date().getFullYear() - this.age; // this 指向 window 或 undefined +> }; +> return fn(); +> } +> }; +> console.log(obj.getAge()); // NaN +> ``` +> +> 然而,箭头函数没有 *this*,箭头函数的 *this* 是继承父执行上下文里面的 *this* +> +> ```js +> var obj = { +> age: 18, +> getAge: function () { +> var a = this.age; // 18 +> var fn = () => new Date().getFullYear() - this.age; // this 指向 obj 对象 +> return fn(); +> } +> }; +> +> console.log(obj.getAge()); // 2003 +> ``` + + + +### 84. *new* 一个箭头函数后,它的 *this* 指向什么? + +>参考答案: +> +>我不知道这道题是出题人写错了还是故意为之。 +> +>箭头函数无法用来充当构造函数,所以是无法 *new* 一个箭头函数的。 +> +>当然,也有可能是面试官故意挖的一个坑,等着你往里面跳。 + + + +### 85. *promise* 的其他方法有用过吗?如 *all、race*。请说下这两者的区别 + +>参考答案: +> +>*promise.all* 方法参数是一个 *promise* 的数组,只有当所有的 *promise* 都完成并返回成功,才会调用 *resolve*,当有一个失败,都会进*catch*,被捕获错误,*promise.all* 调用成功返回的结果是每个 *promise* 单独调用成功之后返回的结果组成的数组,如果调用失败的话,返回的则是第一个 *reject* 的结果 +> +>*promise.race* 也会调用所有的 *promise*,返回的结果则是所有 *promise* 中最先返回的结果,不关心是成功还是失败。 + + + +### 86. *class* 是如何实现的 + +>参考答案: +> +>*class* 是 *ES6* 新推出的关键字,它是一个语法糖,本质上就是基于这个原型实现的。只不过在以前 *ES5* 原型实现的基础上,添加了一些 *_classCallCheck、_defineProperties、_createClass*等方法来做出了一些特殊的处理。 +> +>例如: +> +>```js +>class Hello { +> constructor(x) { +> this.x = x; +> } +> greet() { +> console.log("Hello, " + this.x) +> } +>} +>``` +> +>```js +>"use strict"; +> +>function _classCallCheck(instance, Constructor) { +> if (!(instance instanceof Constructor)) { +> throw new TypeError("Cannot call a class as a function"); +> } +>} +> +>function _defineProperties(target, props) { +> for (var i = 0; i < props.length; i++) { +> var descriptor = props[i]; +> descriptor.enumerable = descriptor.enumerable || false; +> descriptor.configurable = true; +> if ("value" in descriptor) +> descriptor.writable = true; +> Object.defineProperty(target, descriptor.key, descriptor); +> } +>} +> +>function _createClass(Constructor, protoProps, staticProps) { +> console.log("Constructor::",Constructor); +> console.log("protoProps::",protoProps); +> console.log("staticProps::",staticProps); +> if (protoProps) +> _defineProperties(Constructor.prototype, protoProps); +> if (staticProps) +> _defineProperties(Constructor, staticProps); +> return Constructor; +>} +> +>var Hello = /*#__PURE__*/function () { +> function Hello(x) { +> _classCallCheck(this, Hello); +> +> this.x = x; +> } +> +> _createClass(Hello, [{ +> key: "greet", +> value: function greet() { +> console.log("Hello, " + this.x); +> } +> }]); +> +> return Hello; +>}(); +>``` + + + +### 87. *let、const、var* 的区别 + +>参考答案: +> +>请参阅前面第 *22* 题答案。 + + + +### 88. *ES6* 中模块化导入和导出与 *common.js* 有什么区别 + +>参考答案: +> +>CommonJs模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化不会影响到这个值. +> +>``` +>// common.js +>var count = 1; +> +>var printCount = () =>{ +> return ++count; +>} +> +>module.exports = { +> printCount: printCount, +> count: count +>}; +>// index.js +>let v = require('./common'); +>console.log(v.count); // 1 +>console.log(v.printCount()); // 2 +>console.log(v.count); // 1 +>``` +> +>你可以看到明明common.js里面改变了count,但是输出的结果还是原来的。这是因为count是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动的值。将common.js里面的module.exports 改写成 +> +>``` +>module.exports = { +> printCount: printCount, +> get count(){ +> return count +> } +>}; +>``` +> +>这样子的输出结果是 1,2,2 +> +>而在ES6当中,写法是这样的,是利用export 和import导入的 +> +>``` +>// es6.js +>export let count = 1; +>export function printCount() { +> ++count; +>} +>// main1.js +>import { count, printCount } from './es6'; +>console.log(count) +>console.log(printCount()); +>console.log(count) +>``` +> +>ES6 模块是动态引用,并且不会缓存,模块里面的变量绑定其所有的模块,而是动态地去加载值,并且不能重新赋值, +> +>ES6 输入的模块变量,只是一个“符号连接符”,所以这个变量是只读的,对它进行重新赋值会报错。如果是引用类型,变量指向的地址是只读的,但是可以为其添加属性或成员。 +> +>另外还想说一个 *export default* +> +>``` +> let count = 1; +> function printCount() { +> ++count; +>} +>export default { count, printCount} +>// main3.js +>import res form './main3.js' +>console.log(res.count) +>``` +> +>export与export default的区别及联系: +> +>1. export与export default均可用于导出常量、函数、文件、模块等 +> +>2. 你可以在其它文件或模块中通过 import + (常量 | 函数 | 文件 | 模块)名的方式,将其导入,以便能够对其进行使用 +> +>3. 在一个文件或模块中,export、import可以有多个,export default仅有一个 +> +>4. 通过export方式导出,在导入时要加{ },export default则不需要。 + + + +### 89. 说一下普通函数和箭头函数的区别 + +>参考答案: +> +>请参阅前面第 *8、25、83* 题答案。 + + + +### 90. 说一下 *promise* 和 *async* 和 *await* 什么关系 + +>参考答案: +> +>*await* 表达式会造成异步函数停止执行并且等待*promise*的解决,当值被*resolved*,异步函数会恢复执行以及返回*resolved*值。如果该值不是一个*promise*,它将会被转换成一个*resolved*后的*promise*。如果*promise*被*rejected*,*await* 表达式会抛出异常值。 + + + +### 91. 说一下你学习过的有关 *ES6* 的知识点 + +>参考答案: +> +>这种题目是开放题,可以简单列举一下 *ES6* 的新增知识点。( *ES6* 的新增知识点参阅前面第 *44* 题) +> +>然后说一下自己平时开发中用得比较多的是哪些即可。 +> +>一般面试官会针对你所说的内容进行二次提问。例如:你回答平时开发中箭头函数用得比较多,那么面试官极大可能针对箭头函数展开二次提问,询问你箭头函数有哪些特性?箭头函数 *this* 特点之类的问题。 + + + +### 92. 了解过 *js* 中 *arguments* 吗?接收的是实参还是形参? + +>参考答案: +> +>*JS* 中的 *arguments* 是一个伪数组对象。这个伪数组对象将包含调用函数时传递的所有的实参。 +> +>与之相对的,*JS* 中的函数还有一个 *length* 属性,返回的是函数形参的个数。 + + + +### 93. *ES6* 相比于 *ES5* 有什么变化 + +>参考答案: +> +>*ES6* 相比 *ES5* 新增了很多新特性,这里可以自己简述几个。 +> +>具体的新增特性可以参阅前面第 *44* 题。 + + + +### 94. 强制类型转换方法有哪些? + +>参考答案: +> +>JavaScript 中的数据类型转换,主要有三种方式: +> +>1. 转换函数 +> +>*js* 提供了诸如 *parseInt* 和 *parseFloat* 这些转换函数,通过这些转换函数可以进行数据类型的转换 。 +> +>2. 强制类型转换 +> +>还可使用强制类型转换(*type casting*)处理转换值的类型。 +> +>例如: +> +>- *Boolean*(*value*) 把给定的值转换成 *Boolean* 型; +>- *Number*(*value*)——把给定的值转换成数字(可以是整数或浮点数); +>- *String*(*value*)——把给定的值转换成字符串。 +> +>3. 利用 *js* 变量弱类型转换。 +> +>例如: +> +>- 转换字符串:直接和一个空字符串拼接,例如:`a = "" + 数据` +> +>- 转换布尔:!!数据类型,例如:`!!"Hello"` +> +>- 转换数值:数据*1 或 /1,例如:`"Hello * 1"` + + + +### 95. 纯函数 + +>参考答案: +> +>一个函数,如果符合以下两个特点,那么它就可以称之为**纯函数**: +> +>1. 对于相同的输入,永远得到相同的输出 +>2. 没有任何可观察到的副作用 + +> 解析: +> +> 针对上面的两个特点,我们一个一个来看。 +> +> - 相同输入得到相同输出 +> +> 我们先来看一个不纯的反面典型: +> +> ``` +> let greeting = 'Hello' +> +> function greet (name) { +> return greeting + ' ' + name +> } +> +> console.log(greet('World')) // Hello World +> ``` +> +> 上面的代码中,*greet('World')* 是不是永远返回 *Hello World* ? 显然不是,假如我们修改 *greeting* 的值,就会影响 *greet* 函数的输出。即函数 *greet* 其实是 **依赖外部状态** 的。 +> +> 那我们做以下修改: +> +> ``` +> function greet (greeting, name) { +> return greeting + ' ' + name +> } +> +> console.log(greet('Hi', 'Savo')) // Hi Savo +> ``` +> +> 将 *greeting* 参数也传入,这样对于任何输入参数,都有与之对应的唯一的输出参数了,该函数就符合了第一个特点。 +> +> - 没有副作用 +> +> 副作用的意思是,这个函数的运行,**不会修改外部的状态**。 +> +> 下面再看反面典型: +> +> ``` +> const user = { +> username: 'savokiss' +> } +> +> let isValid = false +> +> function validate (user) { +> if (user.username.length > 4) { +> isValid = true +> } +> } +> ``` +> +> 可见,执行函数的时候会修改到 *isValid* 的值(注意:如果你的函数没有任何返回值,那么它很可能就具有副作用!) +> +> 那么我们如何移除这个副作用呢?其实不需要修改外部的 *isValid* 变量,我们只需要在函数中将验证的结果 *return* 出来: +> +> ``` +> const user = { +> username: 'savokiss' +> } +> +> function validate (user) { +> return user.username.length > 4; +> } +> +> const isValid = validate(user) +> ``` +> +> 这样 *validate* 函数就不会修改任何外部的状态了~ + + + +### 96. *JS* 模块化 + +>参考答案: +> +>模块化主要是用来抽离公共代码,隔离作用域,避免变量冲突等。 +> +>模块化的整个发展历史如下: +> +>**IIFE**: 使用自执行函数来编写模块化,特点:**在一个单独的函数作用域中执行代码,避免变量冲突**。 +> +>```js +>(function(){ +> return { +> data:[] +> } +>})() +>``` +> +>**AMD**: 使用requireJS 来编写模块化,特点:**依赖必须提前声明好**。 +> +>```js +>define('./index.js',function(code){ +> // code 就是index.js 返回的内容 +>}) +>``` +> +>**CMD**: 使用seaJS 来编写模块化,特点:**支持动态引入依赖文件**。 +> +>```js +>define(function(require, exports, module) { +> var indexCode = require('./index.js'); +>}); +>``` +> +>**CommonJS**: nodejs 中自带的模块化。 +> +>```js +>var fs = require('fs'); +>``` +> +>**UMD**:兼容AMD,CommonJS 模块化语法。 +> +>**webpack(require.ensure)**:webpack 2.x 版本中的代码分割。 +> +>**ES Modules**: ES6 引入的模块化,支持import 来引入另一个 js 。 +> +>```js +>import a from 'a'; +>``` + + + +### 97. 看过 *jquery* 源码吗? + +>参考答案: +> +>开放题,但是需要注意的是,如果看过 *jquery* 源码,不要简单的回答一个“看过”就完了,应该继续乘胜追击,告诉面试官例如哪个哪个部分是怎么怎么实现的,并针对这部分的源码实现,可以发表一些自己的看法和感想。 + + + +### 98. 说一下 *js* 中的 *this* + +> 参考答案: +> +> 请参阅前面第 *17* 题答案。 + + + +### 99. *apply call bind* 区别,手写 + +>参考答案: +> +>apply call bind 区别 ? +> +>*call* 和 *apply* 的功能相同,区别在于传参的方式不一样: +> +>- *fn.call(obj, arg1, arg2, ...)* 调用一个函数, 具有一个指定的 *this* 值和分别地提供的参数(参数的列表)。 +>- *fn.apply(obj, [argsArray])* 调用一个函数,具有一个指定的 *this* 值,以及作为一个数组(或类数组对象)提供的参数。 +> +>*bind* 和 *call/apply* 有一个很重要的区别,一个函数被 *call/apply* 的时候,会直接调用,但是 *bind* 会创建一个新函数。当这个新函数被调用时,*bind( )* 的第一个参数将作为它运行时的 *this*,之后的一序列参数将会在传递的实参前传入作为它的参数。 +> +>实现 *call* 方法: +> +>```js +>Function.prototype.call2 = function (context) { +> //没传参数或者为 null 是默认是 window +> var context = context || (typeof window !== 'undefined' ? window : global) +> // 首先要获取调用 call 的函数,用 this 可以获取 +> context.fn = this +> var args = [] +> for (var i = 1; i < arguments.length; i++) { +> args.push('arguments[' + i + ']') +> } +> eval('context.fn(' + args + ')') +> delete context.fn +>} +> +>// 测试 +>var value = 3 +>var foo = { +> value: 2 +>} +> +>function bar(name, age) { +> console.log(this.value) +> console.log(name) +> console.log(age) +>} +>bar.call2(null) +>// 浏览器环境: 3 undefinde undefinde +>// Node环境:undefinde undefinde undefinde +> +>bar.call2(foo, 'cc', 18) // 2 cc 18 +> +>``` +> +> +> +>实现 *apply* 方法: +> +>```js +>Function.prototype.apply2 = function (context, arr) { +> var context = context || (typeof window !== 'undefined' ? window : global) +> context.fn = this; +> +> var result; +> if (!arr) { +> result = context.fn(); +> } +> else { +> var args = []; +> for (var i = 0, len = arr.length; i < len; i++) { +> args.push('arr[' + i + ']'); +> } +> result = eval('context.fn(' + args + ')') +> } +> +> delete context.fn +> return result; +>} +> +>// 测试: +> +>var value = 3 +>var foo = { +> value: 2 +>} +> +>function bar(name, age) { +> console.log(this.value) +> console.log(name) +> console.log(age) +>} +>bar.apply2(null) +>// 浏览器环境: 3 undefinde undefinde +>// Node环境:undefinde undefinde undefinde +> +>bar.apply2(foo, ['cc', 18]) // 2 cc 18 +>``` +> +> +> +>实现 *bind* 方法: +> +>```js +>Function.prototype.bind2 = function (oThis) { +> 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 aArgs = Array.prototype.slice.call(arguments, 1), +> fToBind = this, +> fNOP = function () { }, +> fBound = function () { +> return fToBind.apply(this instanceof fNOP && oThis +> ? this +> : oThis || window, +> aArgs.concat(Array.prototype.slice.call(arguments))); +> }; +> +> fNOP.prototype = this.prototype; +> fBound.prototype = new fNOP(); +> +> return fBound; +>} +> +>// 测试 +>var test = { +> name: "jack" +>} +>var demo = { +> name: "rose", +> getName: function () { return this.name; } +>} +> +>console.log(demo.getName()); // 输出 rose 这里的 this 指向 demo +> +>// 运用 bind 方法更改 this 指向 +>var another2 = demo.getName.bind2(test); +>console.log(another2()); // 输出 jack 这里 this 指向了 test 对象了 +>``` + + + +### 100. 手写 *reduce flat* + +> 参考答案: +> +> *reduce* 实现: +> +> ```js +> Array.prototype.my_reduce = function (callback, initialValue) { +> if (!Array.isArray(this) || !this.length || typeof callback !== 'function') { +> return [] +> } else { +> // 判断是否有初始值 +> let hasInitialValue = initialValue !== undefined; +> let value = hasInitialValue ? initialValue : tihs[0]; +> for (let index = hasInitialValue ? 0 : 1; index < this.length; index++) { +> const element = this[index]; +> value = callback(value, element, index, this) +> } +> return value +> } +> } +> +> let arr = [1, 2, 3, 4, 5] +> let res = arr.my_reduce((pre, cur, i, arr) => { +> console.log(pre, cur, i, arr) +> return pre + cur +> }, 10) +> console.log(res)//25 +> ``` +> +> +> +> *flat* 实现: +> +> ```js +> let arr = [1, [2, 3, [4, 5, [12, 3, "zs"], 7, [8, 9, [10, 11, [1, 2, [3, 4]]]]]]]; +> +> //万能的类型检测方法 +> const checkType = (arr) => { +> return Object.prototype.toString.call(arr).slice(8, -1); +> } +> //自定义flat方法,注意:不可以使用箭头函数,使用后内部的this会指向window +> Array.prototype.myFlat = function (num) { +> //判断第一层数组的类型 +> let type = checkType(this); +> //创建一个新数组,用于保存拆分后的数组 +> let result = []; +> //若当前对象非数组则返回undefined +> if (!Object.is(type, "Array")) { +> return; +> } +> //遍历所有子元素并判断类型,若为数组则继续递归,若不为数组则直接加入新数组 +> this.forEach((item) => { +> let cellType = checkType(item); +> if (Object.is(cellType, "Array")) { +> //形参num,表示当前需要拆分多少层数组,传入Infinity则将多维直接降为一维 +> num--; +> if (num < 0) { +> let newArr = result.push(item); +> return newArr; +> } +> //使用三点运算符解构,递归函数返回的数组,并加入新数组 +> result.push(...item.myFlat(num)); +> } else { +> result.push(item); +> } +> }) +> return result; +> } +> console.time(); +> +> console.log(arr.flat(Infinity)); //[1, 2, 3, 4, 5, 12, 3, "zs", 7, 8, 9, 10, 11, 1, 2, 3, 4]; +> +> console.log(arr.myFlat(Infinity)); //[1, 2, 3, 4, 5, 12, 3, "zs", 7, 8, 9, 10, 11, 1, 2, 3, 4]; +> //自定义方法和自带的flat返回结果一致!!!! +> console.timeEnd(); +> ``` + + + +### 101. == 隐试转换的原理?是怎么转换的 + +> 参考答案: +> +> **两个与类型转换有关的函数:valueOf()和toString()** +> +> - valueOf()的语义是,返回这个对象逻辑上对应的原始类型的值。比如说,String包装对象的valueOf(),应该返回这个对象所包装的字符串。 +> - toString()的语义是,返回这个对象的字符串表示。用一个字符串来描述这个对象的内容。 +> +> valueOf()和toString()是定义在Object.prototype上的方法,也就是说,所有的对象都会继承到这两个方法。但是在Object.prototype上定义的这两个方法往往不能满足我们的需求(Object.prototype.valueOf()仅仅返回对象本身),因此js的许多内置对象都重写了这两个函数,以实现更适合自身的功能需要(比如说,String.prototype.valueOf就覆盖了在Object.prototype中定义的valueOf)。当我们自定义对象的时候,最好也重写这个方法。重写这个方法时要遵循上面所说的语义。 +> +> **js内部用于实现类型转换的4个函数** +> +> 这4个方法实际上是ECMAScript定义的4个抽象的操作,它们在js内部使用,进行类型转换。js的使用者不能直接调用这些函数。 +> +> - ToPrimitive ( input [ , PreferredType ] ) +> - ToBoolean ( argument ) +> - ToNumber ( argument ) +> - ToString ( argument ) +> +> 需要区分这里的 ToString() 和上文谈到的 toString(),一个是 js 引擎内部使用的函数,另一个是定义在对象上的函数。 +> +> (1)ToPrimitive ( input [ , PreferredType ] ) +> +> 将 input 转化成一个原始类型的值。PreferredType参数要么不传入,要么是Number 或 String。**如果PreferredType参数是Number**,ToPrimitive这样执行: +> +> 1. 如果input本身就是原始类型,直接返回input。 +> 2. 调用**input.valueOf()**,如果结果是原始类型,则返回这个结果。 +> 3. 调用**input.toString()**,如果结果是原始类型,则返回这个结果。 +> 4. 抛出TypeError异常。 +> +> **以下是PreferredType不为Number时的执行顺序。** +> +> - 如果PreferredType参数是String,则交换上面这个过程的第2和第3步的顺序,其他执行过程相同。 +> - 如果PreferredType参数没有传入 +> - 如果input是内置的Date类型,PreferredType 视为String +> - 否则PreferredType 视为 Number +> +> **可以看出,ToPrimitive依赖于valueOf和toString的实现。** +> +> (2)ToBoolean ( argument ) +> +> image-20210819164742154 +> +> 只需要记忆 *0, null, undefined, NaN, ""* 返回 *false* 就可以了,其他一律返回 *true*。 +> +> (3)ToNumber ( argument ) +> +> image-20210819164927980 +> +> ToNumber的转化并不总是成功,有时会转化成NaN,有时则直接抛出异常。 +> +> (4)ToString ( argument ) +> +> image-20210819165004906 +> +> 当js期望得到某种类型的值,而实际在那里的值是其他的类型,就会发生隐式类型转换。系统内部会自动调用我们前面说ToBoolean ( argument )、ToNumber ( argument )、ToString ( argument ),尝试转换成期望的数据类型。 + + + +### 102. ['1', '2', '3'].map(parseInt) 结果是什么,为什么 (字节) + +> 参考答案: +> +> [1, NaN, NaN] + +>解析: +> +>一、为什么会是这个结果? +>1. *map* 函数 +> +>将数组的每个元素传递给指定的函数处理,并返回处理后的数组,所以 *['1','2','3'].map(parseInt)* 就是将字符串 *1,2,3* 作为元素;*0,1,2* 作为下标分别调用 *parseInt* 函数。即分别求出 *parseInt('1',0), parseInt('2',1), parseInt('3',2)* 的结果。 +> +>2. *parseInt* 函数(重点) +> +>概念:以第二个参数为基数来解析第一个参数字符串,通常用来做十进制的向上取整(省略小数)如:parseInt(2.7) //结果为2 +> +>特点:接收两个参数 *parseInt(string,radix)* +> +>*string*:字母(大小写均可)、数组、特殊字符(不可放在开头,特殊字符及特殊字符后面的内容不做解析)的任意字符串,如 '2'、'2w'、'2!' +> +>*radix*:解析字符串的基数,基数规则如下: +> +> 1) 区间范围介于 *2~36* 之间; +> +> 2 ) 当参数为 *0*,*parseInt( )* 会根据十进制来解析; +> +> 3 ) 如果忽略该参数,默认的基数规则: +> +>​ 如果 *string* 以 "0x" 开头,parseInt() 会把 string 的其余部分解析为十六进制的整数;parseInt("0xf") // 15 +>​ 如果 *string* 以 0 开头,其后的字符解析为八进制或十六进制的数字;parseInt("08") // 8 +>​ 如果 *string* 以 1 ~ 9 的数字开头,parseInt() 将把它解析为十进制的整数;parseInt("88.99f") // 88 +>​ 只有字符串中的第一个数字会被返回。parseInt("10.33") // 返回10; +>​ 开头和结尾的空格是允许的。parseInt(" 69 10 ") // 返回69 +>​ 如果字符串的第一个字符不能被转换为数字,返回 NaN。parseInt("f") // 返回 NaN 而 parseInt("f",16) // 返回15 +> +>二、*parseInt* 方法解析的运算过程 +> +>parseInt('101.55',10); // 以十进制解析,运算过程:向上取整数(不做四舍五入,省略小数),结果为 101。 +> +>parseInt('101',2); // 以二进制解析,运算过程:1*2的2次方+0*2的1次方+1*2的0次方=4+0+1=5,结果为 5。 +> +>parseInt('101',8); // 以八进制解析,运算过程:1*8的2次方+0*8的1次方+1*8的0次方=64+0+1=65,结果为 65。 +> +>parseInt('101',16); // 以十六进制解析,运算过程:1*16的2次方+0*16的1次方+1*16的0次方=256+0+1=257,结果为 257。 +> +>三、再来分析一下结果 +> +>*['1','2','3'].map(parseInt)* 即 +> +>parseInt('1',0); radix 为 0,parseInt( ) 会根据十进制来解析,所以结果为 *1*; +> +>parseInt('2',1); radix 为 1,超出区间范围,所以结果为 *NaN*; +> +>parseInt('3',2); radix 为 2,用2进制来解析,应以 *0* 和 *1* 开头,所以结果为 *NaN*。 + + + +### 103. 防抖,节流是什么,如何实现 (字节) + +> 参考答案: +> +> 我们在平时开发的时候,会有很多场景会频繁触发事件,比如说搜索框实时发请求,*onmousemove、resize、onscroll* 等,有些时候,我们并不能或者不想频繁触发事件,这时候就应该用到函数防抖和函数节流。 +> +> 函数防抖(*debounce*),指的是短时间内多次触发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行。 +> +> 具体实现: +> +> ```js +> /** +> * 函数防抖 +> * @param {function} func 一段时间后,要调用的函数 +> * @param {number} wait 等待的时间,单位毫秒 +> */ +> function debounce(func, wait){ +> // 设置变量,记录 setTimeout 得到的 id +> let timerId = null; +> return function(...args){ +> if(timerId){ +> // 如果有值,说明目前正在等待中,清除它 +> clearTimeout(timerId); +> } +> // 重新开始计时 +> timerId = setTimeout(() => { +> func(...args); +> }, wait); +> } +> } +> ``` +> +> +> +> 函数节流(*throttle*),指连续触发事件但是在 *n* 秒中只执行一次函数。即 *2n* 秒内执行 *2* 次... 。节流如字面意思,会稀释函数的执行频率。 +> +> 具体实现: +> +> ```js +> function throttle(func, wait) { +> let context, args; +> let previous = 0; +> return function () { +> let now = +new Date(); +> context = this; +> args = arguments; +> if (now - previous > wait) { +> func.apply(context, args); +> previous = now; +> } +> } +> } +> ``` + + + +### 104. 介绍下 *Set、Map、WeakSet* 和 *WeakMap* 的区别(字节) + +> 参考答案: +> +> **Set** +> +> - 成员唯一、无序且不重复 +> +> - 键值与键名是一致的(或者说只有键值,没有键名) +> +> - 可以遍历,方法有 *add, delete,has* +> +> **WeakSet** +> +> - 成员都是对象 +> +> - 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存 *DOM* 节点,不容易造成内存泄漏 +> +> - 不能遍历,方法有 *add, delete,has* +> +> **Map** +> +> - 本质上是健值对的集合,类似集合 +> +> - 可以遍历,方法很多,可以跟各种数据格式转换 +> +> **WeakMap** +> +> - 只接受对象作为健名(*null* 除外),不接受其他类型的值作为健名 +> - 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾机制回收,此时键名是无效的 +> +> - 不能遍历,方法有 *get、set、has、delete* + + + +### 105. *setTimeout、Promise、Async/Await* 的区别(字节) + +> 参考答案: +> +> 事件循环中分为宏任务队列和微任务队列。 +> +> 其中 *setTimeout* 的回调函数放到宏任务队列里,等到执行栈清空以后执行; +> +> *promise.then* 里的回调函数会放到相应宏任务的微任务队列里,等宏任务里面的同步代码执行完再执行; +> +> *async* 函数表示函数里面可能会有异步方法,*await* 后面跟一个表达式,*async* 方法执行时,遇到 *await* 会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行。 + + + +### 106. *Promise* 构造函数是同步执行还是异步执行,那么 *then* 方法呢?(字节) + +> 参考答案: +> +> *promise* 构造函数是同步执行的,*then* 方法是异步执行,*then* 方法中的内容加入微任务中。 + + + +### 107. 情人节福利题,如何实现一个 *new* (字节) + +>参考答案: +>首先我们需要明白 *new* 的原理。关于 *new* 的原理,主要分为以下几步: +> +> - 创建一个空对象 。 +> +> - 由 *this* 变量引用该对象 。 +> +> - 该对象继承该函数的原型(更改原型链的指向) 。 +> +> - 把属性和方法加入到 *this* 引用的对象中。 +> +> - 新创建的对象由 *this* 引用 ,最后隐式地返回 *this* +> +> 明白了这个原理后,我们就可以尝试来实现一个 *new* 方法,参考示例如下: +> +> ```js +> // 构造器函数 +let Parent = function (name, age) { + this.name = name; + this.age = age; +}; +Parent.prototype.sayName = function () { + console.log(this.name); +}; +//自己定义的new方法 +let newMethod = function (Parent, ...rest) { + // 1.以构造器的prototype属性为原型,创建新对象; + let child = Object.create(Parent.prototype); + // 2.将this和调用参数传给构造器执行 + let result = Parent.apply(child, rest); + // 3.如果构造器没有手动返回对象,则返回第一步的对象 + return typeof result === 'object' ? result : child; +}; +//创建实例,将构造函数Parent与形参作为参数传入 +const child = newMethod(Parent, 'echo', 26); +child.sayName() //'echo'; +//最后检验,与使用new的效果相同 +console.log(child instanceof Parent)//true +console.log(child.hasOwnProperty('name'))//true +console.log(child.hasOwnProperty('age'))//true +console.log(child.hasOwnProperty('sayName'))//false +> ``` + + + +### 108. 实现一个 *sleep* 函数(字节) + +> 参考答案: +> +> ```js +> function sleep(delay) { +> var start = (new Date()).getTime(); +> while ((new Date()).getTime() - start < delay) { +> continue; +> } +> } +> +> function test() { +> console.log('111'); +> sleep(2000); +> console.log('222'); +> } +> +> test() +> ``` +> +> 这种实现方式是利用一个伪死循环阻塞主线程。因为 *JS* 是单线程的。所以通过这种方式可以实现真正意义上的 *sleep*。 + + + +### 109. 使用 sort() 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果 (字节) + +> 参考答案: +> +> *sort* 方法默认按照 *ASCII* 码来排序,如果要按照数字大小来排序,需要传入一个回调函数,如下: +> +> ```js +> [3, 15, 8, 29, 102, 22].sort((a,b) => {return a - b}); +> ``` + + + +### 110. 实现 5.add(3).sub(2) (百度) + +> 参考答案: +> +> 这里想要实现的是链式操作,那么我们可以考虑在 *Number* 类型的原型上添加 *add* 和 *sub* 方法,这两个方法返回新的数 +> +> 示例如下: +> +> ```js +> Number.prototype.add = function (number) { +> if (typeof number !== 'number') { +> throw new Error('请输入数字~'); +> } +> return this.valueOf() + number; +> }; +> Number.prototype.minus = function (number) { +> if (typeof number !== 'number') { +> throw new Error('请输入数字~'); +> } +> return this.valueOf() - number; +> }; +> console.log((5).add(3).minus(2)); // 6 +> ``` + + + +### 111. 给定两个数组,求交集 + +> 参考答案: +> +> 示例代码如下: +> +> ```js +> function intersect(nums1, nums2) { +> let i = j = 0, +> len1 = nums1.length, +> len2 = nums2.length, +> newArr = []; +> if (len1 === 0 || len2 === 0) { +> return newArr; +> } +> nums1.sort(function (a, b) { +> return a - b; +> }); +> nums2.sort(function (a, b) { +> return a - b; +> }); +> while (i < len1 || j < len2) { +> if (nums1[i] > nums2[j]) { +> j++; +> } else if (nums1[i] < nums2[j]) { +> i++; +> } else { +> if (nums1[i] === nums2[j]) { +> newArr.push(nums1[i]); +> } +> if (i < len1 - 1) { +> i++; +> } else { +> break; +> } +> if (j < len2 - 1) { +> j++; +> } else { +> break; +> } +> } +> } +> return newArr; +> }; +> // 测试 +> console.log(intersect([3, 5, 8, 1], [2, 3])); +> ``` + + + +### 112. 为什么普通 *for* 循环的性能远远高于 *forEach* 的性能,请解释其中的原因。 + +> 参考答案: +> +> *for* 循环按顺序遍历,*forEach* 使用 *iterator* 迭代器遍历 +> +> 下面是一段性能测试的代码: +> +> ```js +> let arrs = new Array(100000); +> console.time('for'); +> for (let i = 0; i < arrs.length; i++) { +> }; +> console.timeEnd('for'); +> console.time('forEach'); +> arrs.forEach((arr) => { +> }); +> console.timeEnd('forEach'); +> +> for: 2.263ms +> forEach: 0.254ms +> ``` +> +> 在10万这个级别下,`forEach`的性能是`for`的十倍 +> +> ```js +> for: 2.263ms +> forEach: 0.254ms +> ``` +> +> 在100万这个量级下,`forEach`的性能是和`for`的一致 +> +> ```js +> for: 2.844ms +> forEach: 2.652ms +> ``` +> +> 在1000万级以上的量级上 ,`forEach`的性能远远低于`for`的性能 +> +> ```js +> for: 8.422ms +> forEach: 30.328m +> ``` +> +> 我们从语法上面来观察: +> +> ```js +> arr.forEach(callback(currentValue [, index [, array]])[, thisArg]) +> ``` +> +> 可以看到 *forEach* 是有回调的,它会按升序为数组中含有效值的每一项执行一次 *callback*,且除了抛出异常以外,也没有办法中止或者跳出 *forEach* 循环。那这样的话执行就会额外的调用栈和函数内的上下文。 +> +> 而 *for* 循环则是底层写法,不会产生额外的消耗。 +> +> 在实际业务中没有很大的数组时,*for* 和 *forEach* 的性能差距其实很小,*forEach* 甚至会优于 *for* 的时间,且更加简洁,可读性也更高,一般也会优先使用 *forEach* 方法来进行数组的循环处理。 + + + +### 113. 实现一个字符串匹配算法,从长度为 n 的字符串 S 中,查找是否存在字符串 T,T 的长度是 m,若存在返回所在位置。 + +> 参考答案: +> +> ```js +> // 完全不用 API +> var getIndexOf = function (s, t) { +> let n = s.length; +> let m = t.length; +> if (!n || !m || n < m) return -1; +> for (let i = 0; i < n; i++) { +> let j = 0; +> let k = i; +> if (s[k] === t[j]) { +> k++; j++; +> while (k < n && j < m) { +> if (s[k] !== t[j]) break; +> else { +> k++; j++; +> } +> } +> if (j === m) return i; +> } +> } +> return -1; +> } +> +> // 测试 +> console.log(getIndexOf("Hello World", "rl")) +> ``` + + + +### 114. 使用 *JavaScript Proxy* 实现简单的数据绑定 + +> 参考答案: +> +> 示例代码如下: +> +> ```js +> +> hello,world +> +>

+> +> +> +> ``` + + + +### 115. 数组里面有 *10* 万个数据,取第一个元素和第 *10* 万个元素的时间相差多少(字节) + +> 参考答案: +> +> 消耗时间几乎一致,差异可以忽略不计 + +> 解析: +> +> - 数组可以直接根据索引取的对应的元素,所以不管取哪个位置的元素的时间复杂度都是 O(1) +> - *JavaScript* 没有真正意义上的数组,所有的数组其实是对象,其“索引”看起来是数字,其实会被转换成字符串,作为属性名(对象的 *key*)来使用。所以无论是取第 *1* 个还是取第 *10* 万个元素,都是用 *key* 精确查找哈希表的过程,其消耗时间大致相同。 + + + +### 116. 打印出 *1~10000* 以内的对称数 + +> 参考答案: +> +> ```js +> function isSymmetryNum(start, end) { +> for (var i = start; i < end + 1; i++) { +> var iInversionNumber = +(i.toString().split("").reverse().join("")); +> +> if (iInversionNumber === i && i > 10) { +> console.log(i); +> } +> +> } +> } +> isSymmetryNum(1, 10000); +> ``` + + + +### 117. 简述同步和异步的区别 + +> 参考答案: +> +> 同步意味着每一个操作必须等待前一个操作完成后才能执行。 +> 异步意味着操作不需要等待其他操作完成后才开始执行。 +> 在 *JavaScript* 中,由于单线程的特性导致所有代码都是同步的。但是,有些异步操作(例如:`XMLHttpRequest` 或 `setTimeout`)并不是由主线程进行处理的,他们由本机代码(浏览器 API)所控制,并不属于程序的一部分。但程序中被执行的回调部分依旧是同步的。 +> +> 加分回答: +> +> - *JavaScript* 中的同步任务是指在主线程上排队执行的任务,只有前一个任务执行完成后才能执行后一个任务;异步任务是指进入任务队列(*task queue*)而非主线程的任务,只有当任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程中进行执行。 +> - *JavaScript* 的并发模型是基于 “*event loop*”。 +> - 像 `alert` 这样的方法回阻塞主线程,以致用户关闭他后才能继续进行后续的操作。 +> - *JavaScript* 主要用于和用户互动及操作 DOM,多线程的情况和异步操作带来的复杂性相比决定了他单线程的特性。 +> - *Web Worker* 虽然允许 *JavaScript* 创建多个线程,但子线程完全受主线程控制,且不能操作 *DOM*。因此他还是保持了单线程的特性。 + + + +### 118. 怎么添加、移除、复制、创建、和查找节点 + +> 参考答案: +> +> 1)创建新节点 +> +> *createDocumentFragment*( ) // 创建一个*DOM* 片段 +> +> *createElement*( ) // 创建一个具体的元素 +> +> *createTextNode*( ) // 创建一个文本节点 +> +> (2)添加、移除、替换、插入 +> +> *appendChild*( ) +> +> *removeChild*( ) +> +> *replaceChild*( ) +> +> *insertBefore*( ) // 在已有的子节点前插入一个新的子节点 +> +> (3)查找 +> +> *getElementsByTagName*( ) //通过标签名称 +> +> *getElementsByName*( ) // 通过元素的 *Name* 属性的值 +> +> *getElementById*( ) // 通过元素 *Id*,唯一性 +> +> *querySelector*( ) // 用于接收一个 *CSS* 选择符,返回与该模式匹配的第一个元素 +> +> *querySelectorAll*( ) // 用于选择匹配到的所有元素 + + + +### 119. 实现一个函数 *clone* 可以对 *Javascript* 中的五种主要数据类型(*Number、string、 Object、Array、Boolean*)进行复制 + +> 参考答案: +> +> 示例代码如下: +> +> ```js +> /** +> * 对象克隆 +> * 支持基本数据类型及对象 +> * 递归方法 +> */ +> function clone(obj) { +> var o; +> switch (typeof obj) { +> case "undefined": +> break; +> case "string": +> o = obj + ""; +> break; +> case "number": +> o = obj - 0; +> break; +> case "boolean": +> o = obj; +> break; +> case "object": // object 分为两种情况 对象(Object)或数组(Array) +> if (obj === null) { +> o = null; +> } else { +> if (Object.prototype.toString.call(obj).slice(8, -1) === "Array") { +> o = []; +> for (var i = 0; i < obj.length; i++) { +> o.push(clone(obj[i])); +> } +> } else { +> o = {}; +> for (var k in obj) { +> o[k] = clone(obj[k]); +> } +> } +> } +> break; +> default: +> o = obj; +> break; +> } +> return o; +> } +> ``` + + + +### 120. 如何消除一个数组里面重复的元素 + +> 参考答案: +> +> 请参阅前面第 *2* 题。 + + + +### 121. 写一个返回闭包的函数 + +> 参考答案: +> +> ```js +> function foo() { +> var i = 0; +> return function () { +> console.log(i++); +> } +> } +> var f1 = foo(); +> f1(); // 0 +> f1(); // 1 +> f1(); // 2 +> ``` + + + +### 122. 使用递归完成 1 到 100 的累加 + +> 参考答案: +> +> ```js +> function add(x, y){ +> if(x === y){ +> return x; +> } else { +> return y + add(x, y-1); +> } +> } +> +> console.log(add(1, 100)) +> ``` + + + +### 123. *Javascript* 有哪几种数据类型 + +> 参考答案: +> +> 请参阅前面第 *26* 题。 + + + +### 124. 如何判断数据类型 + +> 参考答案: +> +> 请参阅前面第 *69* 题。 + + + +### 125. console.log(1+'2')和 console.log(1-'2')的打印结果 + +> 参考答案: +> +> 第一个打印出 '12',是一个 *string* 类型的值。 +> +> 第二个打印出 -1,是一个 *number* 类型的值 + + + +### 126. *JS* 的事件委托是什么,原理是什么 + +> 参考答案: +> +> 事件委托,又被称之为事件代理。在 *JavaScript* 中,添加到页面上的事件处理程序数量将直接关系到页面整体的运行性能。导致这一问题的原因是多方面的。 +> +> 首先,每个函数都是对象,都会占用内存。内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 *DOM* 访问次数,会延迟整个页面的交互就绪时间。 +> +> 对事件处理程序过多问题的解决方案就是事件委托。 +> +> 事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,*click* 事件会一直冒泡到 *document* 层次。也就是说,我们可以为整个页面指定一个 *onclick* 事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。 + + + +### 127. 如何改变函数内部的 *this* 指针的指向 + +> 参考答案: +> +> 可以通过 *call、apply、bind* 方法来改变 *this* 的指向,关于 *call、apply、bind* 方法的具体使用,请参阅前面 *102* 题 + + + +### 128. *JS* 延迟加载的方式有哪些? + +> 参考答案: +> +> - *defer* 属性 +> - *async* 属性 +> - 使用 *jQuery* 的 *getScript*( ) 方法 +> - 使用 *setTimeout* 延迟方法 +> - 把 *JS* 外部引入的文件放到页面底部,来让 *JS* 最后引入 + + + +### 129. 说说严格模式的限制 + +> 参考答案: +> +> 什么是严格模式? +> +> 严格模式对 *JavaScript* 的语法和行为都做了一些更改,消除了语言中一些不合理、不确定、不安全之处;提供高效严谨的差错机制,保证代码安全运行;禁用在未来版本中可能使用的语法,为新版本做好铺垫。在脚本文件第一行或函数内第一行中引入"use strict"这条指令,就能触发严格模式,这是一条没有副作用的指令,老版的浏览器会将其作为一行字符串直接忽略。 +> +> 例如: +> +> ```js +> "use strict";//脚本第一行 +> function add(a,b){ +> "use strict";//函数内第一行 +> return a+b; +> } +> +> ``` +> +> 进入严格模式后的限制 +> +> - 变量必须声明后再赋值 +> - 不能有重复的参数名,函数的参数也不能有同名属性 +> - 不能使用*with*语句 +> - 不能对只读属性赋值 +> - 不能使用前缀 *0*表示八进制数 +> - 不能删除不可删除的属性 +> - *eval* 不会在它的外层作用域引入变量。 +> - *eval*和*arguments*不能被重新赋值 +> - *arguments* 不会自动反应函数的变化 +> - 不能使用 *arguments.callee* +> - 不能使用 *arguments.caller* +> - 禁止 *this* 指向全局对象 +> - 不能使用 *fn.caller* 和 *fn.arguments* 获取函数调用的堆栈 +> - 增加了保留字 + + + +### 130. *attribute* 和 *property* 的区别是什么? + +> 参考答案: +> +> property 和 attribute 非常容易混淆,两个单词的中文翻译也都非常相近(property:属性,attribute:特性),但实际上,二者是不同的东西,属于不同的范畴。 +> +> - property是DOM中的属性,是JavaScript里的对象; +> - attribute是HTML标签上的特性,它的值只能够是字符串; +> +> 简单理解,Attribute就是dom节点自带的属性,例如html中常用的id、class、title、align等。 +> +> 而Property是这个DOM元素作为对象,其附加的内容,例如childNodes、firstChild等。 + + + +### 131. *ES6* 能写 *class* 么,为什么会出现 *class* 这种东西? + +> 参考答案: +> +> 在 *ES6* 中,可以书写 *class*。因为在 *ES6* 规范中,引入了 *class* 的概念。使得 *JS* 开发者终于告别了直接使用原型对象模仿面向对象中的类和类继承时代。 +> +> 但是 *JS* 中并没有一个真正的 *class* 原始类型, *class* 仅仅只是对原型对象运用语法糖。 +> +> 之所以出现 *class* 关键字,是为了使 *JS* 更像面向对象,所以 *ES6* 才引入 *class* 的概念。 + + + +### 132. 常见兼容性问题 + +> 参考答案: +> +> 常见的兼容性问题很多,这里列举一些: +> +> 1. 关于获取行外样式 *currentStyle* 和 *getComputedStyle* 出现的兼容问题 +> +> 我们都知道 *JS* 通过 *style* 不可以获取行外样式,如果我们需要获取行外样式就会使用这两种 +> +> - IE 下:*currentStyle* +> +> - chrome、FF 下:*getComputedStyle* 第二个参数的作用是获取伪类元素的属性值 +> +> +> +> 2. 关于“索引”获取字符串每一项出现的兼容性的问题 +> +> 对于字符串也有类似于数组这样通过下标索引获取每一项的值 +> +> ```js +> var str = 'abcd'; +> console.log(str[2]); +> ``` +> +> 但是低版本的浏览器 *IE6、7* 不兼容 +> +> +> +> 3. 关于使用 *firstChild、lastChild* 等,获取第一个/最后一个元素节点是产生的问题 +> +> - IE6-8下: *firstChild,lastChild,nextSibling,previousSibling* 获取第一个元素节点 +> - 高版本浏览器IE9+、FF、Chrome:获取的空白文本节点 +> +> +> +> 4. 关于使用 *event* 对象,出现兼容性问题 +> +> 在 *IE8* 及之前的版本浏览器中,*event* 事件对象是作为 *window* 对象的一个属性。 +> +> 所以兼容的写法如下: +> +> ```js +> function(event){ +> event = event || window.event; +> } +> ``` +> +> +> +> 5. 关于事件绑定的兼容性问题 +> +> - *IE8* 以下用: attachEvent('事件名',fn); +> +> - *FF、Chrome、IE9-10* 用: attachEventLister('事件名',fn,false); +> +> +> +> 6. 关于获取滚动条距离而出现的问题 +> +> 当我们获取滚动条滚动距离时: +> +> - *IE、Chrome: document.body.scrollTop* +> +> - *FF: document.documentElement.scrollTop* +> +> 兼容处理: +> +> *var scrollTop = document.documentElement.scrollTop||document.body.scrollTop* + + + +### 133. 函数防抖节流的原理 + +> 参考答案: +> +> 请参阅前面第 *49、106* 题。 + + + +### 134. 原始类型有哪几种?*null* 是对象吗? + +> 参考答案: +> +> 在 *JavaScript* 中,数据类型整体上来讲可以分为两大类:**基本类型**和**引用数据类型** +> +> 基本数据类型,一共有 *6* 种: +> +> ```text +> string,symbol,number,boolean,undefined,null +> ``` +> +> 其中 *symbol* 类型是在 *ES6* 里面新添加的基本数据类型。 +> +> 引用数据类型,就只有 *1* 种: +> +> ```js +> object +> ``` +> +> 基本数据类型的值又被称之为原始值或简单值,而引用数据类型的值又被称之为复杂值或引用值。 +> +> 关于原始类型和引用类型的区别,可以参阅第 *26* 题。 +> +> *null* 表示空,但是当我们使用 *typeof* 来进行数据类型检测的时候,得到的值是 *object*。 +> +> 具体原因可以参阅前面第 *68* 题。 + + + +### 135. 为什么 *console.log(0.2+0.1==0.3) // false* + +> 参考答案: +> +> 因为浮点数的计算存在 *round-off* 问题,也就是浮点数不能够进行精确的计算。并且: +> +> - 不仅 *JavaScript*,所有遵循 *IEEE 754* 规范的语言都是如此; +> - 在 *JavaScript* 中,所有的 *Number* 都是以 *64-bit* 的双精度浮点数存储的; +> - 双精度的浮点数在这 *64* 位上划分为 *3* 段,而这 *3* 段也就确定了一个浮点数的值,*64bit* 的划分是“*1-11-52*”的模式,具体来说: +> - 就是 *1* 位最高位(最左边那一位)表示符号位,*0* 表示正,*1* 表示负; +> - *11* 位表示指数部分; +> - *52* 位表示尾数部分,也就是有效域部分 + + + +### 136. 说一下 *JS* 中类型转换的规则? + +> 参考答案: +> +> 类型转换可以分为两种,**隐性转换**和**显性转换**。 +> +> **1. 隐性转换** +> +> 当不同数据类型之间进行相互运算,或者当对非布尔类型的数据求布尔值的时候,会发生隐性转换。 +> +> 预期为数字的时候:算术运算的时候,我们的结果和运算的数都是数字,数据会转换为数字来进行计算。 +> +> | 类型 | 转换前 | 转换后 | +> | --------- | --------- | ------ | +> | number | 4 | 4 | +> | string | "1" | 1 | +> | string | "abc" | NaN | +> | string | "" | 0 | +> | boolean | true | 1 | +> | boolean | false | 0 | +> | undefined | undefined | NaN | +> | null | null | 0 | +> +> 预期为字符串的时候:如果有一个操作数为字符串时,使用`+`符号做相加运算时,会自动转换为字符串。 +> +> 预期为布尔的时候:前面在介绍布尔类型时所提到的 9 个值会转为 false,其余转为 true +> +> **2. 显性转换** +> +> 所谓显性转换,就是只程序员强制将一种类型转换为另外一种类型。显性转换往往会使用到一些转换方法。常见的转换方法如下: +> +> - 转换为数值类型:`Number()`,`parseInt()`,`parseFloat()` +> +> - 转换为布尔类型:`Boolean()` +> +> - 转换为字符串类型:`toString()`,`String()` +> +> 当然,除了使用上面的转换方法,我们也可以通过一些快捷方式来进行数据类型的显性转换,如下: +> +> - 转换字符串:直接和一个空字符串拼接,例如:`a = "" + 数据` +> +> - 转换布尔:!!数据类型,例如:`!!"Hello"` +> +> - 转换数值:数据*1 或 /1,例如:`"Hello * 1"` + + + +### 137. 深拷贝和浅拷贝的区别?如何实现 + +> 参考答案: +> +> - **浅拷贝**:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝(浅复制) +> +> 浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。 +> +> - **深拷贝**:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。 +> +> **浅拷贝方法** +> +> 1. 直接赋值 +> 2. *Object.assign* 方法:可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。当拷贝的 *object* 只有一层的时候,是深拷贝,但是当拷贝的对象属性值又是一个引用时,换句话说有多层时,就是一个浅拷贝。 +> 3. *ES6* 扩展运算符,当 *object* 只有一层的时候,也是深拷贝。有多层时是浅拷贝。 +> 4. *Array.prototype.concat* 方法 +> 5. *Array.prototype.slice* 方法 +> 6. *jQuery* 中的 *$.extend*:在 *jQuery* 中,*$.extend(deep,target,object1,objectN)* 方法可以进行深浅拷贝。*deep* 如过设为 *true* 为深拷贝,默认是 *false* 浅拷贝。 +> +> **深拷贝方法** +> +> 1. *$.extend(deep,target,object1,objectN)*,将 *deep* 设置为 *true* +> 2. *JSON.parse(JSON.stringify)*:用 *JSON.stringify* 将对象转成 *JSON* 字符串,再用 *JSON.parse* 方法把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。这种方法虽然可以实现数组或对象深拷贝,但不能处理函数。 +> 3. 手写递归 +> +> 示例代码如下: +> +> ```js +> function deepCopy(oldObj, newobj) { +> for (var key in oldObj) { +> var item = oldObj[key]; +> // 判断是否是对象 +> if (item instanceof Object) { +> if (item instanceof Function) { +> newobj[key] = oldObj[key]; +> } else { +> newobj[key] = {}; //定义一个空的对象来接收拷贝的内容 +> deepCopy(item, newobj[key]); //递归调用 +> } +> +> // 判断是否是数组 +> } else if (item instanceof Array) { +> newobj[key] = []; //定义一个空的数组来接收拷贝的内容 +> deepCopy(item, newobj[key]); //递归调用 +> } else { +> newobj[key] = oldObj[key]; +> } +> } +> } +> ``` + + + +### 138. 如何判断 *this*?箭头函数的 *this* 是什么 + +> 参考答案: +> +> 有关如何判断 *this*,可以参阅前面 17 题。 +> +> 有关箭头函数的 *this* 指向,可以参阅前面 *24、25* 题 + + + +### 139. *call、apply* 以及 *bind* 函数内部实现是怎么样的 + +> 参考答案: +> +> 请参阅前面 *102* 题。 + + + +### 140. 为什么会出现 *setTimeout* 倒计时误差?如何减少 + +> 参考答案: +> +> 定时器是属于宏任务(*macrotask*) 。如果当前执行栈所花费的时间大于定时器时间,那么定时器的回调在宏任务(*macrotask*) 里,来不及去调用,所有这个时间会有误差。 + + + +### 141. 谈谈你对 *JS* 执行上下文栈和作用域链的理解 + +> 参考答案: +> +> **什么是执行上下文?** +> +> 简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。 +> +> **执行上下文的类型** +> +> JavaScript 中有三种执行上下文类型。 +> +> - **全局执行上下文** — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 `this` 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。 +> - **函数执行上下文** — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。 +> - **Eval 函数执行上下文** — 执行在 `eval` 函数内部的代码也会有它属于自己的执行上下文。 +> +> **调用栈** +> +> 调用栈是解析器(如浏览器中的的javascript解析器)的一种机制,可以在脚本调用多个函数时,跟踪每个函数在完成执行时应该返回控制的点。(如什么函数正在执行,什么函数被这个函数调用,下一个调用的函数是谁) +> +> - 当脚本要调用一个函数时,解析器把该函数添加到栈中并且执行这个函数。 +> - 任何被这个函数调用的函数会进一步添加到调用栈中,并且运行到它们被上个程序调用的位置。 +> - 当函数运行结束后,解释器将它从堆栈中取出,并在主代码列表中继续执行代码。 +> - 如果栈占用的空间比分配给它的空间还大,那么则会导致“栈溢出”错误。 +> +> **作用域链** +> +> 当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止,,而作用域链,就是有当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域对符合访问权限的变量和函数的有序访问。 + + + +### 142. *new* 的原理是什么?通过 *new* 的方式创建对象和通过字面量创建有什么区别? + +> 参考答案: +> +> 关于 *new* 的原理,主要分为以下几步: +> +> - 创建一个空对象 。 +> +> - 由 *this* 变量引用该对象 。 +> +> - 该对象继承该函数的原型(更改原型链的指向) 。 +> +> - 把属性和方法加入到 *this* 引用的对象中。 +> +> - 新创建的对象由 *this* 引用 ,最后隐式地返回 *this*,过程如下: +> +> ```js +> var obj = {}; +> obj.__proto__ = Base.prototype; +> Base.call(obj); +> ``` +> +> 通过 *new* 的方式创建对象和通过字面量创建的对象,区别在于 *new* 出来的对象的原型对象为`构造函数.prototype`,而字面量对象的原型对象为 `Object.prototype` +> +> 示例代码如下: +> +> ```js +> function Computer() {} +> var c = new Computer(); +> var d = {}; +> console.log(c.__proto__ === Computer.prototype); // true +> console.log(d.__proto__ === Object.prototype); // true +> ``` + + + +### 143. *prototype* 和 \__*proto*__ 区别是什么? + +> 参考答案: +> +> *prototype* 是构造函数上面的一个属性,指向实例化出来对象的原型对象。 +> +> \__*proto*__ 是对象上面的一个隐式属性,指向自己的原型对象。 + + + +### 144. 使用 ES5 实现一个继承? + +> 参考答案: +> +> 请参阅第 *47* 题。 + + + +### 145. 取数组的最大值(*ES5、ES6*) + +> 参考答案: +> +> ```js +> var arr = [3, 5, 8, 1]; +> // ES5 方式 +> console.log(Math.max.apply(null, arr)); // 8 +> // ES6 方式 +> console.log(Math.max(...arr)); // 8 +> ``` + + + +### 146. *ES6* 新的特性有哪些? + +> 参考答案: +> +> 请参阅前面第 *44* 题。 + + + +### 147. *Promise* 有几种状态, *Promise* 有什么优缺点 ? + +> 参考答案: +> +> *Promise* 有三种状态: +> +> *pending、fulfilled、rejected*(未决定,履行,拒绝),同一时间只能存在一种状态,且状态一旦改变就不能再变。*Promise* 是一个构造函数,*promise* 对象代表一项有两种可能结果(成功或失败)的任务,它还持有多个回调,出现不同结果时分别发出相应回调。 +> +> - 初始化状态:*pending* +> - 当调用 *resolve*(成功) 状态:*pengding=>fulfilled* +> - 当调用 *reject*(失败) 状态:*pending=>rejected* +> +> *Promise* 的优点是解决了回调地狱,缺点是代码并没有因为新方法的出现而减少,反而变得更加复杂,同时理解难度也加大。所以后面出现了 *async/await* 的异步解决方案。 + + + +### 148. *Promise* 构造函数是同步还是异步执行,*then* 呢 ? *Promise* 如何实现 *then* 处理 ? + +> 参考答案: +> +> *promise* 构造函数是同步执行的,*then* 方法是异步执行,*then* 方法中的内容加入微任务中。 +> +> 接下来我们来看 *promise* 如何实现 *then* 的处理。 +> +> 我们知道 *then* 是用来处理 *resolve* 和 *reject* 函数的回调。那么首先我们来定义 *then* 方法。 +> +> ##### 1、then方法需要两个参数,其中onFulfilled代表resolve成功的回调,onRejected代表reject失败的回调。 +> +> ```js +> then(onFulfilled,onRejected){} +> ``` +> +> ##### 2、我们知道promise的状态是不可逆的,在状态发生改变后,即不可再次更改,只有状态为FULFILLED才会调用onFulfilled,状态为REJECTED调用onRejected +> +> ```js +> then(onFulfilled, onRejected){ +> if (this.status == Promise.FULFILLED) { +> onFulfilled(this.value) +> } +> if (this.status == Promise.REJECTED) { +> onRejected(this.value) +> } +> } +> ``` +> +> ##### 3、then方法的每个方法都不是必须的,所以我们要处理当没有传递参数时,应该设置默认值 +> +> ```js +> then(onFulfilled,onRejected){ +> if(typeof onFulfilled !=='function'){ +> onFulfilled = value => value; +> } +> if(typeof onRejected !=='function'){ +> onRejected = value => value; +> } +> if(this.status == Promise.FULFILLED){ +> onFulfilled(this.value) +> } +> if(this.status == Promise.REJECTED){ +> onRejected(this.value) +> } +> } +> ``` +> +> ##### 4、在执行then方法时,我们要考虑到传递的函数发生异常的情况,如果函数发生异常,我们应该让它进行错误异常处理,统一交给onRejected来处理错误 +> +> ```js +> then(onFulfilled,onRejected){ +> if(typeof onFulfilled !=='function'){ +> onFulfilled = value => value; +> } +> if(typeof onRejected !=='function'){ +> onRejected = value => value; +> } +> if(this.status == Promise.FULFILLED){ +> try{onFulfilled(this.value)}catch(error){ onRejected(error) } +> } +> if(this.status == Promise.REJECTED){ +> try{onRejected(this.value)}catch(error){ onRejected(error) } +> } +> } +> ``` +> +> ##### 5、但是现在我们自己封装的promise有个小问题,我们知道原生的promise中then方法都是异步执行,在一个同步任务执行之后再调用,而我们的现在的情况则是同步调用,因此我们要使用setTimeout来将onFulfilled和onRejected来做异步宏任务执行。 +> +> ```js +> if(this.status=Promise.FULFILLED){ +> setTimeout(()=>{ +> try{onFulfilled(this.value)}catch(error){onRejected(error)} +> }) +> } +> if(this.status=Promise.REJECTED){ +> setTimeout(()=>{ +> try{onRejected(this.value)}catch(error){onRejected(error)} +> }) +> } +> ``` +> +> ##### 现在then方法中,可以处理status为FULFILLED和REJECTED的情况,但是不能处理为pedding的情况,接下来进行几处修改。 +> +> ##### 6、在构造函数中,添加callbacks来保存pending状态时处理函数,当状态改变时循环调用 +> +> ```js +> constructor(executor) { +> ... +> this.callbacks = []; +> ... +> } +> ``` +> +> ##### 7、在then方法中,当status等于pending的情况时,将待执行函数存放到callbacks数组中。 +> +> ```js +> then(onFulfilled,onRejected){ +> ... +> if(this.status==Promise.PENDING){ +> this.callbacks.push({ +> onFulfilled:value=>{ +> try { +> onFulfilled(value); +> } catch (error) { +> onRejected(error); +> } +> } +> onRejected: value => { +> try { +> onRejected(value); +> } catch (error) { +> onRejected(error); +> } +> } +> }) +> } +> ... +> } +> ``` +> +> ##### 8、当执行resolve和reject时,在堆callacks数组中的函数进行执行 +> +> ```js +> resolve(vale){ +> if(this.status==Promise.PENDING){ +> this.status = Promise.FULFILLED; +> this.value = value; +> this.callbacks.map(callback => { +> callback.onFulfilled(value); +> }); +> } +> } +> reject(value){ +> if(this.status==Promise.PENDING){ +> this.status = Promise.REJECTED; +> this.value = value; +> this.callbacks.map(callback => { +> callback.onRejected(value); +> }); +> } +> } +> ``` +> +> ##### 9、then方法中,关于处理pending状态时,异步处理的方法:只需要将resolve与reject执行通过setTimeout定义为异步任务 +> +> ```js +> resolve(value) { +> if (this.status == Promise.PENDING) { +> this.status = Promise.FULFILLED; +> this.value = value; +> setTimeout(() => { +> this.callbacks.map(callback => { +> callback.onFulfilled(value); +> }); +> }); +> } +> } +> reject(value) { +> if (this.status == Promise.PENDING) { +> this.status = Promise.REJECTED; +> this.value = value; +> setTimeout(() => { +> this.callbacks.map(callback => { +> callback.onRejected(value); +> }); +> }); +> } +> } +> ``` +> +> 到此,promise的then方法的基本实现就结束了。 + + + +### 149. *Promise* 和 *setTimeout* 的区别 ? + +> 参考答案: +> +> *JavaScript* 将异步任务分为 *MacroTask*(宏任务) 和 *MicroTask*(微任务),那么它们区别何在呢? +> +> 1. 依次执行同步代码直至执行完毕; +> 2. 检查MacroTask 队列,若有触发的异步任务,则取第一个并调用其事件处理函数,然后跳至第三步,若没有需处理的异步任务,则直接跳至第三步; +> 3. 检查MicroTask队列,然后执行所有已触发的异步任务,依次执行事件处理函数,直至执行完毕,然后跳至第二步,若没有需处理的异步任务中,则直接返回第二步,依次执行后续步骤; +> 4. 最后返回第二步,继续检查MacroTask队列,依次执行后续步骤; +> 5. 如此往复,若所有异步任务处理完成,则结束; +> +> *Promise* 是一个微任务,主线程是一个宏任务,微任务队列会在宏任务后面执行 +> +> *setTimeout* 返回的函数是一个新的宏任务,被放入到宏任务队列 +> +> 所以 *Promise* 会先于新的宏任务执行 + + + +### 150. 如何实现 *Promise.all* ? + +> 参考答案: +> +> `Promise.all` 接收一个 `promise` 对象的数组作为参数,当这个数组里的所有 `promise` 对象全部变为`resolve`或 有 `reject` 状态出现的时候,它才会去调用 `.then` 方法,它们是并发执行的。 +> +> 总结 `promise.all` 的特点 +> +> 1、接收一个 `Promise` 实例的数组或具有 `Iterator` 接口的对象, +> +> 2、如果元素不是 `Promise` 对象,则使用 `Promise.resolve` 转成 `Promise` 对象 +> +> 3、如果全部成功,状态变为 `resolved`,返回值将组成一个数组传给回调 +> +> 4、只要有一个失败,状态就变为 `rejected`,返回值将直接传递给回调 +> `all()` 的返回值也是新的 `Promise` 对象 +> +> 实现 `Promise.all` 方法 +> +> ```js +> function promiseAll(promises) { +> return new Promise(function (resolve, reject) { +> if (!isArray(promises)) { +> return reject(new TypeError('arguments must be an array')); +> } +> var resolvedCounter = 0; +> var promiseNum = promises.length; +> var resolvedValues = new Array(promiseNum); +> for (var i = 0; i < promiseNum; i++) { +> (function (i) { +> Promise.resolve(promises[i]).then(function (value) { +> resolvedCounter++ +> resolvedValues[i] = value +> if (resolvedCounter == promiseNum) { +> return resolve(resolvedValues) +> } +> }, function (reason) { +> return reject(reason) +> }) +> })(i) +> } +> }) +> } +> ``` + + + +### 151. 如何实现 *Promise.finally* ? + +> 参考答案: +> +> *finally* 方法是 *ES2018* 的新特性 +> +> *finally* 方法用于指定不管 *Promise* 对象最后状态如何,都会执行的操作,执行 *then* 和 *catch* 后,都会执行 *finally* 指定的回调函数。 +> +> 方法一:借助 *promise.prototype.finally* 包 +> +> ```js +> npm install promise-prototype-finally +> ``` +> +> ```js +> const promiseFinally = require('promise.prototype.finally'); +> +> // 向 Promise.prototype 增加 finally() +> promiseFinally.shim(); +> +> // 之后就可以按照上面的使用方法使用了 +> ``` +> +> +> +> 方法二:实现 *Promise.finally* +> +> ```js +> Promise.prototype.finally = function (callback) { +> let P = this.constructor; +> return this.then( +> value => P.resolve(callback()).then(() => value), +> reason => P.resolve(callback()).then(() => { throw reason }) +> ); +> }; +> ``` +> +> + + + +### 152. 如何判断 *img* 加载完成 + +> 参考答案: +> +> - 为 *img DOM* 节点绑定 *load* 事件 +> - *readystatechange* 事件:*readyState* 为 *complete* 和 *loaded* 则表明图片已经加载完毕。测试 *IE6-IE10* 支持该事件,其它浏览器不支持。 +> - *img* 的 *complete* 属性:轮询不断监测 *img* 的 *complete* 属性,如果为 *true* 则表明图片已经加载完毕,停止轮询。该属性所有浏览器都支持。 + + + +### 153. 如何阻止冒泡? + +> 参考答案: +> +> ```js +> // 方法一:IE9+,其他主流浏览器 +> event.stopPropagation() +> // 方法二:火狐未实现 +> event.cancelBubble = true; +> // 方法三:不建议滥用,jq 中可以同时阻止冒泡和默认事件 +> return false; +> ``` + + + +### 154. 如何阻止默认事件? + +> 参考答案: +> +> ```js +> // 方法一:全支持 +> event.preventDefault(); +> // 方法二:该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。 +> event.returnValue=false; +> // 方法三:不建议滥用,jq 中可以同时阻止冒泡和默认事件 +> return false; +> ``` + + + +### 155. 如何用原生 *js* 给一个按钮绑定两个 *onclick* 事件? + +> 参考答案: +> +> 使用 *addEventListener* 方法来绑定事件,就可以绑定多个同种类型的事件。 + + + +### 156. 拖拽会用到哪些事件 + +> 参考答案: +> +> 在以前,书写一个拖拽需要用到 *mousedown、mousemove、mouseup* 这 *3* 个事件。 +> +> *HTML5* 推出后,新推出了一组拖拽相关的 *API*,涉及到的事件有 *dragstart、dragover、drop* 这 *3* 个事件。 + + + +### 157. *document.write* 和 *innerHTML* 的区别 + +> 参考答案: +> +> *document.write* 是直接写入到页面的内容流,如果在写之前没有调用 *document.open*, 浏览器会自动调用 *open*。每次写完关闭之后重新调用该函数,会导致页面全部重绘。 +> +> *innerHTML* 则是 *DOM* 页面元素的一个属性,代表该元素的 *html* 内容。你可以精确到某一个具体的元素来进行更改。如果想修改 *document* 的内容,则需要修改 *document.documentElement.innerElement*。 +> +> *innerHTML* 很多情况下都优于 *document.write*,其原因在于不会导致页面全部重绘。 + + + +### 158. *jQuery* 的事件委托方法 *bind 、live、delegate、one、on* 之间有什么区别? + +> 参考答案: +> +> 这几个方法都可以实现事件处理。其中 *on* 集成了事件处理的所有功能,也是目前推荐使用的方法。 +> +> *one* 是指添加的是一次性事件,意味着只要触发一次该事件,相应的处理方法执行后就自动被删除。 +> +> *bind* 是较早版本的绑定事件的方法,现在已被 *on* 替代。 +> +> *live* 和 *delegate* 主要用来做事件委托。*live* 的版本较早,现在已被废弃。*delegate* 目前仍然可用,不过也可用 *on* 来替代它。 + + + +### 159. *$(document).ready* 方法和 *window.onload* 有什么区别? + +> 参考答案: +> +> 主要有两点区别: +> +> 1. 执行时机 +> +> *window.onload* 方法是在网页中的所有的元素(包括元素的所有关联文件)都完全加载到浏览器之后才执行。而通过 *jQuery* 中的`$(document).ready`方法注册的事件处理程序,只要在 *DOM* 完全就绪时,就可以调用了,比如一张图片只要``标签完成,不用等这个图片加载完成,就可以设置图片的宽高的属性或样式等。 +> +> 其实从二者的英文字母可以大概理解上面的话,*onload* 即加载完成,*ready* 即 *DOM* 准备就绪。 +> +> 2. 注册事件  +> +> `$(document).ready`方法可以多次使用而注册不同的事件处理程序,而 *window.onload* 一次只能保存对一个函数的引用,多次绑定函数只会覆盖前面的函数。 + + + +### 160. jquery 中$.get()提交和$.post()提交有区别吗? + +> 参考答案: +> +> 相同点:都是异步请求的方式来获取服务端的数据 +> +> 不同点: +> +> - 请求方式不同:`$.get()` 方法使用 *GET* 方法来进行异步请求的。`$.post()` 方法使用 *POST* 方法来进行异步请求的。 +> - 参数传递方式不同: *GET* 请求会将参数跟在 *URL* 后进行传递,而 *POST* 请求则是作为 *HTTP* 消息的实体内容发送给 *Web* 服务器 的,这种传递是对用户不可见的。 +> - 数据传输大小不同: *GET* 方式传输的数据大小不能超过 *2KB* 而 *POST* 要大的多 +> - 安全问题: *GET* 方式请求的数据会被浏览器缓存起来,因此有安全问题。 + + + +### 161. *await async* 如何实现 (阿里) + +> 参考答案: +> +> async 函数只是 promise 的语法糖,它的底层实际使用的是 generator,而 generator 又是基于 promise 的。实际上,在 babel 编译 async 函数的时候,也会转化成 generatora 函数,并使用自动执行器来执行它。 +> +> 实现代码示例: +> +> ```js +> function asyncToGenerator(generatorFunc) { +> return function() { +> const gen = generatorFunc.apply(this, arguments) +> return new Promise((resolve, reject) => { +> function step(key, arg) { +> let generatorResult +> try { +> generatorResult = gen[key](arg) +> } catch (error) { +> return reject(error) +> } +> const { value, done } = generatorResult +> if (done) { +> return resolve(value) +> } else { +> return Promise.resolve(value).then(val => step('next', val), err => step('throw', err)) +> } +> } +> step("next") +> }) +> } +> } +> ``` +> +> 关于代码的解析,可以参阅:*https://blog.csdn.net/xgangzai/article/details/106536325* + + + +### 162. *clientWidth,offsetWidth,scrollWidth* 的区别 + +> 参考答案: +> +> *clientWidth* = *width*+左右 *padding* +> +> *offsetWidth* = *width* + 左右 *padding* + 左右 *boder* +> +> *scrollWidth*:获取指定标签内容层的真实宽度(可视区域宽度+被隐藏区域宽度)。 + + + +### 163. 产生一个不重复的随机数组 + +> 参考答案: +> +> 示例代码如下: +> +> ```js +> // 生成随机数 +> function randomNumBoth(Min, Max) { +> var Range = Max - Min; +> var Rand = Math.random(); +> var num = Min + Math.round(Rand * Range); //四舍五入 +> return num; +> } +> // 生成数组 +> function randomArr(len, min, max) { +> if ((max - min) < len) { //可生成数的范围小于数组长度 +> return null; +> } +> var hash = []; +> +> while (hash.length < len) { +> var num = randomNumBoth(min, max); +> +> if (hash.indexOf(num) == -1) { +> hash.push(num); +> } +> } +> return hash; +> } +> // 测试 +> console.log(randomArr(10, 1, 100)); +> ``` +> +> 在上面的代码中,我们封装了一个 *randomArr* 方法来生成这个不重复的随机数组,该方法接收三个参数,*len、min* 和 *max*,分别表示数组的长度、最小值和最大值。*randomNumBoth* 方法用来生成随机数。 + + + +### 164. *continue* 和 *break* 的区别 + +> 参考答案: +> +> - *break*:用于永久终止循环。即不执行本次循环中 *break* 后面的语句,直接跳出循环。 +> - *continue*:用于终止本次循环。即本次循环中 *continue* 后面的代码不执行,进行下一次循环的入口判断。 + + + +### 165. 如何在 *jquery* 上扩展插件,以及内部原理(腾讯) + +> 参考答案: +> +> 通过 *$.extend(object);* 为整个 *jQuery* 类添加新的方法。 +> +> 例如: +> +> ```js +> $.extend({ +> sayHello: function(name) { +> console.log('Hello,' + (name ? name : 'World') + '!'); +> }, +> showAge(){ +> console.log(18); +> } +> }) +> +> // 外部使用 +> $.sayHello(); // Hello,World! 无参调用 +> $.sayHello('zhangsan'); // Hello,zhangsan! 带参调用 +> ``` +> +> 通过 *$.fn.extend(object);* 给 *jQuery* 对象添加方法。 +> +> 例如: +> +> ```js +> $.fn.extend({ +> swiper: function (options) { +> var obj = new Swiper(options, this); // 实例化 Swiper 对象 +> obj.init(); // 调用对象的 init 方法 +> } +> }) +> +> // 外部使用 +> $('#id').swiper(); +> ``` +> +> ***extend* 方法内部原理** +> +> ```js +> jQuery.extend( target [, object1 ] [, objectN ] ) +> ``` +> +> 对后一个参数进行循环,然后把后面参数上所有的字段都给了第一个字段,若第一个参数里有相同的字段,则进行覆盖操作,否则就添加一个新的字段。 +> +> 解析如下: +> +> ```js +> // 为与源码的下标对应上,我们把第一个参数称为第0个参数,依次类推 +> jQuery.extend = jQuery.fn.extend = function() { +> var options, name, src, copy, copyIsArray, clone, +> target = arguments[0] || {}, // 默认第0个参数为目标参数 +> i = 1, // i表示从第几个参数凯斯想目标参数进行合并,默认从第1个参数开始向第0个参数进行合并 +> length = arguments.length, +> deep = false; // 默认为浅度拷贝 +> +> // 判断第0个参数的类型,若第0个参数是boolean类型,则获取其为true还是false +> // 同时将第1个参数作为目标参数,i从当前目标参数的下一个 +> // Handle a deep copy situation +> if ( typeof target === "boolean" ) { +> deep = target; +> +> // Skip the boolean and the target +> target = arguments[ i ] || {}; +> i++; +> } +> +> // 判断目标参数的类型,若目标参数既不是object类型,也不是function类型,则为目标参数重新赋值 +> // Handle case when target is a string or something (possible in deep copy) +> if ( typeof target !== "object" && !jQuery.isFunction(target) ) { +> target = {}; +> } +> +> // 若目标参数后面没有参数了,如$.extend({_name:'wenzi'}), $.extend(true, {_name:'wenzi'}) +> // 则目标参数即为jQuery本身,而target表示的参数不再为目标参数 +> // Extend jQuery itself if only one argument is passed +> if ( i === length ) { +> target = this; +> i--; +> } +> +> // 从第i个参数开始 +> for ( ; i < length; i++ ) { +> // 获取第i个参数,且该参数不为null, +> // 比如$.extend(target, {}, null);中的第2个参数null是不参与合并的 +> // Only deal with non-null/undefined values +> if ( (options = arguments[ i ]) != null ) { +> +> // 使用for~in获取该参数中所有的字段 +> // Extend the base object +> for ( name in options ) { +> src = target[ name ]; // 目标参数中name字段的值 +> copy = options[ name ]; // 当前参数中name字段的值 +> +> // 若参数中字段的值就是目标参数,停止赋值,进行下一个字段的赋值 +> // 这是为了防止无限的循环嵌套,我们把这个称为,在下面进行比较详细的讲解 +> // Prevent never-ending loop +> if ( target === copy ) { +> continue; +> } +> +> // 若deep为true,且当前参数中name字段的值存在且为object类型或Array类型,则进行深度赋值 +> // Recurse if we're merging plain objects or arrays +> if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { +> // 若当前参数中name字段的值为Array类型 +> // 判断目标参数中name字段的值是否存在,若存在则使用原来的,否则进行初始化 +> if ( copyIsArray ) { +> copyIsArray = false; +> clone = src && jQuery.isArray(src) ? src : []; +> +> } else { +> // 若原对象存在,则直接进行使用,而不是创建 +> clone = src && jQuery.isPlainObject(src) ? src : {}; +> } +> +> // 递归处理,此处为2.2 +> // Never move original objects, clone them +> target[ name ] = jQuery.extend( deep, clone, copy ); +> +> // deep为false,则表示浅度拷贝,直接进行赋值 +> // 若copy是简单的类型且存在值,则直接进行赋值 +> // Don't bring in undefined values +> } else if ( copy !== undefined ) { +> // 若原对象存在name属性,则直接覆盖掉;若不存在,则创建新的属性 +> target[ name ] = copy; +> } +> } +> } +> } +> +> // 返回修改后的目标参数 +> // Return the modified object +> return target; +> }; +> ``` + + + +### 166. *async/await* 如何捕获错误 + +> 参考答案: +> +> 可以使用 *try...catch* 来进行错误的捕获 +> +> 示例代码: +> +> ```js +> async function test() { +> try { +> const res = await test1() +> } catch (err) { +> console.log(err) +> } +> console.log("test") +> } +> ``` + + + +### 167. *Proxy* 对比 *Object.defineProperty* 的优势 + +> 参考答案: +> +> **Proxy 的优势如下:** +> +> - *Object.defineProperty* 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,而 *Proxy* 可以直接监听对象而非属性; +> - *Object.defineProperty* 无法监控到数组下标的变化,而 *Proxy* 可以直接监听数组的变化; +> - *Proxy* 有多达 *13* 种拦截方法; +> - *Proxy* 作为新标准将受到浏览器厂商重点持续的性能优化; + + + +### 168. 原型链,可以改变原型链的规则吗? + +> 参考答案: +> +> 每个对象都可以有一个原型\__*proto*__,这个原型还可以有它自己的原型,以此类推,形成一个原型链。查找特定属性的时候,我们先去这个对象里去找,如果没有的话就去它的原型对象里面去,如果还是没有的话再去向原型对象的原型对象里去寻找。这个操作被委托在整个原型链上,这个就是我们说的原型链。 +> +> 我们可以通过手动赋值的方式来改变原型链所对应的原型对象。 + + + +### 169. 讲一讲继承的所有方式都有什么?手写一个寄生组合式继承 + +> 参考答案: +> +> 可以参阅前面第 *9、18、47* 题答案。 +> +> 其中圣杯模式就是寄生组合式继承。 + + + +### 170. *JS* 基本数据类型有哪些?栈和堆有什么区别,为什么要这样存储。(快手) + +> 参考答案: +> +> 关于 *JS* 基本数据类型有哪些这个问题,可以参阅前面 *26* 题。 +> +> 栈和堆的区别在于堆是动态分配内存,内存大小不一,也不会自动释放。栈是自动分配相对固定大小的内存空间,并由系统自动释放。 +> +> 在 *js* 中,基本数据都是直接按值存储在栈中的,每种类型的数据占用的内存空间的大小是确定的,并由系统自动分配和自动释放。这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间。 +> +> *js* 中其他类型的数据被称为引用类型的数据(如对象、数组、函数等),它们是通过拷贝和 *new* 出来的,这样的数据存储于堆中。其实,说存储于堆中,也不太准确,因为,引用类型的数据的地址指针是存储于栈中的,当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据。 + + + +### 171. *setTimeout(() => {}, 0)* 什么时候执行 + +> 参考答案: +> +> 因为 *setTimeout* 是异步代码,所以即使后面的时间为 *0*,也要等到同步代码执行完毕后才会执行。 + + + +### 172. *js* 有函数重载吗(网易) + +> 参考答案: +> +> 所谓函数重载,是方法名称进行重用的一种技术形式,其主要特点是“方法名相同,参数的类型或个数不相同”,在调用时会根据传递的参数类型和个数的不同来执行不同的方法体。 +> +> 在 *JS* 中,可以通过在函数内容判断形参的类型或个数来执行不同的代码块,从而达到模拟函数重载的效果。 + + + +### 173. 给你一个数组,计算每个数出现的次数,如果每个数组返回的数都是独一无二的就返回 *true* 相反则返回的 *flase* + +> 参考答案: +> +> 输入:arr = [1,2,2,1,1,3] +> +> 输出:true +> +> 解释:在该数组中,1 出现了 3 次,2 出现了 2 次,3 只出现了 1 次。没有两个数的出现次数相同。 +> +> 代码示例: +> +> ```js +> function uniqueOccurrences(arr) { +> let uniqueArr = [...new Set(arr)] +> let countArr = [] +> for (let i = 0; i < uniqueArr.length; i++) { +> countArr.push(arr.filter(item => item == uniqueArr[i]).length) +> } +> return countArr.length == new Set(countArr).size +> }; +> +> // 测试 +> console.log(uniqueOccurrences([1, 2, 2, 1, 1, 3])); // true +> console.log(uniqueOccurrences([1, 2, 2, 1, 1, 3, 2])); // false +> ``` + + + +### 174. 封装一个能够统计重复的字符的函数,例如 *aaabbbdddddfff* 转化为 *3a3b5d3f* + +> 参考答案: +> +> ```js +> function compression(str) { +> if (str.length == 0) { +> return 0; +> } +> var len = str.length; +> var str2 = ""; +> var i = 0; +> var num = 1; +> while (i < len) { +> if (str.charAt(i) == str.charAt(i + 1)) { +> num++; +> } else { +> str2 += num; +> str2 += str.charAt(i); +> num = 1; +> } +> i++; +> } +> return str2; +> } +> // 测试: +> console.log(compression('aaabbbdddddfff')); // 3a3b5d3f +> ``` + + + +### 175. 写出代码的执行结果,并解释为什么? + +```js +function a() { + console.log(1); +} +(function() { + if (false) { + function a() { + console.log(2); + } + } + console.log(typeof a); + a(); +})() +``` + +> 参考答案: +> +> 会报错,*a is not a function*。 +> +> 因为立即执行函数里面有函数 *a*,*a* 会被提升到该函数作用域的最顶端,但是由于判断条件是 *false*,所以不会进入到条件语句里面, *a* 也就没有值。所以 *typeof* 打印出来是 *undefined*。而后面在尝试调用方法,自然就会报错。 + + + +### 176. 写出代码的执行结果,并解释为什么? + +```js +alert(a); +a(); +var a = 3; +function a() { + alert(10); +}; +alert(a); +a = 6; +a(); +``` + +> 参考答案: +> +> 首先打印 function a() {alert(10);}; +> +> 然后打印 10 +> +> 最后打印 3 + +> 解析: +> +> 首先 a 变量会被提升到该全局作用域的最顶端,然后值为对应的函数,所以第一次打印出来的是函数。 +> +> 接下来调用这个 a 函数,所以打印出 10 +> +> 最后给这个 a 赋值为 3,然后又 alert,所以打印出 3。 +> +> 之后 a 的值还会发生改变,但是由于没有 alert,说明不会再打印出其他值了。 + + + +### 177. 写出下面程序的打印顺序,并简要说明原因 + +```js +setTimeout(function () { + console.log("set1"); + new Promise(function (resolve) { + resolve(); + }).then(function () { + new Promise(function (resolve) { + resolve(); + }).then(function () { + console.log("then4"); + }) + console.log('then2'); + }) +}); +new Promise(function (resolve) { + console.log('pr1'); + resolve(); +}).then(function () { + console.log('then1'); +}); + +setTimeout(function () { + console.log("set2"); +}); +console.log(2); + +new Promise(function (resolve) { + resolve(); +}).then(function () { + console.log('then3'); +}) +``` + +> 参考答案: +> +> 打印结果为: +> +> pr1 +> 2 +> then1 +> then3 +> set1 +> then2 +> then4 +> set2 + + + +### 178. *javascript* 中什么是伪数组?如何将伪数组转换为标准数组 + +> 参考答案: +> +> 在 *JavaScript* 中,*arguments* 就是一个伪数组对象。关于 *arguments* 具体可以参阅后面 *250* 题。 +> +> 可以使用 *ES6* 的扩展运算符来将伪数组转换为标准数组 +> +> 例如: +> +> ```js +> var arr = [...arguments]; +> ``` + + + +### 179. *array* 和 *object* 的区别 + +> 参考答案: +> +> 数组表示有序数据的集合,对象表示无序数据的集合。如果数据顺序很重要的话,就用数组,否则就用对象。 + + + +### 180. *jquery* 事件委托 + +> 参考答案: +> +> 在 *jquery* 中使用 *on* 来绑定事件的时候,传入第二个参数即可。例如: +> +> ```js +> $("ul").on("click","li",function () { +> alert(1); +> }) +> ``` + + + +### 181. *JS* 基本数据类型 + +> 参考答案: +> +> 请参阅前面第 *26* 题 + + + +### 182. 请实现一个模块 *math*,支持链式调用`math.add(2,4).minus(3).times(2);` + +> 参考答案: +> +> 示例代码: +> +> ```js +> class Math { +> constructor(value) { +> let hasInitValue = true; +> if (value === undefined) { +> value = NaN; +> hasInitValue = false; +> } +> Object.defineProperties(this, { +> value: { +> enumerable: true, +> value: value, +> }, +> hasInitValue: { +> enumerable: false, +> value: hasInitValue, +> }, +> }); +> } +> +> add(...args) { +> const init = this.hasInitValue ? this.value : args.shift(); +> const value = args.reduce((pv, cv) => pv + cv, init); +> return new Math(value); +> } +> +> minus(...args) { +> const init = this.hasInitValue ? this.value : args.shift(); +> const value = args.reduce((pv, cv) => pv - cv, init); +> return new Math(value); +> } +> +> times(...args) { +> const init = this.hasInitValue ? this.value : args.shift(); +> const value = args.reduce((pv, cv) => pv * cv, init); +> return new Math(value); +> } +> +> divide(...args) { +> const init = this.hasInitValue ? this.value : args.shift(); +> const value = args.reduce((pv, cv) => pv / cv, init); +> return new Math(value); +> } +> +> toJSON() { +> return this.valueOf(); +> } +> +> toString() { +> return String(this.valueOf()); +> } +> +> valueOf() { +> return this.value; +> } +> +> [Symbol.toPrimitive](hint) { +> const value = this.value; +> if (hint === 'string') { +> return String(value); +> } else { +> return value; +> } +> } +> } +> +> export default new Math(); +> ``` + + + +### 183. 请简述 *ES6* 代码转成 *ES5* 代码的实现思路。 + +> 参考答案: +> +> 说到 *ES6* 代码转成 *ES5* 代码,我们肯定会想到 *Babel*。所以,我们可以参考 *Babel* 的实现方式。 +> +> 那么 *Babel* 是如何把 *ES6* 转成 *ES5* 呢,其大致分为三步: +> +> - 将代码字符串解析成抽象语法树,即所谓的 *AST* +> - 对 *AST* 进行处理,在这个阶段可以对 *ES6* 代码进行相应转换,即转成 *ES5* 代码 +> - 根据处理后的 *AST* 再生成代码字符串 + + + +### 184. 下列代码的执行结果 + +```js +async function async1() { + console.log('async1 start'); + await async2(); + console.log('async1 end'); +} +async function async2() { + console.log('async2'); +} +console.log('script start'); +setTimeout(function () { + console.log('setTimeout'); +}, 0); +async1(); +new Promise(function (resolve) { + console.log('promise1'); + resolve(); +}).then(function () { + console.log('promise2'); +}); +console.log('script end'); +``` + +> 参考答案: +> +> script start +> async1 start +> async2 +> promise1 +> script end +> async1 end +> promise2 +> setTimeout + +> 解析: +> +> 在此之前我们需要知道以下几点: +> +> - setTimeout 属于宏任务 +> - Promise 本身是同步的立即执行函数,Promise.then 属于微任务 +> - async 方法执行时,遇到 await 会立即执行表达式,表达式之后的代码放到微任务执行 +> +> **第一次执行**:执行同步代码 +> +> ```js +> Tasks(宏任务):run script、 setTimeout callback +> Microtasks(微任务):await、Promise then +> JS stack(执行栈): script +> Log: script start、async1 start、async2、promise1、script end +> ``` +> +> **第二次执行**:执行宏任务后,检测到**微任务**队列中不为空、一次性执行完所有微任务 +> +> ```js +> Tasks(宏任务):run script、 setTimeout callback +> Microtasks(微任务):Promise then +> JS stack(执行栈): await +> Log: script start、async1 start、async2、promise1、script end、async1 end、promise2 +> ``` +> +> **第三次执行**:当**微任务**队列中为空时,执行**宏任务**,执行`setTimeout callback`,打印日志。 +> +> ```js +> Tasks(宏任务):null +> Microtasks(微任务):null +> JS stack(执行栈):setTimeout callback +> Log: script start、async1 start、async2、promise1、script end、async1 end、promise2、setTimeout +> ``` + + + +### 185. *JS* 有哪些内置对象? + +> 参考答案: +> +> 数据封装类对象:*String,Boolean,Number,Array* 和 *Object* +> +> 其他对象:*Function,Arguments,Math,Date,RegExp,Error* + + + +### 186. *DOM* 怎样添加、移除、移动、复制、创建和查找节点 + +> 参考答案: +> +> 请参阅前面 *121* 题。 + + + +### 187. *eval* 是做什么的? + +> 参考答案: +> +> 此函数可以接受一个字符串 *str* 作为参数,并把此 *str* 当做一段 *javascript* 代码去执行,如果 *str* 执行结果是一个值则返回此值,否则返回 *undefined*。如果参数不是一个字符串,则直接返回该参数。 +> +> 例如: +> +> ```js +> eval("var a=1");//声明一个变量a并赋值1。 +> eval("2+3");//5执行加运算,并返回运算值。 +> eval("mytest()");//执行mytest()函数。 +> eval("{b:2}");//声明一个对象。 +> ``` + + + +### 188. *null* 和 *undefined* 的区别? + +> 参考答案: +> +> 请参阅前面第 *29* 题。 + + + +### 189. *new* 操作符具体干了什么呢? + +> 参考答案: +> +> - 创建一个空对象 。 +> - 由 this 变量引用该对象 。 +> - 该对象继承该函数的原型(更改原型链的指向) 。 +> - 把属性和方法加入到 this 引用的对象中。 +> - 新创建的对象由 this 引用 ,最后隐式地返回 this,过程如下: +> +> ```js +> var obj = {}; +> obj.__proto__ = Base.prototype; +> Base.call(obj); +> ``` + + + +### 190. 去除字符串中的空格 + +> 参考答案: +> +> 方法一:*replace*正则匹配方法 +> +> 代码示例: +> +> - 去除字符串内所有的空格:`str = str.replace(/\s*/g,"");` +> - 去除字符串内两头的空格:`str = str.replace(/^\s*|\s*$/g,"");` +> - 去除字符串内左侧的空格:`str = str.replace(/^\s*/,"");` +> - 去除字符串内右侧的空格:`str = str.replace(/(\s*$)/g,"");` +> +> 方法二:字符串原生 *trim* 方法 +> +> *trim* 方法能够去掉两侧空格返回新的字符串,不能去掉中间的空格 + + + +### 191. 常见的内存泄露,以及解决方案 + +> 参考答案: +> +> **内存泄露概念** +> +> 内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。 +> +> 内存泄漏通常情况下只能由获得程序源代码和程序员才能分析出来。然而,有不少人习惯于把任何不需要的内存使用的增加描述为内存泄漏,即使严格意义上来说这是不准确的。 +> +> ***JS* 垃圾收集机制** +> +> *JS* 具有自动回收垃圾的机制,即执行环境会负责管理程序执行中使用的内存。在C和C++等其他语言中,开发者的需要手动跟踪管理内存的使用情况。在编写 *JS* 代码的时候,开发人员不用再关心内存使用的问题,所需内存的分配 以及无用的回收完全实现了自动管理。 +> +> Js中最常用的垃圾收集方式是标记清除(mark-and-sweep)。当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占的内存,因为只要执行流进入相应的环境,就可能用到它们。而当变量离开环境时,这将其 标记为“离开环境”。 +> +> **常见内存泄漏以及解决方案** +> +> 1. 意外的全局变量 +> +> Js处理未定义变量的方式比较宽松:未定义的变量会在全局对象创建一个新变量。在浏览器中,全局对象是window。 +> +> ```js +> function foo(arg) { +> bar = "this is a hidden global variable"; //等同于window.bar="this is a hidden global variable" +> this.bar2= "potential accidental global";//这里的this 指向了全局对象(window),等同于window.bar2="potential accidental global" +> } +> ``` +> +> 解决方法:在 JavaScript 程序中添加,开启严格模式'use strict',可以有效地避免上述问题。 +> +> 注意:那些用来临时存储大量数据的全局变量,确保在处理完这些数据后将其设置为null或重新赋值。与全局变量相关的增加内存消耗的一个主因是缓存。缓存数据是为了重用,缓存必须有一个大小上限才有用。高内存消耗导致缓存突破上限,因为缓 存内容无法被回收。 +> +> +> +> 2. 循环引用 +> +> 在js的内存管理环境中,对象 A 如果有访问对象 B 的权限,叫做对象 A 引用对象 B。引用计数的策略是将“对象是否不再需要”简化成“对象有没有其他对象引用到它”,如果没有对象引用这个对象,那么这个对象将会被回收 。 +> +> ```js +> let obj1 = { a: 1 }; // 一个对象(称之为 A)被创建,赋值给 obj1,A 的引用个数为 1 +> let obj2 = obj1; // A 的引用个数变为 2 +> +> obj1 = 0; // A 的引用个数变为 1 +> obj2 = 0; // A 的引用个数变为 0,此时对象 A 就可以被垃圾回收了 +> ``` +> +> 但是引用计数有个最大的问题: 循环引用。 +> +> ```js +> function func() { +> let obj1 = {}; +> let obj2 = {}; +> +> obj1.a = obj2; // obj1 引用 obj2 +> obj2.a = obj1; // obj2 引用 obj1 +> +> } +> ``` +> +> 当函数 func 执行结束后,返回值为 undefined,所以整个函数以及内部的变量都应该被回收,但根据引用计数方法,obj1 和 obj2 的引用次数都不为 0,所以他们不会被回收。要解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。上面的例子可以这么做: +> +> ```js +> obj1 = null; +> obj2 = null; +> ``` +> +> +> +> 3. 被遗忘的计时器和回调函数 +> +> ```js +> let someResource = getData(); +> setInterval(() => { +> const node = document.getElementById('Node'); +> if(node) { +> node.innerhtml = JSON.stringify(someResource)); +> } +> }, 1000); +> ``` +> +> 上面的例子中,我们每隔一秒就将得到的数据放入到文档节点中去。 +> +> 但在 *setInterval* 没有结束前,回调函数里的变量以及回调函数本身都无法被回收。那什么才叫结束呢? +> +> 就是调用了 *clearInterval*。如果回调函数内没有做什么事情,并且也没有被 *clear* 掉的话,就会造成内存泄漏。 +> +> 不仅如此,如果回调函数没有被回收,那么回调函数内依赖的变量也没法被回收。上面的例子中,*someResource* 就没法被回收。同样的,*setTiemout* 也会有同样的问题。所以,当不需要 *interval* 或者 *timeout* 时,最好调用 *clearInterval* 或者 *clearTimeout*。 +> +> +> +> 4. *DOM* 泄漏 +> +> 在 *JS* 中对*DOM*操作是非常耗时的。因为*JavaScript/ECMAScript*引擎独立于渲染引擎,而*DOM*是位于渲染引擎,相互访问需要消耗一定的资源。 而 *IE* 的 *DOM* 回收机制便是采用引用计数的,以下主要针对 *IE* 而言的。 +> +> **a. 没有清理的 DOM 元素引用** +> +> ```js +> var refA = document.getElementById('refA'); +> document.body.removeChild(refA); +> // refA 不能回收,因为存在变量 refA 对它的引用。将其对 refA 引用释放,但还是无法回收 refA。 +> ``` +> +> 解决办法:*refA = null;* +> +> **b. 给 DOM 对象添加的属性是一个对象的引用** +> +> ```js +> var MyObject = {}; +> document.getElementById('mydiv').myProp = MyObject; +> ``` +> +> 解决方法: +> 在 *window.onunload* 事件中写上: *document.getElementById('mydiv').myProp = null;* +> +> **c. DOM 对象与 JS 对象相互引用** +> +> ```js +> function Encapsulator(element) { +> this.elementReference = element; +> element.myProp = this; +> } +> new Encapsulator(document.getElementById('myDiv')); +> ``` +> +> 解决方法: 在 onunload 事件中写上: document.getElementById('myDiv').myProp = null; +> +> **d. 给 DOM 对象用 attachEvent 绑定事件** +> +> ```js +> function doClick() {} +> element.attachEvent("onclick", doClick); +> ``` +> +> 解决方法: 在onunload事件中写上: element.detachEvent('onclick', doClick); +> +> **e. 从外到内执行 appendChild。这时即使调用 removeChild 也无法释放** +> +> ```js +> var parentDiv = document.createElement("div"); +> var childDiv = document.createElement("div"); +> document.body.appendChild(parentDiv); +> parentDiv.appendChild(childDiv); +> ``` +> +> 解决方法: 从内到外执行 appendChild: +> +> ```js +> var parentDiv = document.createElement("div"); +> var childDiv = document.createElement("div"); +> parentDiv.appendChild(childDiv); +> document.body.appendChild(parentDiv); +> ``` +> +> +> +> 5. *JS* 的闭包 +> +> 闭包在 *IE6* 下会造成内存泄漏,但是现在已经无须考虑了。值得注意的是闭包本身不会造成内存泄漏,但闭包过多很容易导致内存泄漏。闭包会造成对象引用的生命周期脱离当前函数的上下文,如果闭包如果使用不当,可以导致环形引用(*circular reference*),类似于死锁,只能避免,无法发生之后解决,即使有垃圾回收也还是会内存泄露。 +> +> +> +> 6. *console* +> +> 控制台日志记录对总体内存配置文件的影响可能是许多开发人员都未想到的极其重大的问题。记录错误的对象可以将大量数据保留在内存中。注意,这也适用于: +> +> (1) 在用户键入 JavaScript 时,在控制台中的一个交互式会话期间记录的对象。 +> (2) 由 console.log 和 console.dir 方法记录的对象。 + + + +### 192. 箭头函数和普通函数里面的 *this* 有什么区别 + +> 参考答案: +> +> 请参阅前面第 *24、25* 题 + + + +### 193. 设计⼀个⽅法(*isPalindrom*)以判断是否回⽂(颠倒后的字符串和原来的字符串⼀样为回⽂) + +> 参考答案: +> +> 示例代码如下: +> +> ```js +> function isPalindrome(str) { +> if (typeof str !== 'string') { +> return false +> } +> return str.split('').reverse().join('') === str +> } +> +> // 测试 +> console.log(isPalindrome('HelleH')); // true +> console.log(isPalindrome('Hello')); // false +> ``` + + + +### 194. 设计⼀个⽅法(*findMaxDuplicateChar*)以统计字符串中出现最多次数的字符 + +> 参考答案: +> +> 示例代码如下: +> +> ```js +> function findMaxDuplicateChar(str) { +> let cnt = {}, //用来记录所有的字符的出现频次 +> c = ''; //用来记录最大频次的字符 +> for (let i = 0; i < str.length; i++) { +> let ci = str[i]; +> if (!cnt[ci]) { +> cnt[ci] = 1; +> } else { +> cnt[ci]++; +> } +> if (c == '' || cnt[ci] > cnt[c]) { +> c = ci; +> } +> } +> console.log(cnt); // { H: 1, e: 1, l: 3, o: 2, ' ': 1, W: 1, r: 1, d: 1 } +> return c; +> } +> +> // 测试 +> console.log(findMaxDuplicateChar('Hello World')); // l +> ``` + + + +### 195. 设计⼀段代码,使得通过点击按钮可以在 *span* 中显示⽂本框中输⼊的值 + +> 参考答案: +> +> 示例代码如下: +> +> ```html +> +> 在右侧输入框中输入内容 +> +> +> +> +> ``` + + + +### 196. *map* 和 *forEach* 的区别? + +> 参考答案: +> +> **两者区别** +> +> `forEach()`方法不会返回执行结果,而是`undefined`。 +> +> 也就是说,`forEach()`会修改原来的数组。而`map()`方法会得到一个新的数组并返回。 +> +> **适用场景** +> +> `forEach`适合于你并不打算改变数据的时候,而只是想用数据做一些事情 – 比如存入数据库或则打印出来。 +> +> `map()`适用于你要改变数据值的时候。不仅仅在于它更快,而且返回一个新的数组。这样的优点在于你可以使用复合(*composition*)(*map, filter, reduce* 等组合使用)来玩出更多的花样。 + + + +### 197. *Array* 的常用方法 + +> 参考答案: +> +> *Array* 的常用方法很多,挑选几个自己在实际开发中用的比较多的方法回答即可。 +> +> image-20210817231325109 +> +> 更多 *Array* 相关用法可以参阅:*https://www.w3school.com.cn/jsref/jsref_obj_array.asp* + + + +### 198. 数组去重的多种实现方式 + +> 参考答案: +> +> 请参阅前面第 *2* 题答案。 + + + +### 199. 什么是预解析(预编译) + +> 参考答案: +> +> 所谓的预解析(预编译)就是:在当前作用域中,*JavaScript* 代码执行之前,浏览器首先会默认的把所有带 *var* 和 *function* 声明的变量进行提前的声明或者定义。 +> +> 另外,*var* 声明的变量和 *function* 声明的函数在预解析的时候有区别,*var* 声明的变量在预解析的时候只是提前的声明,*function* 声明的函数在预解析的时候会提前声明并且会同时定义。也就是说 *var* 声明的变量和 *function* 声明的函数的区别是在声明的同时有没有同时进行定义。 + + + +### 200. 原始值类型和引用值类型的区别是什么? + +> 参考答案: +> +> 可以参阅前面第 *26* 题 + + + +### 201. 冒泡排序的思路,不用 *sort* + +> 参考答案: +> +> 示例代码如下: +> +> ```js +> var examplearr = [8, 94, 15, 88, 55, 76, 21, 39]; +> function sortarr(arr) { +> for (i = 0; i < arr.length - 1; i++) { +> for (j = 0; j < arr.length - 1 - i; j++) { +> if (arr[j] > arr[j + 1]) { +> var temp = arr[j]; +> arr[j] = arr[j + 1]; +> arr[j + 1] = temp; +> } +> } +> } +> return arr; +> } +> sortarr(examplearr); +> console.log(examplearr); // [8, 15, 21, 39, 55, 76, 88, 94] +> ``` + + + +### 202. *symbol* 用途 + +> 参考答案: +> +> 可以用来表示一个独一无二的变量防止命名冲突。但是面试官问还有吗?我没想出其他的用处就直接答我不知道了,还可以利用 symbol 不会被常规的方法(除了 Object.getOwnPropertySymbols 外)遍历到,所以可以用来模拟私有变量。 +> +> 主要用来提供遍历接口,布置了 symbol.iterator 的对象才可以使用 for···of 循环,可以统一处理数据结构。调用之后回返回一个遍历器对象,包含有一个 next 方法,使用 next 方法后有两个返回值 value 和 done 分别表示函数当前执行位置的值和是否遍历完毕。 +> +> Symbol.for() 可以在全局访问 symbol + + + +### 203. 什么是函数式编程,应用场景是什么 + +> 参考答案: +> +> 函数式编程和面向对象编程一样,是一种编程范式。强调执行的过程而非结果,通过一系列的嵌套的函数调用,完成一个运算过程。 +> 它主要有以下几个特点: +> +> 1. 函数是"一等公民":函数优先,和其他数据类型一样。 +> 2. 只用"表达式",不用"语句":通过表达式(*expression*)计算过程得到一个返回值,而不是通过一个语句(*statement*)修改某一个状态。 +> 3. 无副作用:不污染变量,同一个输入永远得到同一个数据。 +> 4. 不可变性:前面一提到,不修改变量,返回一个新的值。 +> +> 函数式编程的概念其实出来也已经好几十年了,我们能在很多编程语言身上看到它的身影。比如比较纯粹的 *Haskell*,以及一些语言开始逐渐成为多范式编程语言,比如 *Swift*,还有 *Kotlin,Java,Js* 等都开始具备函数式编程的特性。 +> +> **函数式编程在前端的应用场景** +> +> - *Stateless components*:*React* 在 *0.14* 之后推出的无状态组件 +> - *Redux* +> +> **函数式编程在后端的应用场景** +> +> - *Lambda* 架构 + + + +### 204. 事件以及事件相关的兼容性问题 + +> 参考答案: +> +> 事件最早是在 *IE3* 和 *Navigator2* 中出现的,当时是作为分担服务器运算负担的一种手段。要实现和网页的互动,就需要通过 *JavaScript* 里面的事件来实现。 +> +> 每次用户与一个网页进行交互,例如点击链接,按下一个按键或者移动鼠标时,就会触发一个事件。我们的程序可以检测这些事件,然后对此作出响应。从而形成一种交互。 +> +> 当我们绑定事件时,需要遵循事件三要素 +> +> - 事件源:是指那个元素引发的事件。比如当你点击图标的时候,会跳转到百度首页。那么这个图标就是事件源。 +> - 事件:事件是指执行的动作。例如,点击,鼠标划过,按下键盘,获得焦点。 +> - 事件驱动程序:事件驱动程序即执行的结果。例如,当你点击图标的时候,会跳转到百度首页。那么跳转到百度首页就是事件的处理结果。 +> +> ```js +> 事件源.事件 = function() { +> 事件处理函数 +> } +> ``` +> +> 常见的兼容问题,可以参阅前面 *135* 题。 + + + +### 205. *JS* 小数不精准,如何计算 + +> 参考答案: +> +> 方法一:指定要保留的小数位数(0.1+0.2).toFixed(1) = 0.3;这个方法toFixed是进行四舍五入的也不是很精准,对于计算金额这种严谨的问题,不推荐使用,而且不同浏览器对toFixed的计算结果也存在差异。 +> +> 方法二:把需要计算的数字升级(乘以10的n次幂)成计算机能够精确识别的整数,等计算完毕再降级(除以10的n次幂),这是大部分编程语言处理精度差异的通用方法。 + + + +### 206. 写一个 *mySetInterVal(fn, a, b)*,每次间隔 *a,a+b,a+2b* 的时间,然后写一个 *myClear*,停止上面的 *mySetInterVal* + +> 参考答案: +> +> 该题的思路就是每一次在定时器中重启定时器并且在时间每一次都加 *b*,并且要把定时器返回回来,可以作为*myClear*的参数。 +> +> 代码如下: +> +> ```js +> var mySetInterVal = function (fn, a, b) { +> var timer = null; +> var settimer = function (fn, a, b) { +> timer = setTimeout(() => { +> fn(); +> settimer(fn, a + b, b); +> }, a); +> } +> settimer(fn, a, b); +> return timer; +> } +> +> var timer = mySetInterVal(() => { console.log('timer') }, 1000, 1000); +> var myClear = function (timer) { +> timer && clearTimeout(timer); +> } +> ``` + + + +### 207. 合并二维有序数组成一维有序数组,归并排序的思路 + +> 参考答案: +> +> 示例代码如下: +> +> ```js +> function merge(left, right) { +> let result = [] +> while (left.length > 0 && right.length > 0) { +> if (left[0] < right[0]) { +> result.push(left.shift()) +> } else { +> result.push(right.shift()) +> } +> } +> return result.concat(left).concat(right) +> } +> function mergeSort(arr) { +> if (arr.length === 1) { +> return arr +> } +> while (arr.length > 1) { +> let arrayItem1 = arr.shift(); +> let arrayItem2 = arr.shift(); +> let mergeArr = merge(arrayItem1, arrayItem2); +> arr.push(mergeArr); +> } +> return arr[0] +> } +> +> let arr1 = [[1, 2, 3], [4, 5, 6], [7, 8, 9], [1, 2, 3], [4, 5, 6]]; +> let arr2 = [[1, 4, 6], [7, 8, 10], [2, 6, 9], [3, 7, 13], [1, 5, 12]]; +> console.log(mergeSort(arr1)) +> console.log(mergeSort(arr2)) +> ``` + + + +### 208. 给定一个字符串,请你找出其中不含有重复字符的最长子串的长度。 + +> 参考答案: +> +> 首先,我们肯定需要封装一个函数,而这个函数接收一个字符串作为参数,返回不含有重复字符的子串长度。来看下面的示例: +> +> 示例 1: +> +> 输入: “abcabcbb” +> 输出: 3 +> 解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。 +> +> 示例 2: +> +> 输入: “bbbbb” +> 输出: 1 +> 解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。 +> +> 示例 3: +> +> 输入: “pwwkew” +> 输出: 3 +> 解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。 +> 请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。 +> +> 示例代码: +> +> ```js +> var lengthOfLongestSubstring = function (s) { +> var y = []; +> var temp = []; +> var maxs = 0; +> if (s == "") { +> return 0; +> } +> if (s.length == 1) { +> return 1; +> } +> for (var i = 0; i < s.length; i++) { +> if (temp.includes(s[i])) { +> +> y.push(temp.length); +> temp.shift(); +> continue; +> } else { +> temp.push(s[i]) +> y.push(temp.length); +> } +> +> } +> for (var j = 0; j < y.length; j++) { +> if (maxs <= y[j]) { +> maxs = y[j] +> } +> } +> return maxs; +> }; +> // 测试 +> console.log(lengthOfLongestSubstring('abcabcbb')); // 3 +> console.log(lengthOfLongestSubstring('bbbbb')); // 1 +> console.log(lengthOfLongestSubstring('pwwkew')); // 3 +> +> ``` + + + +### 209. 有一堆整数,请把他们分成三份,确保每一份和尽量相等(11,42,23,4,5,6 4 5 6 11 23 42 56 78 90)(滴滴 *2020*) + +> 参考答案: +> +> 本道题目是一道考察算法的题目,主要是考察编程基本功和一定的想像力。 +> +> 具体的实现如下: +> +> ```js +> function fun(total, n) { +> //先对整个数组进行排序 +> total.sort((a, b) => a - b); +> +> //求和 +> var sum = 0; +> for (var i = 0; i < total.length; i++) { +> sum += total[i]; +> } +> +> var avg = Math.ceil(sum / n); +> +> //结果数组 +> var result = []; //长度为n +> +> for (var i = 0; i < n; i++) { +> result[i] = [total.pop()]; +> result[i].sum = result[i][0]; +> +> //组成一个分数组 +> while (result[i].sum < avg && total.length > 0) { +> for (var j = 0; j < total.length; j++) { +> if (result[i].sum + total[j] >= avg) { +> result[i].push(total[j]); +> result[i].sum += total[j]; +> break; +> } +> } +> +> if (j == total.length) { +> result[i].push(total.pop()); +> result[i].sum += result[i][result[i].length - 1]; +> } else { +> //从数组中移除此元素 +> total.splice(j, 1); +> } +> } +> +> sum -= result[i].sum; +> avg = Math.ceil(sum / (n - 1 - i)); +> +> } +> return result; +> } +> +> // 测试 +> var arr = [11, 42, 23, 4, 5, 6, 4, 5, 6, 11, 23, 42, 56, 78, 90]; +> console.log(fun(arr, 3)); +> // [ +> // [ 90, 56, sum: 146 ], +> // [ 78, 42, 11, sum: 131 ], +> // [ 42, 23, 23, 11, 6, 6, 5, 5, 4, 4, sum: 129 ] +> // ] +> ``` + + + +### 210. 手写发布订阅(头条2020) + +> 参考答案: +> +> 示例代码如下: +> +> ```html +> +>
+>

this is a test

+> {{msg}}{{msg}} +>
+> +> +> +> ``` +> +> ```js +> /* +> 1. 创建 Vue 构造函数 +> 在 Vue 构造函数中,调用了 observer 函数,该函数的作用就是对数据进行劫持 +> 劫持具体要做的事儿:复制一份数据,但是不是单纯的复制,而是增加了 getter、setter +> 2. 书写 compile 函数。该函数主要作用于模板,从模板里面要提取信息 +> 提取的东西主要有两个:{{}} 和 v-model +> 3. 创建发布者 Dep 的构造函数,如果数据发生变化,发布者就会遍历内部的数组(花名册),通知订阅者修改数据 +> 4. 创建订阅者 Watcher 的构造函数,如果有数据的变化,发布者就会通知订阅者,订阅者上面存在 update 方法,会进行修改 +> */ +> +> function Vue(options){ +> // this 代表 Vue 的实例对象,本例中就是 vm +> // options.data 这就是实际的数据 {msg : 'xiejie'} +> observer(this,options.data); +> this.$el = options.el; +> compile(this); +> } +> +> // 用于对模板进行信息提取,主要提取 {{}} 和 v-model,然后进行一些操作 +> // {{ }} 会成为观察者,v-model 所对应的控件来绑定事件 +> function compile(vm){ +> var el = document.querySelector(vm.$el); // el 所对应的值为
...
+> var documentFragment = document.createDocumentFragment(); // 创建了一个空的文档碎片 +> var reg = /\{\{(.*)\}\}/; // 创建正则表达式 匹配 {{ }} +> while(el.childNodes[0]){ +> var child = el.childNodes[0]; // 将第一个子节点存储到 child +> if(child.nodeType == 1){ +> // 如果能够进入此 if,说明该节点是一个元素节点 +> for(var key in child.attributes){ +> // 遍历该元素节点的每一个属性,拿到的就是 type="text" v-model="msg" +> var attrName = child.attributes[key].nodeName; // 获取属性名 type、v-model +> if(attrName === 'v-model'){ +> var vmKey = child.attributes[key].nodeValue; // 先获取属性值,也就是 msg +> // 为该节点,也就是 绑定一个 input 事件 +> child.addEventListener('input', function (event) { +> vm[vmKey] = event.target.value; // 获取用户输入的值,然后改变 vm 里面的 msg 属性对应的值,注意这里会触发 setter +> }) +> } +> } +> } +> if(child.nodeType == 3){ +> // 如果能进入此 if,说明该节点是一个文本节点 +> if(reg.test(child.nodeValue)){ +> // 如果能够进入到此 if,说明是 {{ }},然后我们要让其成为订阅者 +> var vmKey = RegExp.$1; // 获取正则里面的捕获值,也就是 msg +> // 实例化一个 Watcher(订阅者),接收 3 个参数:Vue 实例,该文本节点,捕获值 msg +> new Watcher(vm, child, vmKey); +> } +> } +> documentFragment.appendChild(el.childNodes[0]); // 将第一个子节点添加到文档碎片里面 +> } +> // 将文档碎片中节点重新添加到 el,也就是
下面 +> el.appendChild(documentFragment); +> } +> +> // 新建发布者构造函数 +> function Dep() { +> // 将观察者添加到发布者内部的数组里面 +> // 这样以便于通知所有的观察者去更新数据 +> this.subs = []; +> } +> +> Dep.prototype = { +> // 将 watcher 添加到发布者内置的数组里面 +> addSub: function (sub) { +> this.subs.push(sub); +> +> }, +> // 遍历数组里面所有的 watcher,通知它们去更新数据 +> notify: function () { +> this.subs.forEach(function (sub) { +> sub.update(); +> }) +> } +> } +> +> // 新建观察者 Watcher 构造函数 +> // 接收 3 个参数:Vue 实例,文本节点 {{ msg }} 以及捕获内容 msg +> function Watcher(vm, child, vmKey) { +> this.vm = vm; // vm +> this.child = child; // {{ msg }} +> this.vmKey = vmKey; // msg +> Dep.target = this; // 将该观察者实例对象添加给 Dep.target +> this.update(); // 执行节点更新方法 +> Dep.target = null; // 最后清空 Dep.target +> } +> Watcher.prototype = { +> // 节点更新方法 +> update: function () { +> // 相当于:{{ msg }}.nodeValue = this.vm['msg'] +> // 这样就更新了文本节点的值,由于这里在获取 vm.msg,所以会触发 getter +> this.child.nodeValue = this.vm[this.vmKey]; +> } +> } +> +> // 该函数的作用是用于数据侦听 +> function observer(vm,obj){ +> var dep = new Dep(); // 新增一个发布者:发布者的作用是告诉订阅者数据已经更改 +> // 遍历数据 +> for(var key in obj){ +> // 将数据的每一项添加到 vm 里面,至此,vm 也有了每一项数据 +> // 但是不是单纯的添加,而是设置了 getter 和 setter +> // 在获取数据时触发 getter,在设置数据时触发 setter +> Object.defineProperty(vm, key, { +> get() { +> console.log("触发get了"); +> // 触发 getter 时,将该 watcher 添加到发布者维护的数组里面 +> if (Dep.target) { +> dep.addSub(Dep.target); // 往发布者的数组里面添加订阅者 +> } +> console.log(dep.subs); +> return obj[key]; +> }, +> set(newVal) { +> console.log("触发set了"); +> obj[key] = newVal; +> dep.notify(); // 发布者发出消息,通知订阅者修改数据 +> } +> }); +> } +> } +> ``` + + + +### 211. 手写用 *ES6proxy* 如何实现 *arr[-1]* 的访问(滴滴2020) + +> 参考答案: +> +> 示例代码如下: +> +> ```js +> const proxyArray = arr => { +> const length = arr.length; +> return new Proxy(arr, { +> get(target, key) { +> key = +key; +> while (key < 0) { +> key += length; +> } +> return target[key]; +> } +> }) +> }; +> var a = proxyArray([1, 2, 3, 4, 5, 6, 7, 8, 9]); +> console.log(a[1]); // 2 +> console.log(a[-10]); // 9 +> console.log(a[-20]); // 8 +> ``` + + + +### 212. 下列代码执行结果 + +```js +console.log(1); +setTimeout(() => { + console.log(2); + process.nextTick(() => { + console.log(3); + }); + new Promise((resolve) => { + console.log(4); + resolve(); + }).then(() => { + console.log(5); + }); +}); +new Promise((resolve) => { + console.log(7); + resolve(); +}).then(() => { + console.log(8); +}); +process.nextTick(() => { + console.log(6); +}); +setTimeout(() => { + console.log(9); + process.nextTick(() => { + console.log(10); + }); + new Promise((resolve) => { + console.log(11); + resolve(); + }).then(() => { + console.log(12); + }); +}); +``` + +> 参考答案: +> +> 1 +> 7 +> 6 +> 8 +> 2 +> 4 +> 3 +> 5 +> 9 +> 11 +> 10 +> 12 + + + +### 213. Number() 的存储空间是多大?如果后台发送了一个超过最大自己的数字怎么办 + +> 参考答案: +> +> Math.pow(2, 53) ,53 为有效数字,会发生截断,等于 JS 能支持的最大数字。 + + + +### 214. 事件是如何实现的?(字节2020) + +> 参考答案: +> +> 基于发布订阅模式,就是在浏览器加载的时候会读取事件相关的代码,但是只有实际等到具体的事件触发的时候才会执行。 +> +> 比如点击按钮,这是个事件(Event),而负责处理事件的代码段通常被称为事件处理程序(Event Handler),也就是「启动对话框的显示」这个动作。 +> +> 在 Web 端,我们常见的就是 DOM 事件: +> +> - DOM0 级事件,直接在 html 元素上绑定 on-event,比如 onclick,取消的话,dom.onclick = null,同一个事件只能有一个处理程序,后面的会覆盖前面的。 +> - DOM2 级事件,通过 addEventListener 注册事件,通过 removeEventListener 来删除事件,一个事件可以有多个事件处理程序,按顺序执行,捕获事件和冒泡事件 +> - DOM3级事件,增加了事件类型,比如 UI 事件,焦点事件,鼠标事件 + + + +### 215. 下列代码执行结果 + +```js +Promise.resolve().then(() => { + console.log(0); + return Promise.resolve(4); +}).then((res) => { + console.log(res) +}) + +Promise.resolve().then(() => { + console.log(1); +}).then(() => { + console.log(2); +}).then(() => { + console.log(3); +}).then(() => { + console.log(5); +}).then(() =>{ + console.log(6); +}) +``` + +> 参考答案: +> +> 0 +> 1 +> 2 +> 3 +> 4 +> 5 +> 6 + +> 解析: +> +> 照着代码,我们先来看初始任务。 +> +> - (初始任务)第一部分 Promise.resolve() 返回 「Promise { undefined }」。 +> +> - (同任务,下同)继续调用 then,then 发现「Promise { undefined }」已解决,直接 enqueue 包含 console.log(0);return Promise.resolve(4) 的任务,之后返回新的「Promise { \ }」(设为 promise0)。被 enqueue 的任务之后会引发 promise0 的 resolve/reject,详见 追加任务一 的 2. 3. 。 +> +> - 继续调用 promise0 上的 then,第二个 then 发现 promise0 还在 pending,因此不能直接 enqueue 新任务,而是将包含 console.log(res) 回调追加到 promise0 的 PromiseFulfillReactions 列表尾部,并返回新的「Promise { }」(设为 promiseRes)(该返回值在代码中被丢弃,但不影响整个过程)。 +> +> - 第二部分 Promise.resolve().then... 同理,只有包含 console.log(1) 的任务被 enqueue。中间结果分别设为 promise1(=Promise.resolve().then(() => {console.log(1);})), promise2, promise3, promise5, promise6。当前任务执行完毕。 +> +> 此时,任务列队上有两个新任务,分别包含有 console.log(0);return Promise.resolve(4) 和 console.log(1) 。我们用 「Job { ??? }」来指代。 +> +> 接下来,「Job { console.log(0);return Promise.resolve(4) }」先被 enqueue,所以先运行「Job { console.log(0);return Promise.resolve(4) }」。 +> +> - (追加任务一)此时「0」被 console.log(0) 输出。Promise.resolve(4) 返回已解决的「Promise { 4 }」,然后 return Promise.resolve(4) 将这个「Promise { 4 }」作为最开始的 Promise.resolve().then(对应 promise0)的 onfulfill 处理程序(即 then(onfulfill, onreject) 的参数 onfulfill)的返回值返回。 +> +> - (同任务,下同)onfulfill 处理程序返回,触发了 promise0 的 Promise Resolve Function(以下简称某 promise(实例)的 resolve)。所谓触发,其实是和别的东西一起打包到「Job { console.log(0);return Promise.resolve(4) }」当中,按流程执行,onfulfill 返回后自然就到它了。(onfulfill 抛异常的话会被捕获并触发 reject,正常返回就是 resolve。) +> +> - promise0 的 resolve 检查 onfulfill 的返回值,发现该值包含**可调用的**「then」属性。这是当然的,因为是「Promise { 4 }」。无论该 Promise 实例是否解决,都将 enqueue 一个新任务包含调用该返回值的 then 的任务(即规范中的 NewPromiseResolveThenableJob(promiseToResolve, thenable, then))。而这个任务才会触发后续操作,在本例中,最终会将 promise0 的 PromiseFulfillReactions (其中有包含 console.log(res) 回调)再打包成任务 enqueue 到任务列队上。当前任务执行完毕。 +> +> 此时,任务列队上还是有两个任务(一进一出),「Job { console.log(1) }」和「NewPromiseResolveThenableJob(promise0, 「Promise { 4 }」, 「Promise { 4 }」.then)」。接下来执行「Job { console.log(1) }」。 +> +> - (追加任务二)「1」被输出。 +> +> - (同任务,下同)onfulfill 处理程序返回 undefined。(JavaScript 的函数默认就是返回 undefined。) +> +> - promise1 的 resolve 发现 undefined 连 Object 都不是,自然不会有 then,所以将 undefined 作为 promise1 的解决结果。即 promise1 从「Promise { \ }」变为 「Promise { undefined }」(fulfill)。 +> +> - resolve 继续查看 promise1 的 PromiseFulfillReactions。(reject 则会查看 PromiseRejectReactions。)有一个之前被 promise1.then 调用追加上的包含 console.log(2) 的回调。打包成任务入列。(如有多个则依次序分别打包入列。)当前任务执行完毕。 +> +> 此时,任务列队上仍然有两个任务(一进一出)。「NewPromiseResolveThenableJob(...)」和 「Job { console.log(2) }」。执行「NewPromiseResolveThenableJob(...)」。 +> +> - (追加任务三)调用 「Promise { 4 }」的 then。这个调用的参数(处理程序 onfulfill 和 onreject) 用的正是 promise0 的 resolve 和 reject。 +> +> - 由于「Promise { 4 }」的 then 是标准的,行为和其他的 then 一致。(可参见初始任务的步骤 2. 。)它发现「Promise { 4 }」已解决,结果是 4。于是直接 enqueue 包含 promise0 的 resolve 的任务,参数是 4。理论上同样返回一个「Promise { }」,由于是在内部,不被外部观察,也不产生别的影响。)当前任务执行完毕。 +> +> 此时,任务列队上依旧有两个任务(一进一出)。「Job { console.log(2) }」和 「Job { promise0 的 resolve }」。执行「Job { console.log(2) }」。 +> +> - (追加任务四)过程类似「Job { console.log(1) }」的执行。「2」被输出。「Job { console.log(3) }」入列。其余不再赘述。当前任务执行完毕。 +> +> 此时,任务列队上依然有两个任务(一进一出)。「Job { promise0 的 resolve }」和「Job { console.log(3) }」。执行「Job { promise0 的 resolve }」。 +> +> - (追加任务五)promise0 的 resolve 查看 PromiseFulfillReactions 发现有被 promise0.then 追加的回调。打包成任务入列。该任务包含 console.log(res),其中传递 promise0 解决结果 4 给参数 res。当前任务执行完毕。 +> +> 此时,任务列队上还是两个任务(一进一出)。「Job { console.log(3) }」和「Job { console.log(res) }」。 +> +> - (追加任务六)输出「3」。「Job { console.log(5) }」入列。 +> +> 此时,任务列队上还是两个任务(一进一出)。「Job { console.log(res) }」和「Job { console.log(5) }」。 +> +> - (追加任务七)输出「4」。由于 promiseRes 没有被 then 追加回调。就此打住。 +> +> 此时,任务列队上终于不再是两个任务了。下剩「Job { console.log(5) }」。 +> +> - (追加任务八)输出「5」。「Job { console.log(6) }」入列。 +> +> 最后一个任务(追加任务九)输出「6」。任务列队清空。 +> +> 因此,输出的顺序是「0 1 2 3 4 5 6」。 +> +> 总结一下,除去初始任务,总共 enqueue 了 9 个任务。其中,第一串 Promise + then... enqueue 了 4 个。第二串 Promise + then... enqueue 了 5 个。分析可知,每增加一个 then 就会增加一个任务入列。 +> +> 而且,第一串的 return Promise.resolve(4) 的写法额外 enqueue 了 2 个任务,分别在 promise0 的 resolve 时(追加任务一 3.)和调用「Promise { 4 }」的 then 本身时(追加任务三 2.)。 +> +> 根据规范,它就该这样。说不上什么巧合,可以算是有意为之。处理程序里返回 thenable 对象就会导致增加两个任务入列。 + + + +### 216. 判断数组的方法,请分别介绍它们之间的区别和优劣 + +> 参考答案: +> +> 方法一:**instanceof 操作符判断** +> +> **用法:arr instanceof Array** +> +> **instanceof 主要是用来判断某个实例是否属于某个对象** +> +> ```js +> let arr = []; +> console.log(arr instanceof Array); // true +> ``` +> +> 缺点:instanceof是判断类型的prototype是否出现在对象的原型链中,但是对象的原型可以随意修改,所以这种判断并不准确。并且也不能判断对象和数组的区别 +> +> 方法二:**对象构造函数的 constructor判断** +> +> **用法:arr.constructor === Array** +> +> **Object的每个实例都有构造函数 constructor,用于保存着用于创建当前对象的函数** +> +> ```js +> let arr = []; +> console.log(arr.constructor === Array); // true +> ``` +> +> 方法三:**Array 原型链上的 isPrototypeOf** +> +> **用法:Array.prototype.isPrototypeOf(arr**) +> +> **Array.prototype 属性表示 Array 构造函数的原型** +> +> 其中有一个方法是 isPrototypeOf() 用于测试一个对象是否存在于另一个对象的原型链上。 +> +> ```js +> let arr = []; +> console.log(Array.prototype.isPrototypeOf(arr)); // true +> ``` +> +> 方法四:**Object.getPrototypeOf** +> +> **用法:Object.getPrototypeOf(arr) === Array.prototype** +> +> **Object.getPrototypeOf() 方法返回指定对象的原型** +> +> 所以只要跟Array的原型比较即可 +> +> ```js +> let arr = []; +> console.log(Object.getPrototypeOf(arr) === Array.prototype); // true +> ``` +> +> 方法五:**Object.prototype.toString** +> +> **用法:Object.prototype.toString.call(arr) === '[object Array]'** +> +> 虽然Array也继承自Object,但js在Array.prototype上重写了toString,而我们通过toString.call(arr)实际上是通过原型链调用了。 +> +> ```js +> let arr = []; +> console.log(Object.prototype.toString.call(arr) === '[object Array]'); // true +> ``` +> +> 缺点:不能精准判断自定义对象,对于自定义对象只会返回[object Object] +> +> 方法六:**Array.isArray** +> +> **用法:Array.isArray(arr)** +> +> **ES5中新增了Array.isArray方法,IE8及以下不支持** +> +> Array.isArray ( arg ) +> isArray 函数需要一个参数 arg,如果参数是个对象并且 class 内部属性是 "Array", 返回布尔值 true;否则它返回 false。 +> +> ```js +> let arr = []; +> console.log(Array.isArray(arr)); // true +> ``` +> +> 缺点:Array.isArray是ES 5.1推出的,**不支持IE6~8**,所以在使用的时候需要注意兼容性问题。 + + + +### 217. JavaScript 中的数组和函数在内存中是如何存储的? + +> 参考答案: +> +> 在 *JavaScript* 中,数组不是以一段连续的区域存储在内存中,而是一种哈希映射的形式存储在堆内容里面。它可以通过多种数据结构实现,其中一种是链表。如下图所示: +> +> image-20210818205006459 +> +> JavaScript 中的函数是存储在堆内存中的,具体的步骤如下: +> +> 1. 开辟堆内存(*16* 进制得到内存地址) +> 2. 声明当前函数的作用域(函数创建的上下文才是他的作用域,和在那执行的无关) +> 3. 把函数的代码以字符串的形式存储在堆内存中(函数再不执行的情况下,只是存储在堆内存中的字符串) +> 4. 将函数堆的地址,放在栈中供变量调用(函数名) + + + +### 218. *JavaScript* 是如何运行的?解释型语言和编译型语言的差异是什么? + +> 参考答案: +> +> 关于第一个问题,这不是三言两语或者几行文字就能够讲清楚的,这里放上一篇博文地址: +> +> *https://segmentfault.com/a/1190000019530109* +> +> 之后在直播课或者录屏课进行详细的讲解 +> +> 第二个问题:解释型语言和编译型语言的差异是什么? +> +> 电脑能认得的是二进制数,不能够识别高级语言。所有高级语言在电脑上执行都需要先转变为机器语言。但是高级语言有两种类型:编译型语言和解释型语言。常见的编译型语言语言有C/C++、Pascal/Object 等等。常见的解释性语言有python、JavaScript等等。 +> +> 编译型语言先要进行编译,然后转为特定的可执行文件,这个可执行文件是针对平台的(CPU类型),可以这么理解你在PC上编译一个C源文件,需要经过预处理,编译,汇编等等过程生成一个可执行的二进制文件。当你需要再次运行改代码时,不需要重新编译代码,只需要运行该可执行的二进制文件。优点,编译一次,永久执行。还有一个优点是,你不需要提供你的源代码,你只需要发布你的可执行文件就可以为客户提供服务,从而保证了你的源代码的安全性。但是,如果你的代码需要迁移到linux、ARM下时,这时你的可执行文件就不起作用了,需要根据新的平台编译出一个可执行的文件。这也就是多个平台需要软件的多个版本。缺点是,跨平台能力差。 +> +> 解释型语言需要一个解释器,在源代码执行的时候被解释器翻译为一个与平台无关的中间代码,解释器会把这些代码翻译为及其语言。打个比方,编译型中的编译相当于一个翻译官,它只能翻译英语,而且中文文章翻译一次就不需要重新对文章进行二次翻译了,但是如果需要叫这个翻译官翻译德语就不行了。而解释型语言中的解释器相当于一个会各种语言的机器人,而且这个机器人回一句一句的翻译你的语句。对于不同的国家,翻译成不同的语言,所以,你只需要带着这个机器人就可以。解释型语言的有点是,跨平台,缺点是运行时需要源代码,知识产权保护性差,运行效率低。 + + + +### 219. 列举你所了解的编程范式? + +> 参考答案: +> +> 编程范式 *Programming paradigm* 是指计算机中编程的典范模式或方法。 +> +> 常见的编程范式有:函数式编程、程序编程、面向对象编程、指令式编程等。 +> +> 不同的编程语言也会提倡不同的“编程范型”。一些语言是专门为某个特定的范型设计的,如 *Smalltalk* 和 *Java* 支持面向对象编程。而 *Haskell* 和 *Scheme* 则支持函数式编程。现代编程语言的发展趋势是支持多种范型,例如 *ES* 支持函数式编程的同时也支持面向对象编程。 + + + +### 220. 什么是面向切面(AOP)的编程? + +> 参考答案: +> +> **什么是AOP?** +> +> AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后, 再通过“动态织入”的方式掺入业务逻辑模块中。 +> +> +> +> **AOP能给我们带来什么好处?** +> +> AOP的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块。 +> +> +> +> **JavaScript实现AOP的思路?** +> +> 通常,在 JavaScript 中实现 AOP,都是指把一个函数“动态织入”到另外一个函数之中,具体的实现技术有很多,下面我用扩展 Function.prototype 来做到这一点。 +> +> 主要就是两个函数,在Function的原型上加上before与after,作用就是字面的意思,在函数的前面或后面执行,相当于**无侵入**把一个函数插入到另一个函数的前面或后面,应用得当可以很好的实现代码的解耦,js中的代码实现如下: +> +> ```js +> //Aop构造器 +> function Aop(options){ +> this.options = options +> } +> //业务方法执行前钩子 +> Aop.prototype.before = function(cb){ +> cb.apply(this) +> } +> //业务方法执行后钩子 +> Aop.prototype.after = function(cb){ +> cb.apply(this) +> } +> //业务方法执行器 +> Aop.prototype.execute = function(beforeCb,runner,afterCb){ +> this.before(beforeCb) +> runner.apply(this) +> this.after(afterCb) +> } +> +> var aop = new Aop({ +> afterInfo:'执行后', +> runnerInfo:'执行中', +> beforeInfo:'执行前' +> }) +> +> var beforeCb = function(){ +> console.log(this.options.beforeInfo) +> } +> var afterCb = function(){ +> console.log(this.options.afterInfo) +> } +> var runnerCb = function(){ +> console.log(this.options.runnerInfo) +> } +> +> aop.execute(beforeCb,runnerCb,afterCb) +> ``` +> +> 应用的一些例子: +> +> 1. 为 *window.onload* 添加方法,防止 *window.onload* 被二次覆盖 +> 2. 无侵入统计某个函数的执行时间 +> 3. 表单校验 +> 4. 统计埋点 +> 5. 防止 *csrf* 攻击 + + + +### 221. *JavaScript* 中的 *const* 数组可以进行 *push* 操作吗?为什么? + +> 参考答案: +> +> 可以进行 *push* 操作。虽然 *const* 表示常量,但是当我们把一个数组赋值给 *const* 声明的变量时,实际上是把这个数组的地址赋值给该变量。而 *push* 操作是在数组地址所指向的堆区添加元素,地址本身并没有发生改变。 +> +> 示例代码: +> +> ```js +> const arr = [1]; +> arr.push(2); +> console.log(arr); // [1, 2] +> ``` + + + +### 222. JavaScript 中对象的属性描述符有哪些?分别有什么作用? + +> 参考答案: +> +> 从*ES5*开始,添加了对对象**属性描述符**的支持。现在*JavaScript*中支持 *4* 种属性描述符: +> +> - **configurable:** 当且仅当该属性的*configurable*键值为*true*时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。 +> - **enumerable:** 当且仅当该属性的*enumerable*键值为*true*时,该属性才会出现在对象的枚举属性中。 +> - **value:** 该属性对应的值。可以是任何有效的 *JavaScript* 值(数值,对象,函数等)。 +> - **writable:** 当且仅当该属性的*writable*键值为*true*时,属性的值,也就是上面的value,才能被赋值运算符改变。 + + + +### 223. *JavaScript* 中 *console* 有哪些 *api* ? + +> 参考答案: +> +> **console.assert(expression, object[, object...])** +> +> 接收至少两个参数,第一个参数的值或返回值为`false`的时候,将会在控制台上输出后续参数的值。 +> +> **console.count([label])** +> +> 输出执行到该行的次数,可选参数 label 可以输出在次数之前。 +> +> **console.dir(object)** +> +> 将传入对象的属性,包括子对象的属性以列表形式输出。 +> +> **console.error(object[, object...])** +> +> 用于输出错误信息,用法和常见的`console.log`一样,不同点在于输出内容会标记为错误的样式,便于分辨。 +> +> **console.group** +> +> 这是个有趣的方法,它能够让控制台输出的语句产生不同的层级嵌套关系,每一个`console.group()`会增加一层嵌套,相反要减少一层嵌套可以使用`console.groupEnd()`方法。 +> +> **console.info(object[, object...])** +> +> 此方法与之前说到的`console.error`一样,用于输出信息,没有什么特别之处。 +> +> **console.table()** +> +> 可将传入的对象,或数组以表格形式输出,相比传统树形输出,这种输出方案更适合内部元素排列整齐的对象或数组,不然可能会出现很多的 undefined。 +> +> **console.log(object[, object...])** +> +> 输入一段 *log* 信息。 +> +> **console.profile([profileLabel])** +> +> 这是个挺高大上的东西,可用于性能分析。在 JS 开发中,我们常常要评估段代码或是某个函数的性能。在函数中手动打印时间固然可以,但显得不够灵活而且有误差。借助控制台以及`console.profile()`方法我们可以很方便地监控运行性能。 +> +> **console.time(name)** +> 计时器,可以将成对的`console.time()`和`console.timeEnd()`之间代码的运行时间输出到控制台上,`name`参数可作为标签名。 +> +> **console.trace()** +> +> `console.trace()`用来追踪函数的调用过程。在大型项目尤其是框架开发中,函数的调用轨迹可以十分复杂,`console.trace()`方法可以将函数的被调用过程清楚地输出到控制台上。 +> +> **console.warn(object[, object...])** +> +> 输出参数的内容,作为警告提示。 + + + +### 224. 简单对比一下 *Callback、Promise、Generator、Async* 几个异步 *API* 的优劣? + +> 参考答案: +> +> 请参阅前面第 *31* 题答案。 + + + +### 225. *Object.defineProperty* 有哪几个参数?各自都有什么作用 + +> 参考答案: +> +> 在 *JavaScript* 中,通过 *Object.defineProperty* 方法可以设置对象属性的特性,选项如下: +> +> - *get*:一旦目标属性被访问时,就会调用相应的方法 +> - *set*:一旦目标属性被设置时,就会调用相应的方法 +> - *value*:这是属性的值,默认是 *undefined* +> - *writable*:这是一个布尔值,表示一个属性是否可以被修改,默认是 *true* +> - *enumerable*:这是一个布尔值,表示在用 *for-in* 循环遍历对象的属性时,该属性是否可以显示出来,默认值为 *true* +> - *configurable*:这是一个布尔值,表示我们是否能够删除一个属性或者修改属性的特性,默认值为 *true* + + + +### 226. *Object.defineProperty* 和 *ES6* 的 *Proxy* 有什么区别? + +> 参考答案: +> +> ##### 1、*Object.defineproperty* +> +> 可以用于监听对象的数据变化 +> +> 语法: ***Object.defineproperty(obj, key, descriptor)*** +> +> ```javascript +> let obj = { +> age: 11 +> } +> let value = 'xiaoxiao'; +> //defineproperty 有 gettter 和 setter +> Object.defineproperty(obj, 'name', { +> get() { +> return value +> }, +> set(newValue) { +> value = newValue +> } +> }) +> obj.name = 'pengpeng'; +> ``` +> +> 此外 还有以下配置项 : +> +> - *configurable* +> - *enumerable* +> - *value* +> +> 缺点: +> +> 1. 无法监听数组变化 +> +> 2. 只能劫持对象的属性,属性值也是对象那么需要深度遍历 +> +> ##### 2、*proxy* :可以理解为在被劫持的对象之前 加了一层拦截 +> +> ```javascript +> let proxy = new Proxy({}, { +> get(obj, prop) { +> return obj[prop] +> }, +> set(obj, prop, val) { +> obj[prop] = val +> } +> }) +> ``` +> +> - *proxy* 返回的是一个新对象, 可以通过操作返回的新的对象达到目的 +> - *proxy* 有多达 *13* 种拦截方法 +> +> **总结:** +> +> - *Object.defineProperty* 无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应 +> - *Object.defineProperty* 只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。*Proxy* 可以劫持整个对象,并返回一个新的对象。 +> - *Proxy* 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。 + + + +### 227. *intanceof* 操作符的实现原理及实现 + +> 参考答案: +> +> *instanceof* 主要作用就是判断一个实例是否属于某种类型 +> +> 例如: +> +> ```js +> let Dog = function(){} +> let tidy = new Dog() +> tidy instanceof Dog //true +> ``` +> +> *intanceof* 操作符实现原理 +> +> ```js +> function wonderFulInstanceOf(instance, constructorFn) { +> let constructorFnProto = constructorFn.prototype; // 取右表达式的 prototype 值,函数构造器指向的function +> instanceProto = instance.__proto__; // 取左表达式的__proto__值,实例的__proto__ +> while (true) { +> if (instanceProto === null) { +> return false; +> } +> if (instanceProto === constructorFnProto) { +> return true; +> } +> instanceProto = instanceProto.__proto__ +> } +> } +> ``` +> +> 其实 *instanceof* 主要的实现原理就是只要 *constructorFn* 的 *prototype* 在*instance*的原型链上即可。 +> +> 因此,*instanceof* 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 *prototype*,如果查找失败,则会返回 *false*,告诉我们左边变量并非是右边变量的实例。 + + + +### 228. 强制类型转换规则? + +> 参考答案: +> +> 首先需要参阅前面第 *104* 题答案。了解隐式转换所调用的函数。 +> +> 当程序员显式调用 Boolean(value)、Number(value)、String(value) 完成的类型转换,叫做显示类型转换。 +> +> 当通过 new Boolean(value)、new Number(value)、new String(value) 传入各自对应的原始类型的值,可以实现“装箱”,将原始类型封装成一个对象。 +> +> 其实这三个函数不仅仅可以当作构造函数,它们可以直接当作普通的函数来使用,将任何类型的参数转化成原始类型的值: +> +> ```javascript +> Boolean('sdfsd'); // true +> Number("23"); // 23 +> String({a:24}); // "[object Object]" +> ``` +> +> 其实这三个函数用于类型转换的时候,调用的就是 js 内部的 *ToBoolean ( argument )、ToNumber ( argument )、ToString ( argument )* 方法,从而达到显式转换的效果。 + + + +### 229. *Object.is*( ) 与比较操作符 “===”、“==” 的区别 + +> 参考答案: +> +> == (或者 !=) 操作在需要的情况下自动进行了类型转换。=== (或 !==)操作不会执行任何转换。 +> +> ===在比较值和类型时,可以说比==更快。 +> +> 而在*ES6*中,*Object.is*( ) 类似于 ===,但在三等号判等的基础上特别处理了 *NaN* 、-0 和 +0 ,保证 -0 和 +0 不再相同,但 *Object.is(NaN, NaN)* 会返回 *true*。 + + + +### 230. `+` 操作符什么时候用于字符串的拼接? + +> 参考答案: +> +> 在有一边操作数是字符串时会进行字符串拼接。 +> +> 示例代码: +> +> ```js +> console.log(5 + '5', typeof (5 + '5')); // 55 string +> ``` + + + +### 231. *object.assign* 和扩展运算法是深拷贝还是浅拷贝 + +> 参考答案: +> +> 这两个方式都是浅拷贝。 +> +> 在拷贝的对象只有一层时是深拷贝,但是一旦对象的属性值又是一个对象,也就是有两层或者两层以上时,就会发现这两种方式都是浅拷贝。 + + + +### 232. *const* 对象的属性可以修改吗 + +> 参考答案: +> +> 可以修改,具体原因可以参阅前面第 *231* 题。 + + + +### 233. 如果 *new* 一个箭头函数的会怎么样 + +> 参考答案: +> +> 会报错,因为箭头函数无法作为构造函数。 + + + +### 234. 扩展运算符的作用及使用场景 + +> 参考答案: +> +> 扩展运算符是三个点(...),主要用于展开数组,将一个数组转为参数序列。 +> +> 扩展运算符使用场景: +> +> - 代替数组的 *apply* 方法 +> - 合并数组 +> - 复制数组 +> - 把 *arguments* 或 *NodeList* 转为数组 +> - 与解构赋值结合使用 +> - 将字符串转为数组 + + + +### 235. *Proxy* 可以实现什么功能? + +> 参考答案: +> +> *Proxy* 是 *ES6* 中新增的一个特性。*Proxy* 让我们能够以简洁易懂的方式控制外部对对象的访问。其功能非常类似于设计模式中的代理模式。 +> +> *Proxy* 在目标对象的外层搭建了一层拦截,外界对目标对象的某些操作,必须通过这层拦截。 +> +> 使用 *Proxy* 的好处是对象只需关注于核心逻辑,一些非核心的逻辑(如:读取或设置对象的某些属性前记录日志;设置对象的某些属性值前,需要验证;某些属性的访问控制等)可以让 *Proxy* 来做。从而达到关注点分离,降级对象复杂度的目的。 +> +> *Proxy* 的基本语法如下: +> +> ```javascript +> var proxy = new Proxy(target, handler); +> ``` +> +> 通过构造函数来生成 *Proxy* 实例,构造函数接收两个参数。*target* 参数是要拦截的目标对象,*handler* 参数也是一个对象,用来定制拦截行为。 +> +> *Vue 3.0* 主要就采用的 *Proxy* 特性来实现响应式,相比以前的 *Object.defineProperty* 有以下优点: +> +> - 可以劫持整个对象,并返回一个新的对象 +> - 有 *13* 种劫持操作 + + + +### 236. 对象与数组的解构的理解 + +> 参考答案: +> +> 解构是 *ES6* 的一种语法规则,可以将一个对象或数组的某个属性提取到某个变量中。 +> +> 解构对象示例: +> +> ```js +> // var/let/const{属性名}=被解构的对象 +> const user = { +> name: "abc", +> age: 18, +> sex: "男", +> address: { +> province: "重庆", +> city: "重庆" +> } +> } +> let { name, age, sex, address} = user; +> console.log(name, age, sex, address); +> ``` +> +> 解构数组示例: +> +> ```js +> const [a, b, c] = [1, 2, 3]; +> ``` + + + +### 237. 如何提取高度嵌套的对象里的指定属性? + +> 参考答案: +> +> 一般会使用递归的方式来进行查找。下面是一段示例代码: +> +> ```js +> function findKey(data, field) { +> let finding = ''; +> for (const key in data) { +> if (key === field) { +> finding = data[key]; +> } +> if (typeof (data[key]) === 'object') { +> finding = findKey(data[key], field); +> } +> if (finding) { +> return finding; +> } +> } +> return null; +> } +> // 测试 +> console.log(findKey({ +> name: 'zhangsan', +> age: 18, +> stuInfo: { +> stuNo: 1, +> classNo: 2, +> score: { +> htmlScore: 100, +> cssScore: 90, +> jsScore: 95 +> } +> } +> }, 'cssScore')); // 90 +> ``` + + + +### 238. *Unicode、UTF-8、UTF-16、UTF-32* 的区别? + +> 参考答案: +> +> *Unicode* **为世界上所有字符都分配了一个唯一的数字编号**,这个编号范围从 *0x000000* 到 *0x10FFFF* (十六进制),有 *110* 多万,每个字符都有一个唯一的 *Unicode* 编号,这个编号一般写成 *16* 进制,在前面加上 U+。例如:“马”的 *Unicode* 是 *U+9A6C*。 +> *Unicode* 就相当于一张表,建立了字符与编号之间的联系。 +> +> image-20210817160125144 +> +> ***Unicode* 本身只规定了每个字符的数字编号是多少,并没有规定这个编号如何存储。** +> +> 那我们可以直接把 *Unicode* 编号直接转换成二进制进行存储,怎么对应到二进制表示呢? +> +> *Unicode* 可以使用的编码有三种,分别是: +> +> - *UFT-8*:一种变长的编码方案,使用 *1~6* 个字节来存储; +> - *UFT-32*:一种固定长度的编码方案,不管字符编号大小,始终使用 *4* 个字节来存储; +> - *UTF-16*:介于 *UTF-8* 和 *UTF-32* 之间,使用 *2* 个或者 *4* 个字节来存储,长度既固定又可变。 + + + +### 239. 为什么函数的 *arguments* 参数是类数组而不是数组?如何遍历类数组? + +> 参考答案: +> +> 首先了解一下什么是数组对象和类数组对象。 +> +> 数组对象:使用单独的变量名来存储一系列的值。从 *Array* 构造函数中继承了一些用于进行数组操作的方法。 +> +> 例如: +> +> ```js +> var mycars = new Array(); +> mycars[0] = "zhangsan"; +> mycars[1] = "lisi"; +> mycars[2] = "wangwu"; +> ``` +> +> 类数组对象:**对于一个普通的对象来说,如果它的所有 property 名均为正整数,同时也有相应的length属性,那么虽然该对象并不是由Array构造函数所创建的,它依然呈现出数组的行为,在这种情况下,这些对象被称为“类数组对象”。** +> +> **两者区别** +> +> - 一个是对象,一个是数组 +> +> - 数组的*length*属性,当新的元素添加到列表中的时候,其值会自动更新。类数组对象的不会。 +> +> - 设置数组的*length*属性可以扩展或截断数组。 +> +> - 数组也是*Array*的实例可以调用*Array*的方法,比如*push、pop*等等 +> +> 所以说*arguments*对象不是一个 *Array* 。它类似于*Array*,但除了*length*属性和索引元素之外没有任何*Array*属性。 +> +> 可以使用 *for...in* 来遍历 *arguments* 这个类数组对象。 + + + +### 240. *escape、encodeURI、encodeURIComponent* 的区别 + +> 参考答案: +> +> *escape* 除了 *ASCII* 字母、数字和特定的符号外,对传进来的字符串全部进行转义编码,因此如果想对 *URL* 编码,最好不要使用此方法。 +> +> *encodeURI* 用于编码整个 *URI*,因为 *URI* 中的合法字符都不会被编码转换。 +> +> *encodeURIComponent* 方法在编码单个*URIComponent*(指请求参数)应当是最常用的,它可以讲参数中的中文、特殊字符进行转义,而不会影响整个 *URL*。 + + + +### 241. *use strict* 是什么意思 ? 使用它区别是什么? + +> 参考答案: +> +> *use strict* 代表开启严格模式,这种模式使得 *Javascript* 在更严格的条件下运行,实行更严格解析和错误处理。 +> +> 开启“严格模式”的优点: +> +> - 消除 *Javascript* 语法的一些不合理、不严谨之处,减少一些怪异行为; +> - 消除代码运行的一些不安全之处,保证代码运行的安全; +> - 提高编译器效率,增加运行速度; +> - 为未来新版本的 *Javascript* 做好铺垫。 + + + +### 242. *for...in* 和 *for...of* 的区别 + +> 参考答案: +> +> *JavaScript* 原有的 *for...in* 循环,只能获得对象的键名,不能直接获取键值。*ES6* 提供 *for...of* 循环,允许遍历获得键值。 +> +> 例如: +> +> ```js +> var arr = ['a', 'b', 'c', 'd']; +> +> for (let a in arr) { +> console.log(a); // 0 1 2 3 +> } +> +> for (let a of arr) { +> console.log(a); // a b c d +> } +> ``` + + + +### 243. *ajax、axios、fetch* 的区别 + +> 参考答案: +> +> *ajax* 是指一种创建交互式网页应用的网页开发技术,并且可以做到无需重新加载整个网页的情况下,能够更新部分网页,也叫作局部更新。 +> +> 使用 *ajax* 发送请求是依靠于一个对象,叫 *XmlHttpRequest* 对象,通过这个对象我们可以从服务器获取到数据,然后再渲染到我们的页面上。现在几乎所有的浏览器都有这个对象,只有 *IE7* 以下的没有,而是通过 *ActiveXObject* 这个对象来创建的。 +> +> *Fetch* 是 *ajax* 非常好的一个替代品,基于 *Promise* 设计,使用 *Fetch* 来获取数据时,会返回给我们一个 *Pormise* 对象,但是 *Fetch* 是一个低层次的 *API*,想要很好的使用 *Fetch*,需要做一些封装处理。 +> +> 下面是 *Fetch* 的一些缺点 +> +> - *Fetch* 只对网络请求报错,对 *400,500* 都当做成功的请求,需要封装去处理 +> - *Fetch* 默认不会带 *cookie*,需要添加配置项。 +> - *Fetch* 不支持 *abort*,不支持超时控制,使用 *setTimeout* 及 *Promise.reject* 的实现超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费。 +> - *Fetch* 没有办法原生监测请求的进度,而 *XHR* 可以。 +> +> *Vue2.0* 之后,*axios* 开始受到更多的欢迎了。其实 *axios* 也是对原生 *XHR* 的一种封装,不过是 *Promise* 实现版本。它可以用于浏览器和 *nodejs* 的 *HTTP* 客户端,符合最新的 *ES* 规范。 + + + +### 244. 下面代码的输出是什么?( *D* ) + +```javascript +function sayHi() { + console.log(name); + console.log(age); + var name = "Lydia"; + let age = 21; +} + +sayHi(); +``` + +- A: *Lydia* 和 *undefined* +- B: *Lydia* 和 *ReferenceError* +- C: *ReferenceError* 和 *21* +- D: *undefined* 和 *ReferenceError* + +> **分析:** +> +> 在 *sayHi* 函数内部,通过 *var* 声明的变量 *name* 会发生变量提升,*var name* 会提升到函数作用域的顶部,其默认值为 *undefined*。因此输出 *name* 时得到的值为 *undefined*; +> +> *let* 声明的 *age* 不会发生变量提升,在输出 *age* 时该变量还未声明,因此会抛出 *ReferenceError* 的报错。 + + + +### 245. 下面代码的输出是什么?( *C* ) + +```javascript +for (var i = 0; i < 3; i++) { + setTimeout(() => console.log(i), 1); +} + +for (let i = 0; i < 3; i++) { + setTimeout(() => console.log(i), 1); +} +``` + +- A: *0 1 2* 和 *0 1 2* +- B: *0 1 2* 和 *3 3 3* +- C: *3 3 3* 和 *0 1 2* + +> 分析: +> +> *JavaScript* 中的执行机制,*setTimeout* 为异步代码,因此在 *setTimeout* 执行时,*for* 循环已经执行完毕。 +> +> 第一个 *for* 循环中的变量 *i* 通过 *var* 声明, 为全局变量,因此每一次的 *i++* 都会将全局变量 *i* 的值加 *1*,当第一个 *for* 执行完成后 *i* 的值为 *3*。所以再执行 *setTimeout* 时,输出 *i* 的值都为 *3*; +> +> 第二个 *for* 循环中的变量 *i* 通过 *let* 声明,为局部变量,因此每一次 *for* 循环时都会产生一个块级作用域,用来存储本次循环中新产生的 *i* 的值。当循环结束后,*setTimeout* 会沿着作用域链去对应的块级作用域中寻找对应的 *i* 值。 + + + +### 246. 下面代码的输出是什么?( *B* ) + +```javascript +const shape = { + radius: 10, + diameter() { + return this.radius * 2; + }, + perimeter: () => 2 * Math.PI * this.radius +}; + +shape.diameter(); +shape.perimeter(); +``` + +- A: *20* 和 *62.83185307179586* +- B: *20* 和 *NaN* +- C: *20* 和 *63* +- D: *NaN* 和 *63* + +> **分析:** +> +> *diameter* 作为对象的方法,其内部的 *this* 指向调用该方法的对象,因此 *this.raduus* 获取到的是 *shape.radius* 的值 *10*,再乘以 *2* 输出的值即为 *20*; +> +> *perimeter* 是一个箭头函数,其内部的 *this* 应该继承声明时所在上下文中的 *this*,在这里即继承全局的 *this*,因此 *this.radius* 值的为 *undefined*,*undefined* 与数值相乘后值为 *NaN*。 + + + +### 247. 下面代码的输出是什么?( *A* ) + +``` ++true; +!"Lydia"; +``` + +- A: *1* 和 *false* +- B: *false* 和 *NaN* +- C: *false* 和 *false* + +> **分析:** +> +> 一元加号会将数据隐式转换为 *number* 类型,*true* 转换为数值为 *1*; +> +> 非运算符 *!* 会将数据隐式转换为 *boolean* 类型后进行取反,*"Lydia"* 转换为布尔值为 *true*,取反后为 *false*。 + + + +### 248. 哪个选项是不正确的?( *A* ) + +```javascript +const bird = { + size: "small" +}; + +const mouse = { + name: "Mickey", + small: true +}; +``` + +- A: *mouse.bird.size* +- B: *mouse[bird.size]* +- C: *mouse[bird["size"]]* +- D: 以上选项都对 + +> **分析:** +> +> *mouse* 对象中没有 *bird* 属性,当访问一个对象不存在的属性时值为 *undefined*,因此 *mouse.bird* 的值为 *undefined*,而 *undefined* 作为原始数据类型没有 *size* 属性,因此再访问 *undefined.size* 时会报错。 + + + +### 249. 下面代码的输出是什么?( *A* ) + +```javascript +let c = { greeting: "Hey!" }; +let d; + +d = c; +c.greeting = "Hello"; +console.log(d.greeting); +``` + +- A: *Hello* +- B: *undefined* +- C: *ReferenceError* +- D: *TypeError* + +> **分析:** +> +> 在 *JavaScript* 中,复杂类型数据在进行赋值操作时,进行的是「引用传递」,因此变量 *d* 和 *c* 指向的是同一个引用。当 *c* 通过引用去修改了数据后,*d* 再通过引用去访问数据,获取到的实际就是 *c* 修改后的数据。 + + + +### 250. 下面代码的输出是什么?( *C* ) + +```js +let a = 3; +let b = new Number(3); +let c = 3; + +console.log(a == b); +console.log(a === b); +console.log(b === c); +``` + +- A: *true* *false* *true* +- B: *false* *false* *true* +- C: *true* *false* *false* +- D: *false* *true* *true* + +> **分析:** +> +> *new Number()* 是 *JavaScript* 中一个内置的构造函数。变量 *b* 虽然看起来像一个数字,但它并不是一个真正的数字:它有一堆额外的功能,是一个对象。 +> +> == 会触发隐式类型转换,右侧的对象类型会自动转换为 *Number* 类型,因此最终返回 *true*。 +> +> === 不会触发隐式类型转换,因此在比较时由于数据类型不相等而返回 *false*。 + + + +### 251. 下面代码的输出是什么?( *D* ) + +```js +class Chameleon { + static colorChange(newColor) { + this.newColor = newColor; + } + + constructor({ newColor = "green" } = {}) { + this.newColor = newColor; + } +} + +const freddie = new Chameleon({ newColor: "purple" }); +freddie.colorChange("orange"); +``` + +- A: *orange* +- B: *purple* +- C: *green* +- D: *TypeError* + +> **分析**: +> +> *colorChange* 方法是静态的。 静态方法仅在创建它们的构造函数中存在,并且不能传递给任何子级。 由于 *freddie* 是一个子级对象,函数不会传递,所以在 *freddie* 实例上不存在 *colorChange* 方法:抛出*TypeError*。 + + + +### 252. 下面代码的输出是什么?( *A* ) + +```js +let greeting; +greetign = {}; // Typo! +console.log(greetign); +``` + +- A: *{}* +- B: *ReferenceError: greetign is not defined* +- C: *undefined* + +> **分析:** +> +> 控制台会输出空对象,因为我们刚刚在全局对象上创建了一个空对象! +> +> 当我们错误地将 *greeting* 输入为 *greetign* 时,*JS* 解释器实际上在浏览器中将其视为 *window.greetign = {}*。 + + + +### 253. 当我们执行以下代码时会发生什么?( *A* ) + + ```js +function bark() { + console.log("Woof!"); +} + +bark.animal = "dog"; + ``` + + - A 什么都不会发生 + - B: *SyntaxError. You cannot add properties to a function this way.* + - C: *undefined* + - D: *ReferenceError* + + > **分析:** + > + > 因为函数也是对象!(原始类型之外的所有东西都是对象) + > + > 函数是一种特殊类型的对象,我们可以给函数添加属性,且此属性是可调用的。 + + + +### 254. 下面代码的输出是什么?( *A* ) + +```js +function Person(firstName, lastName) { + this.firstName = firstName; + this.lastName = lastName; +} + +const member = new Person("Lydia", "Hallie"); +Person.getFullName = () => this.firstName + this.lastName; + +console.log(member.getFullName()); +``` + +- A: *TypeError* +- B: *SyntaxError* +- C: *Lydia Hallie* +- D: *undefined* *undefined* + +> **分析:** +> +> *Person.getFullName* 是将方法添加到了函数身上,因此当我们通过实例对象 *member* 去调用该方法时并不能找到该方法。 + + + +### 255. 下面代码的输出是什么?( *A* ) + +```js +function Person(firstName, lastName) { + this.firstName = firstName; + this.lastName = lastName; +} + +const lydia = new Person("Lydia", "Hallie"); +const sarah = Person("Sarah", "Smith"); + +console.log(lydia); +console.log(sarah); +``` + +- A: *Person { firstName: "Lydia", lastName: "Hallie" }* 和 *undefined* +- B: *Person { firstName: "Lydia", lastName: "Hallie" }* 和 *Person { firstName: "Sarah", lastName: "Smith" }* +- C: *Person { firstName: "Lydia", lastName: "Hallie" }* 和 *{}* +- D: *Person { firstName: "Lydia", lastName: "Hallie" }* 和 *ReferenceError* + +> **分析:** +> +> *lydia* 是调用构造函数后得到的实例对象,拥有 *firstName* 和 *lastName* 属性; +> +> *sarah* 是调用普通函数后得到的返回值,而 *Person* 作为普通函数没有返回值; + + + +### 256. 事件传播的三个阶段是什么?( *D* ) + +- A: 目标 > 捕获 > 冒泡 +- B: 冒泡 > 目标 > 捕获 +- C: 目标 > 冒泡 > 捕获 +- D: 捕获 > 目标 > 冒泡 + + + +### 257. 下面代码的输出是什么?( *C* ) + +```js +function sum(a, b) { + return a + b; +} + +sum(1, "2"); +``` + +- A: *NaN* +- B: *TypeError* +- C: *"12"* +- D: *3* + +> **分析:** +> +> 任意数据类型在跟 *String* 做 + 运算时,都会隐式转换为 *String* 类型。 +> +> 即 *a* 所对应的 *Number* 值 *1*,被隐式转换为了 *String* 值 "1",最终字符串拼接的到 "12"。 + + + +### 258. 下面代码的输出是什么?( *C* ) + +```js +let number = 0; +console.log(number++); +console.log(++number); +console.log(number); +``` + +- A: *1 1 2* +- B: *1 2 2* +- C: *0 2 2* +- D: *0 1 2* + +> **分析:** +> +> ++ 后置时,先输出,后加 *1*;++ 前置时,先加 *1*,后输出; +> +> 第一次输出的值为 0,输出完成后 *number* 加 *1* 变为 *1*。 +> +> 第二次输出,*number* 先加 *1* 变为 *2*,然后输出值 *2*。 +> +> 第三次输出,*number* 值没有变化,还是 *2*。 + + + +### 259. 下面代码的输出是什么?( *B* ) + +```js +function getPersonInfo(one, two, three) { + console.log(one); + console.log(two); + console.log(three); +} + +const person = "Lydia"; +const age = 21; + +getPersonInfo`${person} is ${age} years old`; +``` + +- A: *Lydia* *21* *["", "is", "years old"]* +- B: *["", "is", "years old"]* *Lydia* *21* +- C: *Lydia* *["", "is", "years old"]* *21* + +> **分析:** +> +> 如果使用标记的模板字符串,则第一个参数的值始终是字符串值的数组。 其余参数获取传递到模板字符串中的表达式的值! + + + +### 260. 下面代码的输出是什么?( *C* ) + +```js +function checkAge(data) { + if (data === { age: 18 }) { + console.log("You are an adult!"); + } else if (data == { age: 18 }) { + console.log("You are still an adult."); + } else { + console.log(`Hmm.. You don't have an age I guess`); + } +} + +checkAge({ age: 18 }); + +``` + +- A: *You are an adult!* +- B: *You are still an adult.* +- C: *Hmm.. You don't have an age I guess* + +> **分析:** +> +> 在比较相等性时,原始类型通过它们的值进行比较,而对象通过它们的引用进行比较。 +> +> *data* 和条件中的 *{ age: 18 }* 两个不同引用的对象,因此永远都不相等。 + + + +### 261. 下面代码的输出是什么?( *C* ) + +```js +function getAge(...args) { + console.log(typeof args); +} + +getAge(21); +``` + +- A: *"number"* +- B: *"array"* +- C: *"object"* +- D: *"NaN"* + +> **分析:** +> +> *ES6* 中的不定参数(…*args*)返回的是一个数组。 +> +> *typeof* 检查数组的类型返回的值是 *object*。 + + + +### 262. 下面代码的输出是什么?( *C* ) + +```js +function getAge() { + "use strict"; + age = 21; + console.log(age); +} + +getAge(); +``` + +- A: *21* +- B: *undefined* +- C: *ReferenceError* +- D: *TypeError* + +> **分析:** +> +> *"use strict"* 严格模式中,使用未声明的变量会引发报错。 + + + +### 263. 下面代码的输出是什么?( *A* ) + +```js +const sum = eval("10*10+5"); +``` + +- A: *105* +- B: *"105"* +- C: *TypeError* +- D: *"10\*10+5"* + +> **分析:** +> +> *eval* 方法会将字符串当作 *JavaScript* 代码进行解析。 + + + +### 264. *cool_secret* 可以访问多长时间?( *B* ) + +```js +sessionStorage.setItem("cool_secret", 123); +``` + +- A:永远,数据不会丢失。 +- B:用户关闭选项卡时。 +- C:当用户关闭整个浏览器时,不仅是选项卡。 +- D:用户关闭计算机时。 + +> **分析:** +> +> *sessionStorage* 是会话级别的本地存储,当窗口关闭,则会话结束,数据删除。 + + + +### 265. 下面代码的输出是什么?( *B* ) + +```js +var num = 8; +var num = 10; + +console.log(num); +``` + +- A: *8* +- B: *10* +- C: *SyntaxError* +- D: *ReferenceError* + +> **分析:** +> +> *var* 声明的变量允许重复声明,但后面的值会覆盖前面的值。 + + + +### 266. 下面代码的输出是什么?( *C* ) + +```js +const obj = { 1: "a", 2: "b", 3: "c" }; +const set = new Set([1, 2, 3, 4, 5]); + +obj.hasOwnProperty("1"); +obj.hasOwnProperty(1); +set.has("1"); +set.has(1); +``` + +- A: *false* *true* *false* *true* +- B: *false* *true* *true* *true* +- C: *true* *true* *false* *true* +- D: *true* *true* *true* *true* + + + +### 267. 下面代码的输出是什么?( *C* ) + +```js +const obj = { a: "one", b: "two", a: "three" }; +console.log(obj); +``` + +- A: *{ a: "one", b: "two" }* +- B: *{ b: "two", a: "three" }* +- C: *{ a: "three", b: "two" }* +- D: *SyntaxError* + +> **分析:** +> +> 如果对象有两个具有相同名称的键,则后面的将替前面的键。它仍将处于第一个位置,但具有最后指定的值。 + + + +### 268. 下面代码的输出是什么?( *C* ) + +```js +for (let i = 1; i < 5; i++) { + if (i === 3) continue; + console.log(i); +} +``` + +- A: *1 2* +- B: *1 2 3* +- C: *1 2 4* +- D: *1 3 4* + +> **分析:** +> +> 当 *i* 的值为 *3* 时,进入 *if* 语句执行 *continue*,结束本次循环,立即进行下一次循环。 + + + +### 269. 下面代码的输出是什么?( *A* ) + +```js +String.prototype.giveLydiaPizza = () => { + return "Just give Lydia pizza already!"; +}; + +const name = "Lydia"; + +name.giveLydiaPizza(); +``` + +- A: *"Just give Lydia pizza already!"* +- B: *TypeError: not a function* +- C: *SyntaxError* +- D: *undefined* + +> **分析:** +> +> *String* 是一个内置的构造函数,我们可以为它添加属性。 我们给它的原型添加了一个方法。 原始类型的字符串自动转换为字符串对象,由字符串原型函数生成。 因此,所有字符串(字符串对象)都可以访问该方法! +> +> 当使用基本类型的字符串调用 *giveLydiaPizza* 时,实际上发生了下面的过程: +> +> - 创建一个 *String* 的包装类型实例 +> - 在实例上调用 *substring* 方法 +> - 销毁实例 + + + +### 270. 下面代码的输出是什么?( *B* ) + +```js +const a = {}; +const b = { key: "b" }; +const c = { key: "c" }; + +a[b] = 123; +a[c] = 456; + +console.log(a[b]); +``` + +- A: *123* +- B: *456* +- C: *undefined* +- D: *ReferenceError* + +> **分析:** +> +> 当 *b* 和 *c* 作为一个对象的键时,会自动转换为字符串,而对象自动转换为字符串化时,结果都为 *[Object object]*。因此 *a[b]* 和 *a[c]* 其实都是同一个属性 *a["Object object"]*。 +> +> 对象同名的属性后面的值会覆盖前面的,因此最终 *a["Object object"]* 的值为 *456*。 + + + +### 271. 下面代码的输出是什么?( *B* ) + +```js +const foo = () => console.log("First"); +const bar = () => setTimeout(() => console.log("Second")); +const baz = () => console.log("Third"); + +bar(); +foo(); +baz(); +``` + +- A: *First* *Second* *Third* +- B: *First* *Third* *Second* +- C: *Second* *First* *Third* +- D: *Second* *Third* *First* + +> **分析:** +> +> *bar* 函数中执行的是一段异步代码,按照 *JavaScript* 中的事件循环机制,主线程中的所有同步代码执行完成后才会执行异步代码。因此 *"Second"* 最后输出。 + + + +### 272. 单击按钮时 *event.target* 是什么?( *C* ) + +```html +
+
+ +
+
+``` + +- A: *div* 外部 +- B: *div* 内部 +- C: *button* +- D: 所有嵌套元素的数组 + +> **分析:** +> +> *event.target* 指向的是事件目标,即触发事件的元素。因此点击 *\