day01:0.1 + 0.2 === 0.3 嘛?为什么?怎么解决?
在js中,0.1 + 0.2不等于0.3。
这个语言现象,其实不止在js里面会有,Java也会有同样的问题。具体原因:是因为语言的底层实现里面,他遵照的是IEEE 754这个标准,用浮点数去表示数字,像0.1/0.2这种数字其实是没有办法精确的用浮点数来表示出来的。
那么在做0.1 + 0.2的加法运算时,其实是做最接近于他们的浮点数表示的加法运算,那么加出来肯定不是0.3。
解决方式:先把被加数转化成整数,做好相加之后再转回小数,这样保留他们的精度。
day02:JS 整数是怎么表示的?
JS中的Number类型其实就是双精度浮点数,它是按IEEE754标准实现的,在这个标准中是用科学计数法来表示一个数的。
那么具体而言的话,双精度浮点数,它其中的一位是用来表示这个数正负的,11位是用来表示科学计数法中的指数,用52位来表示科学计数法中的有效数字。
因此的话,在JS中安全的最大整数就是Math.pow(2, 53)-1,这是因为有效数字有效数位是53位。
Ps: 最小安全整数为 -Math.pow(2, 53)-1
day03:Number() 的存储空间是多大?如果后台发送了一个超过最大限制的数字怎么办?
Number的存储空间是由其科学计数法的11个指数位和52个有效数字位所决定的。比如Number的max_value就是在指数为1023,有效数字位二进制全为1的情况下所代表的数。而Number的min_value则是在指数为-1022,有效数字为除最后一位是二进制1,其余位全为0的情况下所代表的数。
我认为,如果后端返回的数已经超过了JS中的最大安全数,就应该以字符串的形式来表示该数。如果说需要做数值运算的话,应该使用BigInt类型,或者自己实现代码逻辑去直接处理数字字符串。
参考资料:探秘 JavaScript 世界的神秘数字 1.7976931348623157e+308
day04:判断数据类型的方式有哪些?
JS中判断数据类型的方式主要有三种:typeof、instanceof、Object原型下的toString方法。
Object原型下的toString方法可以判断JS中所有的类型,但是他的写法比较繁琐,所以对于typeof能判断的数字、字符串、布尔、undefined、function,我们都会优先简便的用typeof来做判断。
es6出来之后,Array可以直接用Array.isArray方法来判断。
至于instanceof,更确切的说它是用来判断一个对象,它所属的类或者说构造函数的,而且在OOP设计中更倾向用多态而不是instanceof,所以instanceof用的极少。
day05:new 一个函数发生了什么?
new一个函数会发生什么?从语义上来说,是创建一个新对象。
具体的行为是:
JS会创建一个空对象,并且把函数的prototype属性设置为该对象的原型。
并且,以该对象为上下文this执行函数。
最后,在不使用return语法的情况下依然返回这个新对象。
day06:symbol 有什么用处?
据我所知,除了JS,在ruby中也有symbol的概念,代表的是全局唯一的标志符。
利用它全局唯一的特征,我们可以用它来隐藏私有属性,解决命名冲突,或者只是作为ID使用。
day07:NaN 是什么,用 typeof 会输出什么?
NaN是Not A Number的缩写,typeof NaN返回的'number'。
NaN常常会出现在其他类型转数字类型失败的时候,或者是错误的进行了数学计算比方说零除以零,对1做开方的时候。
NaN和其他任何值都不相等,判断一个值是不是NaN,要使用isNaN()方法。
day08:如何判断一个对象是不是空对象?
判断一个对象是不是空对象,主要的问题在于要发现这个对象上可能存在的不可枚举属性和symbol属性。
getOwnPropertyNames可以返回一个对象的可枚举属性和不可枚举属性,getOwnPropertySymbol可以返回对象上的Symbol属性,所以当这两个返回结果length都为0的情况下,我们就可以说一个对象它是空对象。
或者说,我们直接用反射类上面的ownKeys方法一步到位,当它为返回的length为零的情况下,我们也可以判断出该对象为空对象。
day09:如何判断数组类型?
从概念上来说,一个对象他有length属性,有索引,它就可以被认为是一个数组。
但是从检测的判断手段上说,我们在ES6之前一般会用Object原型下的toString方法来判断,ES6之后就有isArray这个API来判断。
其他的判断方法,还有从OOP角度的instanceof以及Array.prototype.isPrototypeOf方法。
day10: 函数中的arguments是数组吗?类数组转数组的方法了解一下?
所谓的数组应该拥有数组接口定义中的属性和方法,而函数中的argument它只拥有属性这一块儿,也就是说他只有length属性和索引,却没有数组定义中的方法。所以arguments它只是类数组,而称不上是一个完整的数组概念。
而把一个像argument这样的类数组转成数组,我们可以用ES6之后提供的...拓展操作符,或者,Array.from()这个API。
day11:instance 如何使用?
所谓“instance”在计算机底层就是内存中的一段地址,所谓“使用”在计算机中无非是读和写,具体而言就是增删改/查。
那么组合一下,我们:
增一个instance就用new一个类/一个构造函数,
删就用自动的垃圾回收机制,
改就是通过修改instance的属性和方法,
查就是用户通过点运算符去查看。
day12:如果一个构造函数,bind了一个对象,用这个构造函数创建出的实例会继承这个对象的属性吗?为什么?
如果一个构造函数,bind了一个对象X,用这个构造函数创建出的实例是不会继承X对象的属性的。
因为bind的作用是指定一个函数的this本地变量,那么,在语言设计者设计bind的时候就没有允许bind去指定构造函数的this,那么也就不会有之后的任何效果。
day13:介绍一下Javascript的执行上下文?
我们可以认为程序的本质就是对于变量的CRUD,对于变量的读和写。
而JS是由函数来划分整个程序的。
所谓的函数执行上下文就是在这个函数中所能够访问进而读和写的那些变量的集合,这个集合中就包含了:
- this变量
- arguments变量
- 这个函数中本地声明的变量
- 这个函数的父亲/祖先函数声明的变量
这些就构成函数的执行上下文。
那么除了函数的执行上下文,还有一类上下文,就是全局执行上下文。
全局执行上下文和函数执行上下文大同小异,没有了父亲/祖先函数声明的变量,arguments变量也没有了,取而代之的是一些内建的对象像Array/Object,以及一些环境特定的全局对象比如window对象。
day14:window.name有什么用?
我们可以借助window.name对同一个窗口中的不同页面之间进行通信.
因为同一个窗口中的不同页面,他们读写的是同一个window.name,而且跨域的两个页面也可以通过window.name进行通讯。
day15:window.window会输出什么?
window.window是全等于window的.
在浏览器当中,window对象挂载了原生的属性/方法,而window对象自身就是原生属性/方法,所以在window上面挂window是逻辑上自洽的。
day16:什么是闭包?如何产生闭包
闭包是有一个定义的,就是说一个函数,你不仅可以在他内部访问他本地声明的数据,你还可以访问非本地声明的数据(比如说外部的词法环境里的数据),那么他这样的一个函数就是一个闭包了。
所以在JS中,你只要在函数体里面去引用外部的变量,或外部的函数,那么,它都是相当于产生了一个闭包。
PS:所谓的词法环境,它里面存放的都是声明,包括了函数声明,变量声明这些东西。
day17:闭包有什么应用场景?
闭包是处理状态的。所有OOP处理的场景,都是闭包的处理场景。
如果一个过程它是无状态的,那么用纯函数就解决问题了。但是如果说一个过程是有状态的,那么在传统的面向对象编程中,我们是把状态存储在对象中的,而JS在这块儿,我们就可以把状态存储在闭包中。
所以通过闭包我们就可以做到原来在OOP编程中做到的,比如说像单例模式/缓存这种东西。
day18:分析一下箭头语法为什么不能当做构造函数
构造函数调用的过程中会创建新的对象,并且把函数的prototype属性设置为新对象的原型。
箭头函数它本身是没有prototype属性的,这是一方面。第二个方面,箭头函数也没有本地的this用来创建新对象。所以,箭头函数既没有新对象又没有原型,这两大对于构造函数最重要的特征,那么因此,他就没有办法用来当作构造函数。
day19:闭包与科里化、偏应用函数的关系
const curry = function(fn){
return function curryFn(...args){
if(args.length<fn.length){
return function(...newArgs){
return curryFn(...args,...newArgs)
}
}else{
return fn(...args)
}
}
}
let add = (a,b,c)=>a+b+c
// 柯里化
add = curry(add)
console.log(add(1)(2)(3)) // 输出 6
偏函数是对普通一般的函数的处理,他改变了函数的调用方式。
原来,一个函数我们调用它给它传入参数,他就立马执行了它的函数体。但是经过偏函数处理的函数,它是通过调用分步分几次的去接收参数,当所有参数到位了之后,才会真正的去执行函数体。
而在这个过程中,我们就需要存储每一次给它传入的参数,这就需要用到闭包了,这就是闭包和偏函数的关系。
而柯里化呢,它只是一种更为特殊的偏函数处理,它也是分步的让函数可以去接受它的参数,但它更严格的限制了你每一次传入参数,只能传一个。
const add = (x,y,z) => x + y + z
const curriedAdd = x => y => z => x + y + z // 柯里化,基于高阶函数,高阶函数基于闭包
20220112更新:
固定了某某参数的函数,就是偏某某参数的函数。比如,我们在业务层面对通用函数又做了层封装,新函数如果没有增加任何逻辑,而仅仅是固定了某些参数,就是原函数的偏函数。
const partial = (f, ...args) =>(...moreArgs)=> f(...args,...moreArgs) // 偏函数,基于高阶函数,高阶函数基于闭包
day20:谈谈闭包与即时函数的应用
立即执行函数,我们就把它简称为即时函数好了。它的使用场景就是我们不需要一个函数一次定义多次调用的特性,我们只需要一个函数作用域来做一些防止命名冲突,防止全局污染这些事情,这就是即时函数的作用。
那么在即时函数中,我们也可以使用闭包。在即时函数中,我们返回一个函数,然后这个函数用到了即时函数的本地变量,那么,这就是闭包和即时函数的联合使用。
day21:如何利用闭包完成类库封装
ES6之后,一个类库,它至少是一个文件吧,作为一个模块文件被import,然后在这个文件里面会有各种各样的逻辑,会有一些本地变量。执行完了这些逻辑之后,最终会有导出。
那么这个过程就是通过导出实现了我们所谓的封装性,也就是说,我们屏蔽了内部的实现而暴露出某一些接口。
如果暴露出的接口里面包括了一些函数,然后函数它大概率是会访问到模块内本地的变量的,那么这无形之中,其实就是使用了闭包。
day22: 词法作用域、块级作用域、作用域链、静态动态作用域
词法作用域、静态动态作用域:
作用域,其实它是一种规则,就是当你一个函数里面要去访问非本地声明的变量的时候,我该怎么去找这个变量。
如果你是到定义这个函数的环境里面去找这个变量,就叫词法作用域。
如果你是到调用这个函数的环境里面去找这个变量,就叫动态作用域。
为什么叫他动态作用域呢?因为调用这个函数,每次调用的位置不一样的话,就会导致你找到的变量,每次都可能是不一样的。因为这种不确定性/动态性,所以叫他动态作用域。其实this上下文机制,算是一种动态作用域。
而词法作用域,它是一种静态作用域。为什么它是静态的呢?因为你如果是根据词法去找这个外部变量的话,每次找到的变量肯定是同一个变量。
作用域链:
作用域练其实也是一种规则,其实无论你是函数套函数的去调用,还是函数套函数的定义,最里面的那个函数,如果里面它要去访问非本地声明的变量的时候,它是由近及远的去找这个变量,这就是作用域链的规则。
块级作用域:
块级作用域是ES6之后新增的作用域,其实块一直都有,像if-else的花括号,或者说for循环的花括号,但是呢,在ES6之前我们在这些块里面声明的变量,其实是声明到了这个块所在的函数的作用域里面。但是现在有了块级作用域之后呢,我们才能真正的把花括号里面声明的变量放在块级作用域里面。
有了块级作用域,其实就相当于JS里面有了生命周期更短的作用域。
day23: 如何用闭包完成模块化(Webpack原理)
感谢Freddy的答案:
库文件首先要保证不能污染全局
打包后最基础版本就是下面这样
(function (modules) {/* 省略函数内容 */})
([
function (module, exports, __webpack_require__) {
/* 模块index.js的代码 */
},
function (module, exports, __webpack_require__) {
/* 模块bar.js的代码 */
}
]);
可以看到,整个打包生成的代码是一个IIFE(立即执行函数)
webpack也控制了模块的module、exports和require
// 1、模块缓存对象
var installedModules = {};
// 2、webpack实现的require
function __webpack_require__(moduleId) {
// 3、判断是否已缓存模块
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 4、缓存模块
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 5、调用模块函数
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 6、标记模块为已加载
module.l = true;
// 7、返回module.exports
return module.exports;
}
// 8、require第一个模块
return __webpack_require__(__webpack_require__.s = 0);
模块数组作为参数传入IIFE函数后,IIFE做了一些初始化工作:
- IIFE首先定义了installedModules ,这个变量被用来缓存已加载的模块。
- 定义了__webpack_require__ 这个函数,函数参数为模块的id。这个函数用来实现模块的require。
- webpack_require 函数首先会检查是否缓存了已加载的模块,如果有则直接返回缓存模块的exports。
- 如果没有缓存,也就是第一次加载,则首先初始化模块,并将模块进行缓存。
- 然后调用模块函数,也就是前面webpack对我们的模块的包装函数,将module、module.exports和__webpack_require__作为参数传入。注意这里做了一个动态绑定,将模块函数的调用对象绑定为module.exports,这是为了保证在模块中的this指向当前模块。
- 调用完成后,模块标记为已加载。
- 返回模块exports的内容。
- 利用前面定义的__webpack_require__ 函数,require第0个模块,也就是入口模块。
require入口模块时,入口模块会收到收到三个参数
function(module, exports, __webpack_require__) {
"use strict";
var bar = __webpack_require__(1);
bar.bar();
}
webpack传入的第一个参数module是当前缓存的模块,包含当前模块的信息和exports;第二个参数exports是module.exports的引用,这也符合commonjs的规范;第三个__webpack_require__ 则是require的实现。
在我们的模块中,就可以对外使用module.exports或exports进行导出,使用__webpack_require__导入需要的模块,代码跟commonjs完全一样。
这样,就完成了对第一个模块的require,然后第一个模块会根据自己对其他模块的require,依次加载其他模块,最终形成一个依赖网状结构。webpack管理着这些模块的缓存,如果一个模块被require多次,那么只会有一次加载过程,而返回的是缓存的内容,这也是commonjs的规范。
原理还是很简单的,其实就是实现exports和require,然后自动加载入口模块,控制缓存模块
webpack IIFE模式实现源代码参考全栈然叔
day24:为什么一定要有块级作用域?
其实真的不需要有块级作用域,至少JS语言的设计者一开始是这么想的,所以他们一开始就没有设计块级作用域。他们觉得只要有函数作用域,全局作用域就够了。
块级作用域的作用和函数级作用域的作用,其实是相同的。我们可以先思考一下,没有函数级作用域会怎么样,只有全局作用域,那简直就不可想象,所有变量都是存在于全局,那么变量的冲突的可能性会变高。
那么,用这个道理其实是套在块级作用域也是一样的。没有块级作用域的话,其实函数里变量名的冲突也会更高,但有了块级作用域之后呢,它能降低这种冲突的可能性,为编程带来方便。
day25: let为什么能够解决循环陷阱
原来会有循环陷阱问题,是因为我们用了var关键字去声明变量,变量就会被放在块所在的函数的作用域里面,这样子的话,你N次循环访问到的其实都是同一个变量。
那么用了let之后,let声明的变量是放在块本地的本地的作用域里面的,那么你N次循环会有N个块,然后,他们会有各自的变量,是互相独立的,这样就不会有循环陷阱的问题。
day26:介绍一下this指向4种形式
this 四种指向性问题,其实说的是一个函数被调用的四种方式:
- 一个函数可以作为函数被调用
- 可以作为方法被调用
- 可以作为构造函数被调用
- 也可以用call apply bind的方式去调用。
这四种调用方式会造成的是会有不同的this指向。如果你是用函数的方式去调用,那么this在严格模式下就是undefined,在普通模式下就是window。如果你是用方法的方式去调用,那么 this 指向的就是这个方法所属的那个对象。如果你是用构造函数去调用,那么this就是一个空对象。如果你是用call apply bind去调用,那么就是你指定this的值了。
day27:React中的事件绑定与箭头函数
React 的事件绑定说的其实就是我们定义了一个函数,然后传给 React 的框架,然后当事件发生的时候呢,React的框架就会调用这个函数。
那么这个函数的 this 指向,正常情况下,其实是由 React 的框架来决定的。React 框架它如果以构造函数的方式去调用这个函数,或者是以方法的方式去调用这个函数,或者以普通函数的方式去调用这个函数,都会有不同的 this 指向的情况发生。
在现实中呢,React 其实就是以 call(undefined) 的方式去调用了我们传给他的这个函数,也就是说this会指向 undefined。
所以,如果我们真的需要在这个回调函数里面使用 this 的话。或者说,如果我们真的需要在这个回调函数里面拥有一个符合我们自己需求的 this 的话。我们作为函数的定义者,其实是有权利以更高的优先级去指定 this 的,那么手段有两个,一个就是通过箭头函数,另外一个就是通过 bind。
day28: 如何实现call和apply、bind?
我们要去实现call apply by这三个函数,就首先是要确定他们的核心职责是什么。他们的核心职责就是要去调用函数,并且是以指定的 this 对象。
那么调用这个函数的时候,肯定会传入一个函数,传入一个指定的对象。那么调用函数是很简单的,实现的难度主要是围绕在怎么去指定 this 上面。那么我们知道指定 this 的话是有三种方式的啊。一种是以构造函数的方式去调用函数,以普通函数的方式去调用函数,还有就是以方法的方式去调用函数,那么,其中只有以方法的方式去调用函数,才可能让我们有机会去让 this 为任意值。
那么我们知道了可以通过以方法调用函数的方式去指定 this 指向。那么具体操作的话,具体实现的话,我们就需要把所调用的函数挂到所指定的对象上,然后以方法的形式去调用,就能实现call apply bind的核心逻辑了。
Function.prototype.mycall = function (context ,...args) {
let key = Symbol();
context[key] = this;
const res = context[key](...args);
delete context[key];
return res;
}
Function.prototype.myapply = function (context,args) {
let key = Symbol();
context[key] = this;
const res = context[key](...args);
delete context[key];
return res;
}
Function.prototype.mybind = function (context,...args) {
const fn =this;
return function F(...newArgs){
return fn.call(context,...args,...newArgs);
}
}