key 是虚拟节点的唯一 id,通过可以能够更快更准确找到更新前对应的虚拟节点。
Vue
和React
都是通过 diff 算法对比新旧虚拟树节点差异,然后更新节点。当新旧节点对比不一致时,会根据节点的 key 去找寻旧节点,如果未找到则表明为新的节点,反之会进行复用。
针对这个问题我们应该辩证看待,并不是说书写 key 一定是好的,一定是提升性能的。
如果是简单列表,且列表只是单纯数据展示,无相关状态的更改,则可不使用 key,这样在数据更新重新渲染时会更快,因为会跳过 key 的检索与复用逻辑
不管何时,都要求列表必须带 key,大家阅读过React
都会发现,在 commit 阶段,更新操作通过复用来提升性能,这样虽然会有额外性能开销,但是对比频繁的 DOM 更新,还是能接受的。
首先考虑 map
方法的回调函数参数含义
arr.map(function callback(currentValue[, index[, array]]) { }
然后我们分析 parseInt
参数的含义
parseInt(string, radix)
当遍历到 1 时,map 回调函数的参数分别为:1、0,即 parseInt(1, 0),1 的十进制数 为 1
当遍历到 2 时,map 回调函数的参数分别为:2、1,即 parseInt(2, 1),1 进制数为 2 的数不存在,即为 NaN
当遍历到 3 时,map 回调函数的参数分别为:3、2,即 parseInt(3, 2),2 进制数为 3 的数不存在,即为 NaN
在高频事件(例如浏览器页面滚动)触发时,为了优化提升性能,我们经常使用到防抖与节流。
防抖:触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间
节流:高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率
防抖和节流的区别在于,防抖
是如果在给定 n 秒内再次出发,则会重新计算触发事件,如果你一直触发,则一直重新计算,直至你停下;节流
与防抖的区别是,不管你是否重复触发,我都会在你给定的时间到来时,执行事件函数。
防抖
function debounce(fn, wait) {let timeout = null; // 存放定时器返回值return function() {clearTimeout(timeout); // 每当用户输入时将前一个定时器清除掉timeout = setTimeout(() => {// 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数fn.apply(this, arguments);}, wait);};}
当然,考虑到其他一些优化后,我们最终优化的代码,支持立即执行、返回值
function debounce(func, wait, immediate) {var timeout, result;return function() {var context = this;var args = arguments;if (timeout) clearTimeout(timeout);if (immediate) {// 如果已经执行过,不再执行var callNow = !timeout;timeout = setTimeout(function() {timeout = null;}, wait);if (callNow) result = func.apply(context, args);} else {timeout = setTimeout(function() {func.apply(context, args);}, wait);}return result;};}
节流
时间戳形式实现
function throttle(func, wait) {var context, args;var previous = 0;return function() {var now = +new Date();context = this;args = arguments;if (now - previous > wait) {func.apply(context, args);previous = now;}};}
定时器实现
function throttle(func, wait) {var timeout;var previous = 0;return function() {context = this;args = arguments;if (!timeout) {timeout = setTimeout(function() {timeout = null;func.apply(context, args);}, wait);}};}
最终的优化
function throttle(func, wait, options) {var timeout, context, args, result;var previous = 0;if (!options) options = {};var later = function() {previous = options.leading === false ? 0 : new Date().getTime();timeout = null;func.apply(context, args);if (!timeout) context = args = null;};var throttled = function() {var now = new Date().getTime();if (!previous && options.leading === false) previous = now;var remaining = wait - (now - previous);context = this;args = arguments;if (remaining <= 0 || remaining > wait) {if (timeout) {clearTimeout(timeout);timeout = null;}previous = now;func.apply(context, args);if (!timeout) context = args = null;} else if (!timeout && options.trailing !== false) {timeout = setTimeout(later, remaining);}};return throttled;}
添加取消功能
throttled.cancel = function() {clearTimeout(timeout);previous = 0;timeout = null;};
Set
和 Map
主要的应用场景在于 数据重组 和 数据储存
Set
是一种叫做集合的数据结构,Map 是一种叫做字典的数据结构
在进行图的遍历时,会遇到 深度优先
和 广度优先
。通过字面意思,我们能猜出大概,一个是垂直深入,一个是发散广度。
深度优先
我们可以借助栈保存临时数据,直至在某个分支无下一个元素,则出栈,并进行判断该节点的兄弟节点时候有下个节点,有则遍历,以此类推。
广度优先
借助队列从第一个节点开始,先遍历完所有下一个节点,再一次遍历节点的下一个节点。
深度优先(DFS)
Graph.prototype.dfs = function() {var marked = [];for (var i = 0; i < this.vertices.length; i++) {if (!marked[this.vertices[i]]) {dfsVisit(this.vertices[i]);}}function dfsVisit(u) {let edges = this.edges;marked[u] = true;console.log(u);var neighbors = edges.get(u);for (var i = 0; i < neighbors.length; i++) {var w = neighbors[i];if (!marked[w]) {dfsVisit(w);}}}};
广度优先(BFS)
Graph.prototype.bfs = function(v) {var queue = [],marked = [];marked[v] = true;queue.push(v); // 添加到队尾while (queue.length > 0) {var s = queue.shift(); // 从队首移除if (this.edges.has(s)) {console.log('visited vertex: ', s);}let neighbors = this.edges.get(s);for (let i = 0; i < neighbors.length; i++) {var w = neighbors[i];if (!marked[w]) {marked[w] = true;queue.push(w);}}}};
class
声明变量会提升,但不会初始化赋值。变量进入暂时性死区,类似于 let
、const
声明const p = new People(); // it's okfunction People() {this.bar = 1;}const m = new Man(); // ReferenceError: Foo is not definedclass Man {constructor() {this.foo = 1;}}
class
声明内部会启用严格模式function People() {baz = 1; // it's ok}const p = new People();class Man {constructor() {fol = 1; // ReferenceError: fol is not defined}}const m = new Man();
class
的所有方法(包括静态方法和实例方法)是不可枚举// 引用一个未声明的变量function People() {this.bar = 1;}People.say = function() {return 1;};People.prototype.eat = function() {// ...};const pKeys = Object.keys(Bar); // ['say']const pProtoKeys = Object.keys(Bar.prototype); // ['eat']class Man {constructor() {this.foo = 1;}static say() {return 1;}eat() {// ...}}const mKeys = Object.keys(Man); // []const mProtoKeys = Object.keys(Man.prototype); // []
class
的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]]
,不能使用 new
来调用。function People() {this.bar = 1;}People.prototype.print = function() {console.log(this.bar);};const p = new People();const pPrint = new bar.print(); // it's okclass Man {constructor() {this.foo = 42;}print() {console.log(this.foo);}}const m = new Man();const mPrint = new m.print(); // TypeError: foo.print is not a constructor
new
调用 class
。function People() {this.bar = 1;}const p = People(); // it's okclass Man {constructor() {this.foo = 1;}}const m = Man(); // TypeError: Class constructor Foo cannot be invoked without 'new'
class
内部无法重写类名。function People() {People = 'Pap'; // it's okthis.bar = 1;}const p = new People();// People: 'Pap'// bar: People {bar: 1}class Man {constructor() {this.foo = 42;Man = 'Woman'; // TypeError: Assignment to constant variable}}const m = new Man();Man = 'Fol'; // it's ok
考虑这个问题,我们首先回顾一个概念:事件循环中的宏任务队列和微任务队列。
我们通过简单代码来理解一下
console.log('start');setTimeout(function() {console.log('settimeout');});console.log('end');// 输出顺序:start->end->settimeout
Promise 本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候, 此时是异步操作, 会先执行 then/catch 等,当主栈完成后,才会去调用 resolve/reject 中存放的方法执行,打印 p 的时候,是打印的返回结果,一个 Promise 实例。
console.log('script start');let promise1 = new Promise(function(resolve) {console.log('promise1');resolve();console.log('promise1 end');}).then(function() {console.log('promise2');});setTimeout(function() {console.log('settimeout');});console.log('script end');// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
当 JS 主线程执行到 Promise 对象时,
async function async1() {console.log('async1 start');await async2();console.log('async1 end');}async function async2() {console.log('async2');}console.log('script start');async1();console.log('script end');// 输出顺序:script start->async1 start->async2->script end->async1 end
async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
async function asyncTest() {const ret = await asyncFunction();}
转化为
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {try {var info = gen[key](arg);var value = info.value;} catch (error) {reject(error);return;}if (info.done) {resolve(value);} else {Promise.resolve(value).then(_next, _throw);}}function _asyncToGenerator(fn) {return function() {var self = this,args = arguments;return new Promise(function(resolve, reject) {var gen = fn.apply(self, args);function _next(value) {asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);}function _throw(err) {asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);}_next(undefined);});};}function asyncTest() {return _asyncTest.apply(this, arguments);}function _asyncTest() {_asyncTest = _asyncToGenerator(function*() {const ret = yield asyncFunction();});return _asyncTest.apply(this, arguments);}
LazyMan('Tony');// Hi I am TonyLazyMan('Tony').sleep(10).eat('lunch');// Hi I am Tony// 等待了10秒...// I am eating lunchLazyMan('Tony').eat('lunch').sleep(10).eat('dinner');// Hi I am Tony// I am eating lunch// 等待了10秒...// I am eating dinerLazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food');// Hi I am Tony// 等待了5秒...// I am eating lunch// I am eating dinner// 等待了10秒...// I am eating junk food
这是一个很典型的职责链调用问题,我们使用过 jQuery
应该不会陌生链式调用,但是我们发现现在功能中添加了异步操作,我们可以将需要调用的内容存入队列,然后逐步调用。
class LazyManClass {constructor(name) {this.name = name;this.queue = [];console.log(`Hi I am ${name}`);setTimeout(() => {this.next();}, 0);}sleepFirst(time) {const fn = () => {setTimeout(() => {console.log(`等待了${time}秒...`);this.next();}, time);};this.queue.unshift(fn);return this;}sleep(time) {const fn = () => {setTimeout(() => {console.log(`等待了${time}秒...`);this.next();}, time);};this.queue.push(fn);return this;}eat(food) {const fn = () => {console.log(`I am eating ${food}`);this.next();};this.queue.push(fn);return this;}next() {const fn = this.queue.shift();fn && fn();}}function LazyMan(name) {return new LazyManClass(name);}
add(1); // 1add(1)(2); // 3add(1)(2)(3); // 6add(1)(2, 3); // 6add(1, 2)(3); // 6add(1, 2, 3); // 6
这是一个很典型的函数柯里化问题,使用场景很多,比如惰性求值、函数 bind 实现等,理解这个问题能够让我们更懂闭包问题,本题解法核心其实就是运用闭包暂存参数,待到执行时机,执行函数。
const currying = (fn, // 1) =>(judge = (...args // 2) =>args.length >= fn.length // 3? fn(...args) // 4: (...arg) => judge(...args, ...arg)); // 5
sum
函数柯里化为如题 add
函数,第一步传入的 fn
参数即为 add
fn
,并传入收集的参数概念
HTTP 是运行在 TCP 层之上的,而 HTTPS 则是在 HTTP 和 TCP 层直接多加了一个 SSL/TSL 层,SSL 层向上提供加密和解密的服务,对 HTTP 来说是透明的。
对称加密与非对称加密
加密和解密都使用同一种算法的加密方法,称之为对称加密。加密和解密使用不同的算法,称为非对称加密。
对称加密需要一把钥匙就够了,非对称加密算法需要两把钥匙——公钥和私钥。用公钥加密的密文只能用相应的私钥解开,用私钥加密的密文只能用相应的公钥解开。其中,公钥是公开的,私钥是不对外公开的。
两者的主要区别在于密钥的长度不同,长度越长,相应的加/解密花费的时间就会更长,对称加密使用的密钥长度会短一些。
SSL 结合了这两种加密算法的优点。利用非对称加密算法来协商生成对称加密的密钥,然后之后就用对称加密来进行通信。
Client Hello
握手开始时,总是优先客户端会发送 Client Hello
信息给服务端,主要包含
客户端支持的协议版本
Randomly Generated Data
32 字节长度的随机值,用于之后生成主密钥。
Session Identification
Session ID,第一次连接时为空。
Cipher Suite
客户端支持的加密算法列表,按优先级顺序排列。
Server Hello
接着,服务端收到客户端发来的消息之后,会返回 Server Hello
信息给客户端,告知客户端接下来使用的一些参数
Version Number
通信协议版本
Randomly Generated Data
32 字节长度的随机值,用于之后生成主密钥
Session Identification
Session ID
Cipher Suite
加密算法
Server Certificate
服务端还会带上证书返回给客户端。证书中含有服务端的公钥、网站地址、证书的颁发机构等信息。
客户端收到服务端返回的证书之后,会验证该证书的真实合法性。
Server Key Exchange
这个是可选的,取决于使用的加密算法。主要是携带密钥交换的额外数据。
Server Hello Done
表示服务端已经发送完毕,并等待客户端回应。
Client Key Exchange
客户端使用之前发送给服务端及服务端返回的随机数,生成预主密钥,然后用服务端返回的公钥进行加密。
Change Cipher Spec
告诉服务端,之后的所有信息都会使用协商好的密钥和算法加密
Client Finished
客户端的握手工作已经完成。这条信息是握手过程中所有消息的散列值。
Change Cipher Spec Message
告知客户端,会使用刚刚协商的密钥来加密信息
Server Finished Message
表示服务端的握手工作已经完成
概念
考察数字对象原型拓展
Number.prototype.add = function(n) {return this.valueOf() + n;};Number.prototype.minus = function(n) {return this.valueOf() - n;};
flex
布局
div.parent {display: flex;justify-content: center;align-items: center;}
定位
div.parent {position: relative;}div.child {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);}/* 或者 */div.child {width: 50px;height: 10px;position: absolute;top: 50%;left: 50%;margin-left: -25px;margin-top: -5px;}/* 或 */div.child {width: 50px;height: 10px;position: absolute;left: 0;top: 0;right: 0;bottom: 0;margin: auto;}
grid 布局
div.parent {display: grid;}div.child {justify-self: center;align-self: center;}
Inline-block
div.parent {font-size: 0;text-align: center;&::before {content: '';display: inline-block;width: 0;height: 100%;vertical-align: middle;}}div.child {display: inline-block;vertical-align: middle;}
table
div.parent {display: table;}div.child {display: table-cellvertical-align: middle;text-align: center;}
引入箭头函数有两个方面的作用:更简短的函数并且不绑定 this。箭头函数与普通函数不同之处有:
另外提一点,在使用 React
、Vue
相关框架时,要注意,生命周期函数使用箭头函数会带来一些问题。
Redux 的设计参考了 Flux 的模式,作者希望以此来实现时间旅行,保存应用的历史状态,实现应用状态的可预测。所以整个 Redux 都是函数式编程的范式,要求reducer
是纯函数也是自然而然的事情,使用纯函数才能保证相同的输入得到相同的输入,保证状态的可预测。所以 Redux 有三大原则:
currentState = currentReducer(currentState, action)
currentReducer
就是我们在 createStore
中传入的 reducer
,reducer
用来计算 state 的,所以它的返回值必须是 state
,也就是我们整个应用的状态,而不能是 promise
之类的。
要在 reducer 中加入异步的操作,如果你只是单纯想执行异步操作,不会等待异步的返回,那么在 reducer 中执行的意义是什么。如果想把异步操作的结果反应在 state 中,首先整个应用的状态将变的不可预测,违背 Redux 的设计原则,其次,此时的 currentState 将会是 promise 之类而不是我们想要的应用状态,根本是行不通的。
BFC (block format context)就是块级格式上下文,是页面盒模型布局中的一种 CSS 渲染模式,相当于一个独立的容器,里面的元素和外部的元素相互不影响
创建 BFC 的方式
BFC 的特性
<img src="1.jpg" style="width:480px!important;”>
设最大宽度
max-width: 300px;
运用转换
transform: scale(0.625, 0.625);
box-sizing 设置
box-sizing: border-box;padding: 0 90px;
当然,更硬核的有,😆
width: 300px !important;
for (var i = 0; i < 10; i++) {setTimeout(i => {console.log(i);},1000,i,);}
分析
主要考察对于变量作用域的理解,解决变量作用域即可。
方法一
setTimeout
函数的第三个参数,会作为回调函数的第一个参数传入bind
函数部分执行的特性for (var i = 0; i < 10; i++) {setTimeout(i => {console.log(i);},1000,i,);}
或者
for (var i = 0; i < 10; i++) {setTimeout(console.log, 1000, i);}
或者
for (var i = 0; i < 10; i++) {setTimeout(console.log.bind(null, i), 1000);}
方法二
利用 let
变量的特性 — 在每一次 for
循环的过程中,let
声明的变量会在当前的块级作用域里面(for
循环的 body 体,也即两个花括号之间的内容区域)创建一个文法环境(Lexical Environment),该环境里面包括了当前 for
循环过程中的 i
,
for (let i = 0; i < 10; i++) {setTimeout(() => {console.log(i);}, 1000);}
方法三
利用函数自执行的方式,把当前 for 循环过程中的 i 传递进去,构建出块级作用域。
for (var i = 0; i < 10; i++) {(i => {setTimeout(() => {console.log(i);}, 1000);})(i);}
方法四
纯属娱乐,利用 new Function
或者 eval
for (var i = 0; i < 10; i++) {setTimeout(new Function('console.log(i)')(), 1000);}
npm install
命令.npm
目录里node_modules
目录执行 preinstall
preinstall 钩子此时会执行。
确定依赖模块
确定工程中的首层依赖——dependencies 和 devDependencies 中指定的模块
以工程本身为依赖树根节点,此时会多进程深入遍历节点
获取模块
模块扁平(dedupe)
上一步获取到的依赖树,需要清除重复模块。比如 A 模块依赖于 moment
,B 模块也依赖 moment
。在 npm3
以前会严格按照依赖树的结构进行安装,会造成模块冗余。
从 npm3
开始默认加入了一个 dedupe 的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。
这里需要对重复模块进行一个定义,它指的是模块名相同且 semver 兼容。每个 semver 都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个兼容版本,而不必版本号完全一致,这可以使更多冗余模块在 dedupe 过程中被去掉。
举个例子,假设一个依赖树原本是这样:
node_modules-- foo---- lodash@version1
-- bar ---- lodash@version2
假设 version1 和 version2 是兼容版本,则经过 dedupe 会成为下面的形式:
node_modules-- foo-- bar-- lodash(保留的版本为兼容版本)
假设 version1 和 version2 为非兼容版本,则后面的版本保留在依赖树中:
node_modules-- foo-- lodash@version1-- bar---- lodash@version2
安装模块
更新工程中的 node_modules
,并执行模块中的生命周期函数(preinstall
、install
、postinstall
)。
执行工程自身生命周期
当前 npm 工程如果定义了钩子此时会被执行(按照 install
、postinstall
、prepublish
、prepare
的顺序)。
生成或更新版本描述文件,npm install
过程完成。
JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。
var worker = new Worker('work.js');
向 Web Worker 发送计算请求
worker.postMessage('calculate');worker.postMessage({ method: 'echo', args: ['Work'] });
此时我们需要监听 Web Worker 发送回来的消息
worker.onmessage = function(event) {console.log('Received' + event.data);// doSomething...};
完成以后,关闭
worker.terminate();
self.addEventListener('message',function(e) {self.postMessage('You said: ' + e.data);},false,);// 或者this.addEventListener('message',function(e) {self.postMessage('You said: ' + e.data);},false,);// 或者addEventListener('message',function(e) {self.postMessage('You said: ' + e.data);},false,);self.close();
加载其他脚本
importScripts('script1.js');
关于错误的处理
worker.addEventListener('error', function(event) {// ...});
bind
、apply
实现 bind,要注意几个点
Function.prototype.bind2 = function(context) {if (typeof this !== 'function') {throw new Error('Function.prototype.bind - what is trying to be bound is not callable',);}const self = this;const args = Array.prototype.slice.call(arguments, 1);const fNOP = function() {};const fbound = function() {self.apply(this instanceof self ? this : context,args.concat(Array.prototype.slice.call(arguments)),);};fNOP.prototype = this.prototype;fbound.prototype = new fNOP();return fbound;};
实现 apply,不借助 bind 或 call 实现
Function.prototype.apply2 = function(context, arr) {var context = Object(context) || window;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;};
// A JavaScript class.class MyExampleWebpackPlugin {// Define `apply` as its prototype method which is supplied with compiler as its argumentapply(compiler) {// Specify the event hook to attach tocompiler.hooks.emit.tapAsync('MyExampleWebpackPlugin',(compilation, callback) => {console.log('This is an example plugin!');console.log('Here’s the `compilation` object which represents a single build of assets:',compilation,);// Manipulate the build using the plugin API provided by webpackcompilation.addModule(/* ... */);callback();},);}}
或者像这种基础使用
class HelloWorldPlugin {apply(compiler) {compiler.hooks.done.tap('Hello World Plugin', (stats /* stats is passed as an argument when done hook is tapped. */,) => {console.log('Hello World!');});}}module.exports = HelloWorldPlugin;
使用插件
// webpack.config.jsvar HelloWorldPlugin = require('hello-world');module.exports = {// ... configuration settings here ...plugins: [new HelloWorldPlugin({ options: true })],};
一个示例
class FileListPlugin {apply(compiler) {// emit is asynchronous hook, tapping into it using tapAsync, you can use tapPromise/tap(synchronous) as wellcompiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {// Create a header string for the generated file:var filelist = 'In this build:\n\n';// Loop through all compiled assets,// adding a new line item for each filename.for (var filename in compilation.assets) {filelist += '- ' + filename + '\n';}// Insert this list into the webpack build as a new file asset:compilation.assets['filelist.md'] = {source: function() {return filelist;},size: function() {return filelist.length;},};callback();});}}module.exports = FileListPlugin;
Loader 是 webpack 用于在编译过程中解析各类文件格式并输出,本质是一个 node 模块。
我们自定义一个 Loader,做如下描述的事情:
在 webpack 配置文件中的配置
// webpack.config.jsmodule.exports = {//...module: {rules: [{test: /\.txt$/,use: {loader: path.resolve(__dirname, './txt-loader.js'),options: {name: 'YOLO',},},},],},};
在 txt-loader.js 中定义 loader 相关内容
// txt-loader.jsvar utils = require('loader-utils');module.exports = function(source) {const options = utils.getOptions(this);source = source.replace(/\[name\]/g, options.name);return `export default ${JSON.stringify({content: source,filename: this.resourcePath,})}`;};
打包完毕就能看到生成的 txt 文件中内容已经被更改。
Object.prototype.toString.call()
每一个继承 Object 的对象都有 toString
方法,如果 toString
方法没有重写的话,会返回 [Object type]
,其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString
方法时,会直接返回都是内容的字符串,所以我们需要使用 call 或者 apply 方法来改变 toString 方法的执行上下文。
const an = ['Hello', 'An'];an.toString(); // "Hello,An"Object.prototype.toString.call(an); // "[object Array]"
这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined 。
Object.prototype.toString.call('An'); // "[object String]"Object.prototype.toString.call(1); // "[object Number]"Object.prototype.toString.call(Symbol(1)); // "[object Symbol]"Object.prototype.toString.call(null); // "[object Null]"Object.prototype.toString.call(undefined); // "[object Undefined]"Object.prototype.toString.call(function() {}); // "[object Function]"Object.prototype.toString.call({ name: 'An' }); // "[object Object]"
Object.prototype.toString.call()
常用于判断浏览器内置对象
instanceof
instanceof
的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
。
使用 instanceof
判断一个对象是否为数组,instanceof
会判断这个对象的原型链上是否会找到对应的 Array
的原型,找到返回 true
,否则返回 false
。
[] instanceof Array; // true
但 instanceof
只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true。
[] instanceof Object; // true
Array.isArray()
功能:用来判断对象是否为数组
instanceof 与 isArray
当检测 Array 实例时,Array.isArray
优于 instanceof
,因为 Array.isArray
可以检测出 iframes
var iframe = document.createElement('iframe');document.body.appendChild(iframe);xArray = window.frames[window.frames.length - 1].Array;var arr = new xArray(1, 2, 3); // [1,2,3]// Correctly checking for ArrayArray.isArray(arr); // trueObject.prototype.toString.call(arr); // true// Considered harmful, because doesn't work though iframesarr instanceof Array; // false
Array.isArray()
与 Object.prototype.toString.call()
Array.isArray()
是 ES5 新增的方法,当不存在 Array.isArray()
,可以用 Object.prototype.toString.call()
实现。
if (!Array.isArray) {Array.isArray = function(arg) {return Object.prototype.toString.call(arg) === '[object Array]';};}
var b = {a: function() {console.log(this);},};b.a();// this is b
b.a.call(this); // this is window
function B() {(this.a = function() {console.log(this.b);}),(this.b = 1);}let b = new B();b.a();// this is b object;
var a = 1;this.a;
a == ('1'||'2'||'3') ? false : true
写法进行改进,写出你优化后的方法![1, 2, 3].includes(+a);
或者
!['1', '2', '3'].includes(a + '');
或者
!{ 1: true, 2: true, 3: true }[a];
a.b.c.d
比 a['b']['c']['d']
性能高点,后者还要考虑 [ ]
中是变量的情况
再者,从两种形式的结构来看,显然编译器解析前者要比后者容易些,自然也就快一点。
总之,在项目中,尽量将对象中的属性结构使用, 示例
const obj = { name: 'walker', age: 10 };function test() {const { name, age } = obj;console.log(name);console.log(age);}
无缝轮播的核心是制造一个连续的效果。最简单的方法就是复制一个轮播的元素,当复制元素将要滚到目标位置后,把原来的元素进行归位的操作,以达到无缝的轮播效果。
使用 React
结合 Hooks
实现核心代码片段如下:
useEffect(() => {const requestAnimationFrame =window.requestAnimationFrame ||window.webkitRequestAnimationFrame ||window.mozRequestAnimationFrame;const cancelAnimationFrame =window.cancelAnimationFrame ||window.webkitCancelAnimationFrame ||window.mozCancelAnimationFrame;const scrollNode = noticeContentEl.current;const distance = scrollNode.clientWidth / 2;scrollNode.style.left = scrollNode.style.left || 0;window.__offset = window.__offset || 0;let requestId = null;const scrollLeft = () => {const speed = 0.5;window.__offset = window.__offset + speed;scrollNode.style.left = -window.__offset + 'px';// 关键行:当距离小于偏移量时,重置偏移量if (distance <= window.__offset) window.__offset = 0;requestId = requestAnimationFrame(scrollLeft);};requestId = requestAnimationFrame(scrollLeft);if (pause) cancelAnimationFrame(requestId);return () => cancelAnimationFrame(requestId);}, [notice, pause]);
Link 点击事件 handleClick 部分源码
if (_this.props.onClick) _this.props.onClick(event);if (!event.defaultPrevented && // onClick prevented defaultevent.button === 0 && // ignore everything but left clicks!_this.props.target && // let browser handle "target=_blank" etc.!isModifiedEvent(event) // ignore clicks with modifier keys) {event.preventDefault();var history = _this.context.router.history;var _this$props = _this.props,replace = _this$props.replace,to = _this$props.to;if (replace) {history.replace(to);} else {history.push(to);}}
Link 做了 3 件事情:
[123]()
就不会跳转和刷新页面)redux 是 JavaScript 状态容器
,提供可预测化
的状态管理。
应用中所有的 state 都以一个对象树的形式储存在一个单一的 store 中。 惟一改变 state 的办法是触发 action,一个描述发生什么的对象。 为了描述 action 如何改变 state 树,你需要编写 reducers。
你应该把要做的修改变成一个普通对象,这个对象被叫做 action,而不是直接修改 state。然后编写专门的函数来决定每个 action 如何改变应用的 state,这个函数被叫做 reducer。
redux 有且仅有
一个 store 和一个根级的 reduce 函数(reducer)。随着应用不断变大,你应该把根级的 reducer 拆成多个小的 reducers,分别独立地操作 state 树的不同部分,而不是添加新的 stores。这就像一个 React 应用只有一个根级的组件,这个根组件又由很多小组件构成。
redux-saga 是一个用于管理应用程序 Side Effect(副作用
,例如异步获取数据,访问浏览器缓存等)的 library,它的目标
是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易
。
可以想像为,一个 saga 就像是应用程序中一个单独的线程,它独自负责处理副作用
。 redux-saga 是一个 redux 中间件
,意味着这个线程可以通过正常的 redux action 从主应用程序启动,暂停和取消,它能访问完整的 redux state,也可以 dispatch redux action。
redux-saga 使用了 ES6 的 Generator
功能,让异步的流程更易于读取,写入和测试
。(如果你还不熟悉的话,这里有一些介绍性的链接) 通过这样的方式,这些异步的流程看起来就像是标准同步的 Javascript 代码。(有点像 async/await,但 Generator 还有一些更棒而且我们也需要的功能)。
你可能已经用了 redux-thunk 来处理数据的读取。不同于 redux thunk,你不会再遇到回调地狱了,你可以很容易地测试异步流程并保持你的 action 是干净的。
redux-thunk 的缺点在于 api 层与 store 耦合,优点是可以获取到各个异步操作时期状态的值,比较灵活,易于控制
redux-promise 的优点是 api 层与 store 解耦,缺点是对请求失败,请求中的情形没有很好的处理
redux-saga 的优点是 api 层与 store 解耦,对请求中,请求失败都有完善的处理,缺点是代码量较大
hot-module-replacement-plugin 包给 webpack-dev-server 提供了热更新的能力,它们两者是结合使用的,单独写两个包也是出于功能的解耦来考虑的。