JavaScript奇技淫巧

前言

JavaScript一直是我滿喜歡的程式語言,原因可能是因為他的執行環境很方便(只要打開瀏覽器就可以使用),不過更多的可能是一種”情結”,畢竟我第一個接觸的程式語言就是JavaScript。
對於JavaScript的評價有褒有貶,有些人推崇它的彈性,有些人深陷它奇怪的行為。
這邊先不評論太多,只是整理一些有趣的小技巧。

Trick 1: 判斷變數類型是否為Primitive

Object建構式可以用來包裝(wrapper)變數,例如將“number”包裝成“number物件”,而已經是object的變數則Object建構式會回傳值。
因此透過判斷Object回傳的內容是否等於變數值,可以用來判斷是否為物件。

1
2
3
4
5
6
7
8
9
let v1 = 1; // primitive
let v2 = [2, 3]; // array object, non-primitive

function isPrimitive(val) {
return Object(val) !== val;
}

isPrimitive(v1); // true
isPrimitive(v2); // false

Trick 2: 建立純物件(Pure Object)

純物件(Pure Object)代表物件不包含任何function(預設的也沒有)。

1
2
var obj = {}; // 一般方法建立的空物件會包含繼承來的function,參考下圖。
var obj = Object.create(null); // pure object

Trick 3: 移除陣列內重複的元素

利用Set物件只能保存唯一值的特性,可以用來將陣列內重複的元素移除。

1
2
let arr = [1, 2, 3, 3, 4, 4, 4, 5];
let newArr = [...new Set(arr)]; // 1, 2, 3, 4, 5

Trick 4: Declaration與Expression

針對何時應該使用Function Declaration與Function Expression進行比較。
首先是考慮提昇(Hoisting),Function Declaration會被hoist,而Function Expression則否。

1
2
3
4
5
6
7
8
9
10
11
// 先呼叫fun1才宣告的作法,由於fun1會被host,所以可以work
fun1();
function fun1(){
// do some thing
}

// 若使用expression的方式則沒有hosting,會fail
fun2();
const fun2 = ()=>{
// do some thing
}

因此如果是全域功能的function,可以使用function declaration,否則可以使用function expression以避免污染全域變數。
另外也可以進一步使用IIFE(Immediately Invoked Function Expressions)。

1
2
3
array.map( item => {
// do some thing
});

Trick 5: 走訪物件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// for...in, 走訪key
for (const property in obj) {
const value = obj[property];
console.log(property, value);
}

// for...of, 走訪value(物件需iterable)
const allProperties = Object.keys(obj); // 透過Object.keys取得物件屬性
for (const property of allProperties) {
const value = obj[property];
console.log(property, value);
}

//使用 Object.entries()
for (const [key, value] of Object.entries(obj)) {
console.log(key, value);
}

Trick 6: 深拷貝(Deep Copy)

一般的做物件複製的時候,如果只是單純assign的話,只能複製到第一層的內容,而物件內包含的物件概念比較像是複製了”指標”,如此一來當物件內的物件內容被改動時,原本複製的內容也會被改動。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const obj1 = {
key: 'value',
nestedObj: {
key2: 'value2'
}
}

const obj2 = Object.assign({}, obj1); // Object.assign
// 使用展開運算子(spread)效果相同
// const obj2 = {
// ...obj1
// }

obj1.nestedObj.key2 = 'newValue';

console.log(obj2); // obj2.nestedObj.key2連帶被改為'newValue'

此時可以利用JSON stringify/paser做深拷貝:

1
const obj2 = JSON.parse(JSON.stringify(obj));

Trick 7: 型別轉換

1
2
3
4
5
// Convert to string
let var1 = 99 + ""

// Convert to bool
let var2 = !!1

Trick 8: 字串反轉

1
2
3
let str = "ABCDEFG";
const reverse = string => string.split("").reverse().join("");
console.log(reverse(str)); // GFEDCBA

Trick 9: 快速Console Log

1
2
c = console.log.bind(document);
c("message") // message

Trick 10: 檢查DOM Element是否可見

可以用來檢查DOM element是否進入畫面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const callback = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log(${entry.target.id});
}
});
};

const options = {
threshold: 1.0, // 可見比例
};

const observer = new IntersectionObserver(callback, options);
const element = document.getElementById('element');
observer.observe(element);

Trick 11: 展開Array

1
2
3
4
5
6
const flat = (array) => {
return array.reduce((acc, it) => acc.concat(Array.isArray(it) ? flat(it) : it), []);
};

const array = [1, [2, [3, [4, [5]]]]];
const flatArray = flat(array); // [1, 2, 3, 4, 5]

Trick 12: 測試經過時間

1
2
3
console.time()
// do something
console.timeEnd()

Trick 13: For Loop效能優化

透過將length變數初始化,可以避免每次loop都需要get一次

1
2
3
4
arr = [1,2,3,4,5,6,7,8,9,0]
for (let i = 0, length = arr.length; i < length; i++){
// do something
}

Trick 14: String轉Number

1
2
3
4
let int = "9";
int = +int;
// or int = ~~int
console.log(typeof int); //Result: "number"

Trick 15: 取得浮點數的整數部分

1
2
3
4
5
let float = 12.3
console.log(float_var | 0)

let neg_float = -45.6
console.log(neg_float_var | 0)

Trick 16: 用??(Nullish Coalescing)運算子取代||運算子

使用??運算子設定預設值是常見的做法,例如:

1
2
3
4
5
6
let parameter = {
var1 = 'v1',
}

let var1 = parameter.var1 || 'default'; // v1
let var2 = parameter.var2 || 'default'; // default

不過如果設定的值是0或fals時,可能會造成誤判。
此時可以使用更嚴謹的??運算子,他只會判斷undifined與null的狀況。

1
2
3
4
5
let parameter = {
var1 = false,
}
let var1 = parameter.var1 ?? 'default'; // false
let var2 = parameter.var2 || 'default'; // default

Trick 17: 用?.(Optoinal Chaining)運算子

用?.運算子可以簡化用&&判斷null或undefined的情況。

1
2
3
4
5
6
7
8
9
// case 1
let var1 = parameter.property && parameter.property.value;
let var1 = parameter.property?.value

// case 2
let input = form.querySelector('input[name=userInput]')
let inputValue = input ? input.value : undefined

let input = form.querySelector('input[name=userInput]')?.value

Trick 18: 動態import

一般常使用import進行module的載入,不過這種靜態的載入方式有一些限制,
例如必須在script tag上宣告type為module,且不夠彈性。
HTML:

1
<script src="script.js" type="module"></script>

JavaScript:

1
import submodule from './submodule.js';

此時可以採用動態的import:

1
2
3
4
5
6
7
8
9
10
11
// Form 1
import('./submodule.js')
.then((module) => {
// Do something
});

// Form 2
(async () => {
let module = await import('./submodule.js');
// Do something
})();

Trick 19: 取得最後一個元素

1
2
3
arr[arr.length-1]
// or
arr.at(-1)

Trick 20: 設定private變數

在class內宣告#開頭的變數可以將其設為private,此變數不會被繼承。

1
2
3
4
5
6
7
8
9
class ClassWithPrivateField {
#privateField;
#privateMethod() {
return 0;
}
constructor() {
this.#privateField = 0;
}
}

JavaScript奇技淫巧
https://chris-suo.github.io/ChrisComplete/2022/08/14/Javascript-Trick/
Author
Chris Suo
Posted on
August 14, 2022
Licensed under