跳到主要内容

· 阅读需 15 分钟

前言

JavaScript 是一项令人印象深刻的技术。不是因为它设计得特别好,也不是因为世界上几乎所有可以访问互联网的设备都执行 JavaScript 程序。相反,JavaScript 令人印象深刻,是因为它的几乎每一个特性都使它成为优化的噩梦,但是它速度很快。

javascript 为什么会执行速度很快呢?这就是我们需要去深入探究的问题。

在本文中,我们将仔细研究不同 JavaScript 引擎用于实现良好运行时性能的一些技术,在研究过程中省略了一些细节,并简化了事情。本文的目标不是让您了解事物的确切运作方式,而是让您了解并理解引擎如何提升其运行时的一些基本知识。

执行模型

当您的浏览器下载 JavaScript 时,其首要任务是让它尽快运行。它通过将代码转换为字节码、虚拟机指令,然后将其移交给理解如何执行它们的解释器或虚拟机来实现。

您可能会问为什么浏览器会将 JavaScript 转换为虚拟机指令而不是实际的机器指令?这是个好问题。事实上,直到最近,V8(Chrome 的 JavaScript 引擎)还一直在做直接转换为机器指令的工作。

特定编程语言的虚拟机通常是更容易编译的目标,因为它与源语言的关系更密切。实际的机器有一个更通用的指令集,因此需要更多的工作来翻译编程语言以很好地处理这些指令。这种困难意味着编译需要更长的时间,同时也意味着 JavaScript 开始执行需要更长的时间。

例如,理解 JavaScript 的虚拟机也可能理解 JavaScript 对象。因此,执行像 object.x 这样的语句所需的虚拟指令可能是一两条指令。一台不了解 JavaScript 对象如何工作的实际机器需要更多的指令来确定 .x 在内存中的位置以及如何获取它。

虚拟机的问题在于它是虚拟的, 它是不存在的。指令不能直接执行,必须在运行时解释。解释代码总是比直接执行代码慢。

这里有一个问题需要权衡。需要在更快的编译时间与更快的运行时间中做一个选择。在许多情况下,更快的编译是一个很好的权衡。用户不太可能关心单个按钮的点击是否需要 20 或 40 毫秒的执行时间,尤其是当按钮只被按下一次时。快速编译 JavaScript,即使生成的代码执行速度较慢,也会让用户更快地查看页面并与页面交互。

有些情况在计算上是昂贵的。诸如游戏、语法高亮之类的场景。在这种情况下,编译和执行机器指令的时间加起来可能会减少总执行时间。那么 JavaScript 是如何处理这些情况的呢?

经常被执行的代码

每当 JavaScript 引擎检测到某个函数执行了很多次时,它就会将该函数交给优化编译器。该编译器将虚拟机指令翻译成实际的机器指令。更重要的是,由于该函数已经运行了多次,优化编译器可以根据之前的运行做出一些假设。换句话说,它可以执行推测优化以生成更快的代码。

如果这些推测后来被证明是错误的,会发生什么? JavaScript引擎可以简单地删除错误的函数,并还原为使用未优化版本。一旦该函数再运行几次,它就可以尝试再次将其传递给优化编译器,这一次它会提供更多可用于推测优化的信息。

既然我们知道频繁运行的函数在优化过程中使用来自先前执行的信息,接下来要探索的是这是什么类型的信息。

翻译问题

JavaScript 中的几乎所有东西都是对象。不幸的是,JavaScript 对象很难让机器处理。让我们看看下面的代码:

function addFive(obj) {
return obj.method() + 5;
}

将函数转换为机器指令非常简单,就像从函数返回一样。但是机器不知道对象是什么,比如访问obj的method属性需要怎么翻译呢?

如果知道 obj 是什么样子会很有帮助,但在 JavaScript 中我们永远无法确定。任何对象都可以添加或删除方法属性。即使method确实存在,我们实际上也不能确定它是否是一个函数,更不用说调用它之后的返回值了。

让我们尝试将上述代码转换为没有对象的 JavaScript 子集,来了解转换为机器指令可能是什么样的。

首先,我们需要一种表示对象的方法。我们还需要一种从其中检索值的方法。在机器代码中支持数组是比较的,所以我们可能会使用这样的表示:

// An object like { method: function() {} }
// could be represented as:
// [ [ "method" ], // property names
// [ function() {} ] ] // property values

function lookup(obj, name) {
for (var i = 0; i < obj[0].length; i++) {
if (obj[0][i] === name) return i;
}
return -1;
}

参考上述的表示,我们可以尝试对 addFive 进行一个简单的实现

function addFive(obj) {
var propertyIndex = lookup(obj, "method");
var property = propertyIndex < 0
? undefined
: obj[1][propertyIndex];

if (typeof(property) !== "function") {
throw NotAFunction(obj, "method");
}
var callResult = property(/* this */ obj);
return callResult + 5;
}

当然,这在 obj.method() 返回的不是数字的情况下不能运行,所以我们需要稍微调整一下实现:

function addFive(obj) {
var propertyIndex = lookup(obj, "method");
var property = propertyIndex < 0
? undefined
: obj[1][propertyIndex];

if (typeof(property) !== "function") {
throw NotAFunction(obj, "method");
}
var callResult = property(/* this */ obj);
if (typeof(callResult) === "string") {
return stringConcat(callResult, "5");
} else if (typeof(callResult !== "number") {
throw NotANumber(callResult);
}

return callResult + 5;
}

这是能运行的,但我希望很明显,如果我们能提前知道 obj 的结构是什么,以及方法的类型是什么,那么这段代码可以跳过几个步骤。

隐藏类

主流的 JavaScript 引擎都以某种方式跟踪对象是什么样的呢?在 Chrome 中,这个概念被称为隐藏类。

让我们从以下代码片段开始:

var obj = {}; // empty object
obj.x = 1; // shape has now changed to include a `x` property
obj.toString = function() { return "TODO"; }; // shape changes
delete obj.x; // shape changes again

如果我们将其转换为机器指令,我们将如何在添加和删除新属性时跟踪对象的样子?如果我们使用上一个示例将对象表示为数组的想法,它可能看起来像这样:

var emptyObj__Class = [ 
null, // No parent hidden class
[], // Property names
[] // Property types
];

var obj = [
emptyObj__Class, // Hidden class of `obj`
[] // Property values
];

var obj_X__Class = [
emptyObj__Class, // Contains same properties as empty object
["x"], // As well as one property called `x`
["number"] // Where `x` is a number
];

obj[0] = obj_X__Class; // Shape changes
obj[1].push(1); // value of `x`

var obj_X_ToString__Class = [
obj_X__Class, // Contains same properties as previous shape
["toString"], // And one property called `toString`
["function"] // Where `toString` is a function
];

obj[0] = obj_X_ToString__Class; // shape change
obj[1].push(function() { return "TODO"; }); // `toString` value

var obj_ToString__Class = [
null, // Starting from scratch when deleting `x`
["toString"],
["function"]
];

obj[0] = obj_ToString__Class;
obj[1] = [obj[1][1]];

如果我们要生成这样的虚拟机指令,我们现在就有了一种方法来跟踪对象在任何给定时间的样子。然而,这本身并不能真正帮助我们。我们需要将这些信息存储在有价值的地方。

内联缓存

每当 JavaScript 代码对对象执行属性访问时,JavaScript 引擎都会将该对象的隐藏类以及查找结果(属性名称到索引的映射)存储在缓存中。这些缓存被称为内联缓存,它们有两个重要目的:

  • 在执行字节码时,如果所涉及的对象具有缓存中的隐藏类,它们会加速属性访问。
  • 在优化期间,它们包含有关访问对象属性时所涉及的对象类型的信息,这有助于优化编译器生成特别适合这些类型的代码。

内联缓存对它们存储信息的隐藏类的数量有限制。这可以保留内存,但也确保在缓存中执行查找速度很快。如果从内联缓存中检索索引比从隐藏类中检索索引花费的时间更长,则缓存没有任何用处。

据我所知, Chrome在中,内联缓存最多会跟踪 4 个隐藏类。在此之后,内联缓存将被禁用,信息将存储在全局缓存中。全局缓存的大小也有限制,一旦达到限制,新条目将覆盖旧条目。

为了最好地利用内联缓存并帮助优化编译器,应该尝试编写仅对单一类型的对象执行属性访问的函数。不仅如此,生成的代码的性能将是次优的

内联

一种单独且重要的优化是内联。简而言之,这种优化用被调用函数的实现代替了函数调用。举个例子:

function map(fn, list) {
var newList = [];
for (var i = 0; i < list.length; i++) {
newList.push(fn(list[i]));
}

return newList;
}

function incrementNumbers(list) {
return map(function(n) { return n + 1; }, list);
}

incrementNumbers([1, 2, 3]); // returns [2, 3, 4]

内联后,代码最终可能看起来像这样:

function incrementNumbers(list) {
var newList = [];
var fn = function(n) { return n + 1; };
for (var i = 0; i < list.length; i++) {
newList.push(fn(list[i]));
}
return newList;
}

incrementNumbers([1, 2, 3]); // returns [2, 3, 4]

这样做的一个好处是删除了函数调用。更大的好处是 JavaScript 引擎现在可以更深入地了解函数的实际作用。基于这个新版本,JavaScript 引擎可能会决定再次执行内联:

function incrementNumbers(list) {
var newList = [];
for (var i = 0; i < list.length; i++) {
newList.push(list[i] + 1);
}

return newList;
}

incrementNumbers([1, 2, 3]); // returns [2, 3, 4]

另一个函数调用已被删除。更重要的是,优化器现在可能会推测 incrementNumbers 只会以数字列表作为参数被调用。它还可能决定内联 incrementNumbers([1, 2, 3]) 调用本身,并发现 list.length 为 3,这又可能导致:

var list = [1, 2, 3];
var newList = [];
newList.push(list[0] + 1);
newList.push(list[1] + 1);
newList.push(list[2] + 1);
list = newList;

简而言之,内联可以实现跨函数边界无法执行的优化。

但是,可以内联的内容是有限的。由于代码重复,内联会导致更大的函数,这需要额外的内存。 JavaScript 引擎对一个函数在完全跳过内联之前可以达到的大小有一个预算。

一些函数调用也很难内联。特别是当一个函数作为参数传入时。

此外,作为参数传递的函数很难内联,除非它总是同一个函数。虽然这可能会让您觉得这是一件奇怪的事情,但由于内联,最终可能会出现这种情况。

结论

JavaScript 引擎有许多提高运行时性能的技巧,比这里介绍的要多得多。但是,本文中描述的优化适用于大多数浏览器,并且很容易验证它们是否被应用。因此,当我们尝试提高 Elm 的运行时性能时,我们将主要关注这些优化。

参考

What’s up with monomorphism Shapes and inline caches Optimizing prototypes

· 阅读需 11 分钟

前言

本文主要编制了一份清单,列出了在项目中可能会需要用到的的七个Hooks。

1. useToggle

这个hook很常见,它用于在 true 和 false 之间切换布尔值。当我们想要显示/隐藏模式或打开/关闭侧边菜单时,它很有用。这个hook的基本版本如下所示:

实现v1

// useToggle.jsx
import { useState, useCallback } from 'react';

const useToggle = (initialValue = false) => {
const [state, setState] = useState(initialValue);

const toggle = useCallback(() => {
setState((state) => !state);
}, []);

return [state, toggle];
};

export default useToggle;

案例1

import useToggle from './useToggle';
const App = () => {
const [show, toggleShow] = useToggle();
return (
<Modal show={show} onClose={toggleShow}>
<h1>Hello there</h1>
</Modal>
);
}

当我们想要显示/隐藏表中一行的模式时,可以稍微修改此hook以用于用例。我添加了一个 customToggle 方法,该方法将值设置为给定值,而不是切换先前的状态值。

实现v2

// useToggle.jsx modified

import { useState, useCallback } from 'react';

const useToggle = (initialValue = false) => {
const [state, setState] = useState(initialValue);

const toggle = useCallback(() => {
setState((state) => !state);
}, []);

const customToggle = useCallback((value) => {
setState(value);
}, []);

return [state, toggle, customToggle];
};

export default useToggle;

案例2

假设我们在一个表格中有一堆行,我们想提供一个删除行的选项。单击删除按钮应打开一个确认模式。 对于这种类型的功能,我们需要状态中的两个变量。首先,保存一个布尔值来确定是否显示删除确认模式,其次保存必须显示删除模式的行 ID。 使用这个hook,我们可以用一个状态变量来完成。这是如何做到的:

import useToggle from './useToggle';

// initial data
const rows = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' }
];

const App = () => {
// using hook
const [delModal, toggleDelModal, setDelModalCustom] = useToggle();

return (
<div>
<table>
{rows.map(row => (
<tr>
<td>{row.id}</td>
<td>{row.name}</td>
<td onClick={() => setDelModalCustom(row.id)}>Delete</td>
</tr>
))}
</table>

<DeleteModal
show={Boolean(delModal)}
onClose={toggleDelModal}
itemToDelete={delModal} // it will hold the current row id
>
<h1>
Are you sure you want to delete row with id {delModal}
</h1>
</DeleteModal>
</div>
);
}

2. usePageBottom

使用此hook,你可以确定用户是否已滚动到页面底部。非常适合无限滚动的应用程序,当用户滚动到页面底部时,你需要获取更多数据。

// usePageBottom.jsx

import { useState, useEffect } from 'react';

const usePageBottom = () => {
const [reachedBottom, setReachedBottom] = useState(false);

// event handler for determining if the user reached bottom
const handleScroll = () => {
const offsetHeight = document.documentElement.offsetHeight;
const innerHeight = window.innerHeight;
const scrollTop = document.documentElement.scrollTop;

// if current scroll from bottom is less than equal to 10px
const reachingBottom = offsetHeight - (innerHeight + scrollTop) <= 10;

setReachedBottom(reachingBottom);
};

// effect for binding event listener on window scroll
useEffect(() => {
window.addEventListener('scroll', handleScroll);

return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [handleScroll]);

return reachedBottom;
}

案例

import usePageBottom from './usePageBottom';
const App = (props) => {
// hook usage
const scrolledBottom = usePageBottom();
return (
<div className="App" style={{ height: '150vh' }}>
<h1>This is app</h1>
<p>Scrolled to bottom {scrolledBottom}</p>
</div>
);
}

节流版本(可选)

减少像窗口滚动事件这样多次触发的事件的通知是最佳实践,我们可以使用节流设置事件侦听器。我正在使用 lodashthrottle;你也可以使用去debounce

// usePageBottom with throttle

import { useState, useEffect, useMemo } from 'react';
import { throttle } from 'lodash';

const usePageBottom = () => {
const [reachedBottom, setReachedBottom] = useState(false);

// event handler for determining if the user reached bottom
const handleScroll = useMemo(() => {
return throttle(() => {
const offsetHeight = document.documentElement.offsetHeight;
const innerHeight = window.innerHeight;
const scrollTop = document.documentElement.scrollTop;

// if current scroll from bottom is less than equal to 10px
const reachingBottom = offsetHeight - (innerHeight + scrollTop) <= 10;

setReachedBottom(reachingBottom);
}, 1000);
}, []);

// effect for binding event listener on window scroll
useEffect(() => {
window.addEventListener('scroll', handleScroll);

return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [handleScroll]);

return reachedBottom;
}

3. useElementBottom

就像页面底部一样,我们也可以确定用户是否已经滚动到元素的底部。在这个hook中,传递了对元素的引用,因此我们可以使用元素的高度和当前滚动位置来确定元素是否滚动到底部。

// useElementBottom.jsx

import { useState, useEffect, useMemo } from 'react';
import { throttle } from 'lodash';

const useElementBottom = (element) => {
const [reachedBottom, setReachedBottom] = useState(false);

// event handler for determining if the user reached bottom
const handleScroll = useMemo(() => {
return throttle(() => {
const { current } = element; // current holds the reference to element

// if current scroll from bottom is less than equal to 10px
const scrollBottom =
current.scrollHeight - current.scrollTop - current.clientHeight;

const reachingBottom = scrollBottom <= 10;
setReachedBottom(reachingBottom);
}, 1000);
}, []);

// effect for binding event listener on element scroll
useEffect(() => {
const { current } = element;
current.addEventListener('scroll', handleScroll);

return () => current.removeEventListener('scroll', handleScroll);
}, []);

return reachedBottom;
};

export default useElementBottom;

案例

import useElementBottom from './useElementBottom';
const App = (props) => {
const element = useRef();
// hook usage
const scrolledBottom = useElementBottom(element);
return (
<div ref={element} style={{ height: '150vh' }}>
<h1>This is app</h1>
<p>Scrolled to bottom {scrolledBottom}</p>
</div>
);
}

4. usePrevious

我们可以制作一个自定义hook来获取 prop 或 state 的先前值。使用 React 类组件,可以使用 componentDidUpdate 生命周期来获取之前的 prop 和 state 值。对于功能组件,我们可以使用自定义hook来完成,如下所示:

// usePrevious.jsx

import { useEffect, useRef } from 'react';

const usePrevious = (value) => {
const ref = useRef();

// store current value in ref
useEffect(() => {
ref.current = value;
}, [value]);

// return previous value (happens before update in useEffect above)
return ref.current;
};

export default usePrevious;

我们使用 useRef hook来存储先前的值。我们的hook在 useEffect 中更新引用对象之前返回先前的值。

import usePrevious from './usePrevious';

const App = () => {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);

return (
<div>
<h1>Now: {count}, before: {prevCount}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

5. useWindowSize

这个hook返回窗口的宽度和高度。我们在调整窗口大小时设置了一个受限制的 set 事件侦听器,以便我们每次都能获得正确的宽度和高度。

// useWindowSize

import { useState, useEffect, useCallback } from 'react';
import { throttle } from 'throttle';

const useWindowSize = () => {
const [size, setSize] = useState({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
});

// throttled set listener that runs on window resize
const throttledSetResizeListner = useCallback(
throttle(() => {
// set size in state
setSize({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
});
}, 1000),
[]
);

// effect for binding resize event on window
useEffect(() => {
// add throtelled set window resize event listener
window.addEventListener('resize', throttledSetResizeListner);

// remove throttled set window resize event listener
return () =>
window.removeEventListener('resize', throttledSetResizeListner);
}, [throttledSetResizeListner]);

return size;
};

export default useWindowSize;

案例

当你想根据窗口的宽度或高度在你的 JS 代码中做一些事情时,它很有用。

import useWindowSize from './useWindowSize';
const App = () => {
const { innerWidth, innerHeight } = useWindowSize();
return (
<div>
{innerWidth <= 768 ?
'I am on small screen' : 'I am on large screen'}
</div>
);
}

6. useEventListener

使用此hook可以轻松地将事件侦听器绑定到窗口或窗口元素。在hook的effect中,我们检查元素是否是来自 useRef 的引用。如果没有,我们将事件侦听器添加到全局窗口对象。

import { useEffect } from 'react';

const useEventListener = (event, handler, referencedElement) => {
// effect for binding event handler to the element
useEffect(() => {
const element = referencedElement?.current || window;

const isSupported = element && element.addEventListener;

if (!isSupported) return;

// bind event to the element
element.addEventListener(event, handler);

return () => element.removeEventListener(event, handler);
}, [referencedElement, event, handler]);

return;
};

export default useEventListener;
import useEventListener from './useEventListener';

const App = (props) => {
const element = useRef();
useEventListener('mouseover', handler, element);
const handler => () => {
console.log('Event triggered');
}
return (
<div ref={element}>
<h1>This is app</h1>
<div>
);
}

我们还可以使用相同的hook将事件添加到窗口

import useEventListener from './useEventListener';
const App = (props) => {
// adding event listener on window scroll
useEventListener('scroll', handler);
const handler => () => {
console.log('Event triggered');
}
return (
<div>
<h1>This is app</h1>
<div>
);
}

7. useLocalStorageState

有时,可能需要在本地存储和状态之间同步一个值。这个hook的作用完全一样。我们正在使用 useState 并检查指定的键是否在本地存储中具有某个值,以便我们可以将其用作初始值。这允许我们在页面刷新时保持值与状态同步。

// useLocalStorageState

import { useState, useEffect } from 'react';

const useLocalStorageState = (key, defaultValue) => {
const [value, setValue] = useState(() => {
let val;

try {
// if there is a value in local storage for given key, set it as initial state
val = JSON.parse(localStorage.getItem(key) || String(defaultValue));
} catch (error) {
// otherwise, set default value as initial state
val = defaultValue;
}

return val;
});

// effect to update local storage when state changes
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [value]);

return [value, setValue];
};

export default useLocalStorageState;

当值更新时,会运行一个effect ,更新本地存储中的值。

案例

假设你想在本地存储中存储用户主题首选项

import useLocalStorageState from './useLocalStorageState';
const App = (props) => {
// setting default theme to light
const [theme, setTheme] = useLocalStorageState('theme', 'light');
return (
<div>
<p>Current theme is {theme}</p>
<button onClick={() => setTheme('dark')}>
Change theme to dark
</button>
</div>
);
}

当你点击更改主题按钮时,状态和本地存储中的值都会更新。页面刷新后,你将获得存储在本地存储中的最后一个值。

结语

感谢你阅读到最后。我希望这些自定义hook对你有用。

· 阅读需 8 分钟

前言

编写简短、简洁和干净的 JavaScript 代码的技巧😎 JavaScript 有很多很酷的特性,大多数初学者和中级开发人员都不知道。本节挑选了 10 个在日常 JavaScript 项目中经常使用的技巧。

1. 有条件的向对象中添加属性

我们可以使用扩展运算符 ... 来有条件地向 JavaScript 对象快速添加属性。

const condition = true;
const person = {
id: 1,
name: 'John Doe',
...(condition && { age: 16 }),
};

如果每个操作数的计算结果都为true, && 运算符将返回最后计算的表达式。因此返回一个对象 { age: 16 },然后将其作为 person 对象的一部分。

如果条件为 false,则 JavaScript 将执行以下操作:

const person = {
id: 1,
name: 'John Doe',
...(false), // evaluates to false
};
// spreading false has no effect on the object
console.log(person); // { id: 1, name: 'John Doe' }

2. 检查一个属性是否存在于一个对象中

我们可以使用in关键字来检查 JavaScript 对象中是否存在属性

const person = { name: 'John Doe', salary: 1000 };
console.log('salary' in person); // returns true
console.log('age' in person); // returns false

3. 对象中的动态属性名称

使用动态键设置对象属性很简单。只需使用 ['key_name'] 符号添加属性

const dynamic = 'flavour';
var item = {
name: 'Biscuit',
[dynamic]: 'Chocolate'
}
console.log(item); // { name: 'Biscuit', flavour: 'Chocolate' }

同样的技巧也可用于使用动态键引用对象属性:

const keyName = 'name';
console.log(item[keyName]); // returns 'Biscuit'

4. 使用动态键进行对象解构

你可能知道你可以解构一个变量并立即用 : 符号重命名它。但是你知道当你不知道键名或键名是动态的时,你也可以解构对象的属性吗? 首先,让我们看看如何在解构(用别名解构)时重命名变量。

const person = { id: 1, name: 'John Doe' };
const { name: personName } = person;
console.log(personName); // returns 'John Doe'

现在,让我们使用动态键来解构属性:

const templates = {
'hello': 'Hello there',
'bye': 'Good bye'
};
const templateName = 'bye';
const { [templateName]: template } = templates;
console.log(template) // returns 'Good bye'

5. ?? 运算符

??当你要检查变量是 null 还是 undefined 时,运算符很有用。当其左侧操作数为空或未定义时,它返回右侧操作数,否则返回其左侧操作数。

const foo = null ?? 'Hello';
console.log(foo); // returns 'Hello'
const bar = 'Not null' ?? 'Hello';
console.log(bar); // returns 'Not null'
const baz = 0 ?? 'Hello';
console.log(baz); // returns 0

在第三个示例中,返回 0 是因为即使 0 在 JavaScript 中被认为是假的,但它不是 null 或未定义的。你可能认为我们可以使用 ||运算符在这里,但这两者之间存在差异:

const cannotBeZero = 0 || 5;
console.log(cannotBeZero); // returns 5
const canBeZero = 0 ?? 5;
console.log(canBeZero); // returns 0

6. ?. 可选链

我们都可能曾经遇到过TypeError:无法读取 null 的属性“foo”之类的错误。这对每个 JavaSript 开发人员来说都是头疼的问题。引入了可选链就是为了解决这个问题。让我们来看看:

const book = { id:1, title: 'Title', author: null };
// normally, you would do this
console.log(book.author.age) // throws error
console.log(book.author && book.author.age); // returns null (no error)
// with optional chaining
console.log(book.author?.age); // returns undefined
// or deep optional chaining
console.log(book.author?.address?.city); // returns undefined

你还可以使用具有以下功能的可选链:

const person = {
firstName: 'Haseeb',
lastName: 'Anwar',
printName: function () {
return `${this.firstName} ${this.lastName}`;
},
};
console.log(person.printName()); // returns 'Haseeb Anwar'
console.log(persone.doesNotExist?.()); // returns undefined

7. 使用 !! 的布尔转换符

!! 运算符可用于将表达式的结果快速转换为布尔值 truefalse。就是这样:

const greeting = 'Hello there!';
console.log(!!greeting) // returns true
const noGreeting = '';
console.log(!!noGreeting); // returns false

8. 字符串和整数转换

使用 + 运算符快速将字符串转换为数字,如下所示:

const stringNumer = '123';
console.log(+stringNumer); // returns integer 123
console.log(typeof +stringNumer); // returns 'number'

要将数字快速转换为字符串,请使用 + 运算符后跟空字符串 "":

const myString = 25 + '';
console.log(myString); // returns '25'
console.log(typeof myString); // returns 'string'

这些类型转换非常方便,但它们的清晰度和代码可读性较差。因此,在生产中使用它们之前,你可能需要考虑一下。不过可以用才code golf中。

9. 检查数组中的假值

你熟悉 filter、some 和 every 数组方法。但你也应该知道,你可以仅使用布尔方法来测试真值:

const myArray = [null, false, 'Hello', undefined, 0];
// filter falsy values
const filtered = myArray.filter(Boolean);
console.log(filtered); // returns ['Hello']
// check if at least one value is truthy
const anyTruthy = myArray.some(Boolean);
console.log(anyTruthy); // returns true
// check if all values are truthy
const allTruthy = myArray.every(Boolean);
console.log(allTruthy); // returns false

这是它的工作原理。众所周知,这些数组方法采用回调函数,因此我们将布尔值作为回调函数传递。 Boolean 本身接受一个参数并根据参数的真实性返回truefalse。所以我们可以这样说:

myArray.filter(val => Boolean(val));

是不是和这个一样:

myArray.filter(Boolean);

10.展平数组

原型 Array 上有一个方法 flat 可以让你从数组的数组中创建一个数组:

const myArray = [{ id: 1 }, [{ id: 2 }], [{ id: 3 }]];
const flattedArray = myArray.flat();
// returns [ { id: 1 }, { id: 2 }, { id: 3 }

你还可以定义一个深度级别,指定嵌套数组结构应展平的深度。例如:

const arr = [0, 1, 2, [[[3, 4]]]];
console.log(arr.flat(2)); // returns [0, 1, 2, [3,4]]

结语

感谢你阅读到最后。希望这些技巧对你日常开发有用。