5 个提升你 JS 编码水平的实例
- 作者:陈大鱼头
- github: KRISACHAN (opens new window)
虽然 2020 的今天,各种前端框架、工具林立,而这些框架跟工具也帮我们提前解决了不少麻烦的问题,但是工具始终是工具,扎实的基本功才是最核心的,现在一起来通过几个实际的代码片段来提高我们原生 JS 的编码水平。
# 判断数据类型
首先来提问一个:typeof
是否能正确判断类型?
答案是:不可以,因为由于历史原因,在判断原始类型时,typeof null
会等于object
。而且对于对象来说,除了函数,都会转换成object
。例子如下:
typeof 1; // 'number'
typeof "1"; // 'string'
typeof null; //
typeof []; // 'object'
typeof {}; // 'object'
typeof window.alert; // 'function'
2
3
4
5
6
7
再来提问一个,instanceof
是否能正确判断类型?
答案是:还是不可以,虽然instanceof
是通过原型链来判断的,但是对于对象来说,Array
也会被转换成Object
,而且也不能区分基本类型string
和boolean
。例如:
function Func() {}
const func = new Func();
console.log(func instanceof Func); // true
const obj = {};
const arr = [];
obj instanceof Object; // true
arr instanceof Object; // true
arr instanceof Array; // true
const str = "abc";
const str2 = new String("abc");
str instanceof String; // false
str2 instanceof String; // true
2
3
4
5
6
7
8
9
10
11
12
13
14
所以该怎么办呢?
这时候我们可以使用:Object.prototype.toString.call()
所以为什么?
因为每个对象都有一个toString()
方法,当要将对象表示为文本值或以预期字符串的方式引用对象时,会自动调用该方法。默认情况下,从Object
派生的每个对象都会继承toString()
方法。如果此方法未在自定义对象中被覆盖,则toString()
返回[Object type]
,其中type
是对象类型。所以就有以下例子:
Object.prototype.toString.call(new Date()); // [object Date]
Object.prototype.toString.call("1"); // [object String]
Object.prototype.toString.call(1); // [object Numer]
Object.prototype.toString.call(undefined); // [object Undefined]
Object.prototype.toString.call(null); // [object Null]
2
3
4
5
所以综合上述知识点,我们可以封装出以下通用类型判断方法:
var type = function(data) {
var toString = Object.prototype.toString;
var dataType = data instanceof Element
? 'element' // 为了统一DOM节点类型输出
: toString
.call(data)
.replace(/\[object\s(.+)\]/, ''$1')
.toLowerCase()
return dataType
}
2
3
4
5
6
7
8
9
10
使用方法如下:
type("a"); // string
type(1); // number
type(window); // window
type(document.querySelector("h1")); // element
2
3
4
# 通用的数组/类数组对象封装
如果我们使用 ES5/ES6+的数组 API,很容易就能够对数组进行各类的循环操作,但是如果我们要循环一个类数组对象呢?
例如NodeList
。直接循环是会报错的:
document.querySelectorAll("div").map(e => e); // Uncaught TypeError: document.querySelectorAll(...).map is not a function
当然我们可以用扩展运算符:
[...document.querySelectorAll("div")].map(e => e);
那如果我们不用扩展运算符呢?
那么我们就可以利用call
的特性,将NodeList
里的元素一个一个的插入到数组中,例子如下:
var listMap = function(array, type, fn) {
return !fn ? array : Array.prototype[type]["call"](array, fn);
};
2
3
使用方法如下:
var divs = document.querySelectorAll("div");
listMap(divs, "forEach", function(e) {
console.log(e);
});
2
3
4
# 获取 dom 元素节点的偏移量
如果有用过jQuery
的童鞋,就一定不会忘记$('').offset()
这个 api 的强大功能,这个 api 可以轻易获取元素的偏移量,那么如果我们不用jQuery
该怎么实现呢?
我们先来看看例子:
var getOffset = function(el) {
var scrollTop =
el.getBoundingClientRect().top +
document.body.scrollTop +
document.documentElement.scrollTop;
var scrollLeft =
el.getBoundingClientRect().left +
document.body.scrollLeft +
document.documentElement.scrollLeft;
return {
top: scrollTop,
left: scrollLeft
};
};
2
3
4
5
6
7
8
9
10
11
12
13
14
首先我们先来看getBoundingClientRect()
这个方法。
getBoundingClientRect()
方法返回元素的大小及其相对于视口的位置。返回值是一个 DOMRect
对象,是与该元素相关的 CSS 边框集合 。
然后就是document.body.scrollTop
跟 document.documentElement.scrollTop
这两个是一个功能,只不过在不同的浏览器下会有一个始终为 0,所以做了以上的兼容性处理。所以当我们做拖拽功能的时候,就可以依赖上以上属性。
使用方法如下:
var el = document.querySelector(".moveBox");
getOffset(el); // {top: xxx, left: xxx}
2
我们可以看上面的摇杆效果,这里就是利用了offset()
去做位置判断。具体实现代码可以看:https://codepen.io/krischan77/pen/zYxPNPy
# Fade 特效
// Fade in
var fadeIn = function (el) {
el.style.opacity = 0
var last = +new Date()
var tick = function() {
el.style.opacity = +el.style.opacity + (new Date() - last) / 400
last = +new Date()
if (+el.style.opacity < 1) {
requestAnimationFrame(tick))
}
}
tick()
}
// Fade out
var fadeOut = function (el) {
el.style.opacity = 1
var last = +new Date()
var tick = function() {
el.style.opacity = +el.style.opacity - (new Date() - last) / 400
last = +new Date()
if (+el.style.opacity > 0) {
requestAnimationFrame(tick)
}
}
tick()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
上述是淡入淡出效果的具体实现,这里是利用requestAnimationFrame
对opacity
通过递归的方式进行修改。
其实这里需要提一个概念,就是时间分片。
这是一个非常重要的概念,例如 React 的 Fiber 核心实现就是时间分片。它会将一个长任务切分成一个含有若干小任务的任务队列,然后一个接着一个的执行。
requestAnimationFrame
就是这样一个 API,它可以根据系统来决定回调函数的执行时机,其实也就是在下一次重绘之前更新动画帧,因为有这样的机制,所以能防止丢帧。
# 利用队列的概念进行数据操作
队列(queue),是先进先出(FIFO, First-In-First-Out)的线性表。在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为 rear)进行插入操作,在前端(称为 front)进行删除操作。
虽然很多人觉得了解数据结构对前端作用不大,但是如果我们懂一些基础的概念,是否在编码时能够更加扩散我们的思维呢?我们看下面两个例子:
# 获取节点在该父节点下的坐标。
如果我们要操作原生 DOM,那么是绕不开获取节点在该父节点的下标的这个功能的,那么我们该如何实现呢?
当然就是利用我们的循环啦,对子元素集合进行遍历,直到确定下标为止,代码如下:
var index = function(el) {
if (!el) {
return -1;
}
var i = 0;
while ((el = el.previousElementSibling)) {
i++;
}
return i;
};
2
3
4
5
6
7
8
9
10
# 清空子节点
如果我们要清空某个 DOM 节点的子节点,我们有以下的方法:
var empty = function(el) {
while (el.firstChild) {
el.removeChild(el.firstChild);
}
};
2
3
4
5
上面只是提供一个思路,其实el.innerHTML = ''
会更简洁。
# 利用 reduce 进行数据优化
# 数组去重
没错,又是一个老生常谈的问题,数组去重,但是我们这次去除的不仅仅是单个的数据,而是拥有某个相同键值的对象集合。例如下面的例子,我们有以下的数据:
# 牛逼的 reduce
# 数据去重
首先我们来看看一个老生常谈的问题,我们假设有这样的一个对象:
const data = [
{
name: "Kris",
age: "24"
},
{
name: "Andy",
age: "25"
},
{
name: "Kitty",
age: "25"
},
{
name: "Andy",
age: "25"
},
{
name: "Kitty",
age: "25"
},
{
name: "Andy",
age: "25"
},
{
name: "Kitty",
age: "25"
}
];
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
现在我们要去重里面name
重复的对象,这时候我们可以利用reduce
,例子如下:
const dataReducer = (prev, cur, idx) => {
let obj = {};
const { name } = cur;
obj[name] = cur;
return {
...prev,
...obj
};
};
const reducedData = data.reduce(dataReducer, {});
let newData = Object.values(reducedData);
2
3
4
5
6
7
8
9
10
11
# 批量生成对象元素
在鱼头的实际业务中,有一个操作是需要对类似以下的对象进行操作的:
{
a1: 'data',
a2: 'data',
...,
an: 'data'
}
2
3
4
5
6
像我这么懒的鱼,肯定不会一个个手写,所以就有了以下方法
const createList = (item, idx) => {
let obj = {};
obj[`a${idx}`] = "data";
return obj;
};
const listReducer = (acc, cur) => (!acc ? { ...cur } : { ...cur, ...acc });
const obj = Array.from(new Array(20), createList).reduce(listReducer);
2
3
4
5
6
7
如果你喜欢探讨技术,或者对本文有任何的意见或建议,非常欢迎加鱼头微信好友一起探讨,当然,鱼头也非常希望能跟你一起聊生活,聊爱好,谈天说地。 鱼头的微信号是:krisChans95 也可以扫码关注公众号,订阅更多精彩内容。