0%

前言

最近求職的過程中
看到幾間公司有將 Next.js 列在加分項目
因此提升了我對 Next.js 的好奇心

其實我在滿久以前就聽過 Next 了
只知道是 React 的加強版
針對 React 天生不利於 SEO 的缺點提供解決方案
而且不需要依賴 react-router 就能做到換頁
所以最近正式的來學習一下

途中在 CSS 上遇到了小問題

在 Next 中使用 CSS 的方法

先不論 SASS, CSS-in-JS 這些用法
如果要用正統 CSS 的話
有以下兩種方法

1. 在 _app.js 中引入 global CSS

在 Next 專案中
只有一個地方可以引入純 CSS
就是_app.js,如下

1
2
3
4
5
6
// In _app.js
import '../styles/global.css' // <--- Here!

export default function MyApp({Component, pageProps}){
return <Component {...pageProps}>
}

如果在其他元件中import 'xxx.css'的話
會出現以下 error

./styles/global.css
Global CSS cannot be imported from files other than your Custom . Due to the Global nature of stylesheets, and to avoid conflicts, Please move all first-party global CSS imports to pages/_app.js. Or convert the import to Component-Level CSS (CSS Modules).
Read more: https://nextjs.org/docs/messages/css-global

2. 在各別頁面或元件中使用 CSS module

CSS module 也是一個見了許久卻沒使用的東西
他主要的功用是 能依照作用範圍切分 CSS 文件的大小

1
2
3
4
5
/* In page.module.css */
.container {
padding: 0 1rem;
margin: 3rem auto 6rem;
}
1
2
3
4
5
6
7
8
9
10
// In page.js
import styles from './page.module.css' // <--- Here!

export default function page(){
return (
<div className={styles.container}> {/* <--- Here!*/}
<h2>This is a page.</h2>
</div>
)
}

上面的例子,page.module.css就是專門寫給page.js用的
這樣就可以避免跟其他元件的 class 撞名

以前當我要避免撞名時
不是把 APP 的所有 CSS 都寫在一份檔案
就是極致利用 BEM 規則把每個 class 都寫得超長

用了 CSS module 後
當你去看網頁上實際的 class 會發現他可能長這樣
<div class="container__fbLkO"></div>
CSS module 會在 class 後面加上亂碼
避免跟其他元件的 class 撞名

一些限制

進入正題(前面講了好多別的)
CSS module 雖然方便
但仍然有一些限制
例如在.module.css
只能用 class 或 id 選擇器
不能用 element 選擇器

意思是不能在.module.css中這樣寫

1
2
3
p {
color: red;
}

詳細原因可以參考 –> 這個連結 <–

如果基於某些原因必須去選 element
就只能寫在 global.css 中囉

除此之外,偽類偽元素以及選擇器的組合都還是能正常使用的喔

結論

我覺得不管是什麼前端技術(或程式語言技術)
最終都會向元件化靠攏
像 styled component 或今天介紹的 CSS module
都是盡可能的把「屬於元件的部分」跟元件本身放得越近越好
享受方便性的同時,也要同時注意使用上的限制才行

前言

今天要介紹的 JS 觀念是 call, apply 和 bind 三個 function
這些我目前為止都沒用過
因為在過去的專案中,幾乎沒用到 OOP (Object Oriented Programming)
class 也沒用過
自然就沒遇到適合的使用情境
但有時還是會在一些教學影片看到他們的身影
(即使那不是影片主題)
我就覺得應該要來補一下

this 簡介

this是在 javascript 中的重要語法
他指向的是函式被調用的位置

如果是物件調用函式,this就是那個物件
在瀏覽器中直接印this,就是window物件
在node.js直接印this,就是global物件
在嚴格模式中直接印this,就是undefined

如果我們想為物件打造一個 function
就可以在函式中用上this

1
2
3
4
5
const newFriend = {name: 'Josie'}
const greeting = function(myName){
console.log(`Hi, ${this.name}, my name is ${myName}`)
}
// greeting(???)

這時突然發現物件沒有進入點
解決方法可以把 greeting 定義在物件裡面
或是物件之上如果有一個類別的話
也可以定義在prototype裡面
但我們也可以改用 call, apply, bind 來呼叫函式
接下來就來一一介紹

call

call 的功能是執行函式,並指定函式中的this是誰

1
2
3
4
5
6
7
8
9
10
11
12
13
// function.call(object, arg1, arg2, arg3, ...)
// 執行 function,傳入參數 arg1, arg2 ...,並指定函式中的 this 為 object

const newFriend = { name: 'Josie' }

const greeting = function(myName, otherName) {
return console.log(`Hi, ${this.name}, my name is ${myName}. And this is ${otherName}.`)
}

greeting.call(newFriend, 'Tony', 'Bowen')


// Hi, Josie. My name is Tony. And this is Bowen.

apply

基本上跟 call 做一樣的事情
但提供另一個傳入引數 (argument) 的方式
改成傳入包含所有引數的陣列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// function.apply(object, [arg1, arg2, arg3, ...])
// 執行 function,傳入參數 arg1, arg2 ...,並指定函式中的 this 為 object

const newFriend = { name: 'Josie' }

const greeting = function(myName, otherName) {
return console.log(`Hi, ${this.name}, my name is ${myName}. And this is ${otherName}.`)
}

const arr = ['Tony','Bowen']

greeting.apply(newFriend, arr)

// Hi, Josie. My name is Tony. And this is Bowen.

bind

將函式中的 this 綁定給某個對象,然後回傳一個新的函式供使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// function.bind(object) 
// return function
// 將 function 和 object 綁定,回傳一個新 function

const newFriend = {name: 'Josie'}

const greeting = function(myName, otherName){
return console.log(`Hi, ${this.name}, my name is ${myName}. And this is ${otherName}.`)
}

const greetToNewFriend = greeting.bind(newFriend)

greetToNewFriend('Tony','Bowen')

// Hi, Josie, my name is Tony. And this is Bowen.

結論

callapply: 執行函式並指定this所指對象
bind: 回傳一個新函式,是原函式與this所指對象綁定的版本

基本上除非函式中包含this
否則不太會用到這三個函式

前言

這次要講一個我在做專案時一直被他搞死的概念
深拷貝與淺拷貝
雖然已經知道這個概念
相關文章也看到好幾次了
但每次還是會在專案中卡好久好久找不到 bug
最後才發現說:「啊 原來又是這個問題啊」

Call by value/reference

首先要釐清的觀念是 call by value 跟 call by reference

在 javascript 中物件是採用 call by reference

也就是例如當你把已存在記憶體中的物件指派到a變數時
a會指向該記憶體位置,而不是複製其值
讓我們看個例子

1
2
3
4
5
const A = {num: 1}
const B = {num: 1}
const C = A
console.log(A===B) // false
console.log(A===C) // true

當我互相比較這三個物件時
會發現AB明明內容相同,卻回傳 false
只有當C是從A assign 來的時候才得到 true
因為其實 A 和 C 是共用同一個記憶體
而物件的比較是去看記憶體位置
所以得到 true
AB雖然內容一樣,卻因為都是新創的物件
他們會占用不同的記憶體位置
對 JS 來說就是不一樣的囉

淺拷貝

上述的 const C = A 就是淺拷貝的一種
他沒辦法做到完全的複製
而是在「某程度」上保有指向相同記憶體的特性

1
2
3
4
5
const A = {num: 1}
const B = A
console.log(A,B) //{num: 1}, {num: 1}
A.num = 10
console.log(A,B) //{num: 10}, {num: 10}

我只更動A,結果B的內容也改了
很多時候出現 bug 就是因為這個原因

至於剛剛為什麼要強調「某程度」呢?
因為有些做法只能在第一層上進行深拷貝
當你的物件結構更複雜時就會出問題
來看一個被誤以為是深拷貝的技巧 spread operator

1
2
3
4
5
const A = {num: 1}
const B = {...A}
console.log(A,B) //{num: 1}, {num: 1}
A.num = 10
console.log(A,B) //{num: 10}, {num: 1}

看似沒問題,但如果改成二階以上的物件的話…

1
2
3
4
5
const A = {num: {ch: '一'}}
const B = {...A}
console.log(A.num.ch, B.num.ch) // "一", "一"
A.num.ch = '四'
console.log(A.num.ch, B.num.ch) //"四", "四"

另外還有一個做法是 Object.assign()
但結果跟解構賦值一樣
僅限於一階物件

深拷貝

我個人最喜歡的作法是用JSON.stringifyJSON.parse
直接把物件文字化再物件化一次
就一定會是另一個物件了
達到深拷貝的效果,像這樣:

const B = JSON.parse(JSON.stringify(A))

或是如果嫌每次都這樣寫麻煩
可以在專案中定義一個 deepClone function

1
2
3
4
5
6
7
8
const deepClone = (obj) => JSON.parse(JSON.stringify(obj))

const A = {num: {ch: '一'}}
const B = deepClone(A)

console.log(A.num.ch, B.num.ch) //"一", "一"
A.num.ch = '四'
console.log(A.num.ch, B.num.ch) //"四", "一"

結論

淺拷貝: 看似複製,其實是指向相同位址
深拷貝: 另用一個記憶體存值,實質意義上的複製