通常,想要執行什麼事情時,會寫在兩個地方:
不會把要做的事情直接寫在 function component 裡面,原因為:
如果直接寫在 function component 裡面,那就是「每次 render 時都會執行」
但是這樣一邊 render 一邊執行要做的事情,會阻礙 render 的進行,效能會比較差
比較好的做法是:等 render 完成之後,再執行要做的事情
如果要避免 re-render 的話,就會使用到 useCallback, useMemo, memo
安裝完之後,要把 vs code 右下角的 ESLint 打勾來啟用它
propTypes 是 React 提供的方法,用來檢查 prop 的型態是否正確
如果沒有按照 propTypes 的規則去使用這個 prop 的話,就會在 console 顯示 warning(但不會影響到程式碼的執行)
要使用 propTypes,就要先改一下 ESLint 的規則,步驟如下:
先在根目錄新增一個檔案,叫做 .eslintrc.json,在裡面要寫的內容如下
"extends": ["react-app"]
意思是:先延續 Create React App 的規則
"rules"
意思是:再新增我自己訂的規則,"warn"
就是 warning 的意思
.eslintrc.json:
{
"extends": ["react-app"],
"rules": {
"react/prop-types": "warn"
}
}
加上 "react/prop-types"
這個 rule 之後,就會規定我「每個 component 都一定要寫 prop types」,寫起來會有一點麻煩,但好處是可以幫我避免一些預期之外的錯誤
要使用 propTypes 之前,要先在最上方引入:
PropTypes
是 React 提供的,會幫助我定義一些已經有的類型
import PropTypes from "prop-types";
然後,就可以幫 TodoItem
component 定義 propTypes 了(就寫在 TodoItem
component 的後面):
後面加上 .isRequired
意思就是:一定要傳這個 prop,否則就會在 console 出現 warning
PropTypes.func
的 func 是 function 的簡寫
TodoItem.propTypes = {
className: PropTypes.string,
size: PropTypes.string.isRequired,
todo: PropTypes.object,
handleDeleteTodo: PropTypes.func,
handleToggleIsFinished: PropTypes.func,
};
如果想要更詳細的寫出 todo
這個 object 是長什麼樣子,例如 todo
這個 object 裡面會用到 id
, content
, isFinished
這些 key,就可以用 PropTypes.shape()
來寫:
bool
是 boolean 的簡寫
TodoItem.propTypes = {
className: PropTypes.string,
size: PropTypes.string.isRequired,
todo: PropTypes.shape({
id: PropTypes.number,
content: PropTypes.string,
isFinished: PropTypes.bool,
}),
handleDeleteTodo: PropTypes.func,
handleToggleIsFinished: PropTypes.func,
};
如果要把這個 todo list 的畫面變成 json 格式的資料存下來,可以這樣寫:
// 放在 state 裡面
{
inputValue: 'abc',
todos: [
{
content: 'work out',
isChecked: true
},
{
content: 'buy milk',
isChecked: false
}
],
itemLeftCount: 1,
filter: 'active'
}
// 沒有放在 state 裡面
todoId: 2
以資料為中心去思考,有兩個重點:
必備的資料(無法透過其他資料算出來的),才會是 state
例如:todo list 裡面的 todoId
因為畫面上不需要顯示 id,所以 todoId 不會被放在 state 裡面。當 todoId 遞增時,會用 useRef 來存
const todoId = useRef(0);
要用 todoId
的值時,就要寫 todoId.current
才會是我要的值
會需要用 .current
是因為 JS 的 pass by reference -> 一定要是一個「物件」的屬性,才可以被記下來
derive 是「從…中得到」的意思
derived state 就是:不需要直接放在 state 裡面,它是可以被計算出來的 -> 可以透過現有的 state 去組合/計算而成 -> 從現有的 state 衍伸出來的就叫做 derived state
如果是 derived state 就不要放在 state 裡面,否則會很難維護
以上面的 todo list 來說,itemLeftCount
就是 derived state
我根本不需要把 itemLeftCount
放在 state 裡面,我只要去 todos
裡面計算出有多少筆資料的 isChecked
是 false,就會是 itemLeftCount
的結果
// 放在 state 裡面
{
inputValue: 'abc',
todos: [
{
content: 'work out',
isChecked: true
},
{
content: 'buy milk',
isChecked: false
}
],
filter: 'active'
}
// 沒有放在 state 裡面
todoId: 2
App.js:
filteredTodos
就是:選擇不同的 filter 之後要相對應 render 出來的 todos
filteredTodos
是 derived state,因為可以從 filter
和 todos
的狀態來推算出 filteredTodos
export default function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "aaa", isChecked: false },
{ id: 2, content: "bbb", isChecked: true },
]);
const [filter, setFilter] = useState("active");
// 可以從 filter 和 todos 來推算出 filteredTodos,所以 filteredTodos 是 derived state
const filteredTodos = todos.filter((todo) => {
if (filter === "all") return true;
if (filter === "active") return !todo.isChecked;
if (filter === "completed") return todo.isChecked;
});
console.log("render~");
return (
<div>
todo:{" "}
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
{filteredTodos.map((todo) => (
<div key={todo.id}>{todo.content}</div>
))}
</div>
);
}
在 input 欄位每輸入一個字,在 console 就會印出一次 "render~"
原因為:
在 input 欄位每輸入一個字,就會 setValue
一次,所以就會 re-render 一次 -> 重新執行一次 App()
filteredTodos
沒有任何關係,所以 filteredTodos
其實不需要跟著重新計算一次useMemo
useMemo
useMemo
之後,這個 derived state 就不需要每次 re-render 都跟著重新計算一次,只有在「depedency array 裡面的 state 改變時」,才會重新計算一次useMemo
裡面要傳入一個 function,然後要記得 return 東西
useMemo
錯誤使用 dependency array 的做法在 useMemo
要傳入第二個參數(depedency array),如果第二個參數是傳一個「空陣列」的話,那就只有第一次 render 會計算 filteredTodos
(印出 "calculate filteredTodos"),之後的 re-render 都不會再計算一次 filteredTodos
了
export default function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "aaa", isChecked: false },
{ id: 2, content: "bbb", isChecked: true },
]);
const [filter, setFilter] = useState("active");
// 可以從 filter 和 todos 來推算出 filteredTodos,所以 filteredTodos 是 derived state
const filteredTodos = useMemo(() => {
console.log("calculate filteredTodos");
return todos.filter((todo) => {
if (filter === "all") return true;
if (filter === "active") return !todo.isChecked;
if (filter === "completed") return todo.isChecked;
});
}, []);
console.log("render~");
return (
<div>
todo:{" "}
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
{filteredTodos.map((todo) => (
<div key={todo.id}>{todo.content}</div>
))}
</div>
);
}
但是,把 useMemo
的 dependency array 傳一個空陣列,這樣其實是錯的
原因為:
用一個「清空」的按鈕來說明,就知道為什麼是錯的了
我在 input 欄位旁邊放上一個「清空」的按鈕,點擊就會把 todos
變為空陣列
export default function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "aaa", isChecked: false },
{ id: 2, content: "bbb", isChecked: true },
]);
const [filter, setFilter] = useState("active");
// 可以從 filter 和 todos 來推算出 filteredTodos,所以 filteredTodos 是 derived state
const filteredTodos = useMemo(() => {
console.log("calculate filteredTodos");
return todos.filter((todo) => {
if (filter === "all") return true;
if (filter === "active") return !todo.isChecked;
if (filter === "completed") return todo.isChecked;
});
}, []);
console.log("render~");
return (
<div>
todo:{" "}
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
<button
onClick={() => {
setTodos([]);
}}
>
清空所有項目
</button>
{filteredTodos.map((todo) => (
<div key={todo.id}>{todo.content}</div>
))}
</div>
);
}
但是,當我點擊「清空」按鈕後,會發現:頁面上的 todos 還在,並沒有被清空
其實,todos
陣列已經變成空陣列了(App()
有 re-render)
但是因為在 filteredTodos
的 useMemo
的 dependency array 是空陣列,任何的 state 改變都不會讓 filteredTodos
重新計算(依然還是舊的值),所以畫面並不會改變
但是,因為 filteredTodos
是透過 filter
和 todos
計算而成的,所以當 filter
或 todos
改變時,filteredTodos
應該要跟著改變才對
useMemo
正確使用 dependency array 的做法所以,要在 useMemo
的 dependency array 裡面填入 filter, todos
,意思就是:只有當 filter
或 todos
改變時,filteredTodos
才會重新計算一次
如果 filter
或 todos
都沒有改變,那 useMemo
會幫我記住「上一次計算的值」,所以不需要再計算一次
// 可以從 filter 和 todos 來推算出 filteredTodos,所以 filteredTodos 是 derived state
const filteredTodos = useMemo(() => {
console.log("calculate filteredTodos");
return todos.filter((todo) => {
if (filter === "all") return true;
if (filter === "active") return !todo.isChecked;
if (filter === "completed") return todo.isChecked;
});
}, [filter, todos]);
useCallback
記住 functionuseCallback
錯誤使用 dependency array 的做法當我在 input 欄位輸入字時,會觸發 onChange
事件並執行 handleChange
在 handleChange
裡面會把 todos
陣列印出來
export default function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "aaa", isChecked: false },
{ id: 2, content: "bbb", isChecked: true },
]);
const [filter, setFilter] = useState("active");
// 可以從 filter 和 todos 來推算出 filteredTodos,所以 filteredTodos 是 derived state
const filteredTodos = useMemo(() => {
console.log("calculate filteredTodos");
return todos.filter((todo) => {
if (filter === "all") return true;
if (filter === "active") return !todo.isChecked;
if (filter === "completed") return todo.isChecked;
});
}, [filter, todos]);
const handleChange = useCallback((e) => {
console.log(todos);
setValue(e.target.value);
}, []);
console.log("render~");
return (
<div>
todo: <input value={value} onChange={handleChange} />
<button
onClick={() => {
setTodos([]);
}}
>
清空所有項目
</button>
{filteredTodos.map((todo) => (
<div key={todo.id}>{todo.content}</div>
))}
</div>
);
}
在 useCallback
的 dependency array 傳一個空陣列,這是錯誤的寫法
因為:
dependency array 傳一個空陣列代表「只有第一次 render 會產生新的 function,之後就會一直沿用這個 function」
useCallback
會幫我記住上一個 function,所以就算 todos
的 state 改變了,因為 handleChange
還是第一次 render 時的 function,所以在 function 裡面的 todos
就還是第一次 render 時的值,這樣就是錯的
useCallback
正確使用 dependency array 的做法因為在 handleChange
裡面有用到 todos
,所以要在 dependency array 裡面填入 todos
,這樣每一次 re-render 之後才會重新宣告一個新的 handleChange
function,才會拿到最新的 todos
的值
const handleChange = useCallback(
(e) => {
console.log(todos);
setValue(e.target.value);
},
[todos]
);
useMemo
, useCallback
, useEffect
的 dependency array 機制都是一樣的
通常,想要執行什麼事情時,會寫在兩個地方:
不會把要做的事情直接寫在 function component 裡面,原因為:
如果直接寫在 function component 裡面,那就是「每次 render 時都會執行」
但是這樣一邊 render 一邊執行要做的事情,會阻礙 render 的進行,效能會比較差
比較好的做法是:等 render 完成之後,再執行要做的事情
如果要避免 re-render 的話,就會使用到 useCallback, useMemo, memo
安裝完之後,要把 vs code 右下角的 ESLint 打勾來啟用它
propTypes 是 React 提供的方法,用來檢查 prop 的型態是否正確
如果沒有按照 propTypes 的規則去使用這個 prop 的話,就會在 console 顯示 warning(但不會影響到程式碼的執行)
要使用 propTypes,就要先改一下 ESLint 的規則,步驟如下:
先在根目錄新增一個檔案,叫做 .eslintrc.json,在裡面要寫的內容如下
"extends": ["react-app"]
意思是:先延續 Create React App 的規則
"rules"
意思是:再新增我自己訂的規則,"warn"
就是 warning 的意思
.eslintrc.json:
{
"extends": ["react-app"],
"rules": {
"react/prop-types": "warn"
}
}
加上 "react/prop-types"
這個 rule 之後,就會規定我「每個 component 都一定要寫 prop types」,寫起來會有一點麻煩,但好處是可以幫我避免一些預期之外的錯誤
要使用 propTypes 之前,要先在最上方引入:
PropTypes
是 React 提供的,會幫助我定義一些已經有的類型
import PropTypes from "prop-types";
然後,就可以幫 TodoItem
component 定義 propTypes 了(就寫在 TodoItem
component 的後面):
後面加上 .isRequired
意思就是:一定要傳這個 prop,否則就會在 console 出現 warning
PropTypes.func
的 func 是 function 的簡寫
TodoItem.propTypes = {
className: PropTypes.string,
size: PropTypes.string.isRequired,
todo: PropTypes.object,
handleDeleteTodo: PropTypes.func,
handleToggleIsFinished: PropTypes.func,
};
如果想要更詳細的寫出 todo
這個 object 是長什麼樣子,例如 todo
這個 object 裡面會用到 id
, content
, isFinished
這些 key,就可以用 PropTypes.shape()
來寫:
bool
是 boolean 的簡寫
TodoItem.propTypes = {
className: PropTypes.string,
size: PropTypes.string.isRequired,
todo: PropTypes.shape({
id: PropTypes.number,
content: PropTypes.string,
isFinished: PropTypes.bool,
}),
handleDeleteTodo: PropTypes.func,
handleToggleIsFinished: PropTypes.func,
};
如果要把這個 todo list 的畫面變成 json 格式的資料存下來,可以這樣寫:
// 放在 state 裡面
{
inputValue: 'abc',
todos: [
{
content: 'work out',
isChecked: true
},
{
content: 'buy milk',
isChecked: false
}
],
itemLeftCount: 1,
filter: 'active'
}
// 沒有放在 state 裡面
todoId: 2
以資料為中心去思考,有兩個重點:
必備的資料(無法透過其他資料算出來的),才會是 state
例如:todo list 裡面的 todoId
因為畫面上不需要顯示 id,所以 todoId 不會被放在 state 裡面。當 todoId 遞增時,會用 useRef 來存
const todoId = useRef(0);
要用 todoId
的值時,就要寫 todoId.current
才會是我要的值
會需要用 .current
是因為 JS 的 pass by reference -> 一定要是一個「物件」的屬性,才可以被記下來
derive 是「從…中得到」的意思
derived state 就是:不需要直接放在 state 裡面,它是可以被計算出來的 -> 可以透過現有的 state 去組合/計算而成 -> 從現有的 state 衍伸出來的就叫做 derived state
如果是 derived state 就不要放在 state 裡面,否則會很難維護
以上面的 todo list 來說,itemLeftCount
就是 derived state
我根本不需要把 itemLeftCount
放在 state 裡面,我只要去 todos
裡面計算出有多少筆資料的 isChecked
是 false,就會是 itemLeftCount
的結果
// 放在 state 裡面
{
inputValue: 'abc',
todos: [
{
content: 'work out',
isChecked: true
},
{
content: 'buy milk',
isChecked: false
}
],
filter: 'active'
}
// 沒有放在 state 裡面
todoId: 2
App.js:
filteredTodos
就是:選擇不同的 filter 之後要相對應 render 出來的 todos
filteredTodos
是 derived state,因為可以從 filter
和 todos
的狀態來推算出 filteredTodos
export default function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "aaa", isChecked: false },
{ id: 2, content: "bbb", isChecked: true },
]);
const [filter, setFilter] = useState("active");
// 可以從 filter 和 todos 來推算出 filteredTodos,所以 filteredTodos 是 derived state
const filteredTodos = todos.filter((todo) => {
if (filter === "all") return true;
if (filter === "active") return !todo.isChecked;
if (filter === "completed") return todo.isChecked;
});
console.log("render~");
return (
<div>
todo:{" "}
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
{filteredTodos.map((todo) => (
<div key={todo.id}>{todo.content}</div>
))}
</div>
);
}
在 input 欄位每輸入一個字,在 console 就會印出一次 "render~"
原因為:
在 input 欄位每輸入一個字,就會 setValue
一次,所以就會 re-render 一次 -> 重新執行一次 App()
filteredTodos
沒有任何關係,所以 filteredTodos
其實不需要跟著重新計算一次useMemo
useMemo
useMemo
之後,這個 derived state 就不需要每次 re-render 都跟著重新計算一次,只有在「depedency array 裡面的 state 改變時」,才會重新計算一次useMemo
裡面要傳入一個 function,然後要記得 return 東西
useMemo
錯誤使用 dependency array 的做法在 useMemo
要傳入第二個參數(depedency array),如果第二個參數是傳一個「空陣列」的話,那就只有第一次 render 會計算 filteredTodos
(印出 "calculate filteredTodos"),之後的 re-render 都不會再計算一次 filteredTodos
了
export default function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "aaa", isChecked: false },
{ id: 2, content: "bbb", isChecked: true },
]);
const [filter, setFilter] = useState("active");
// 可以從 filter 和 todos 來推算出 filteredTodos,所以 filteredTodos 是 derived state
const filteredTodos = useMemo(() => {
console.log("calculate filteredTodos");
return todos.filter((todo) => {
if (filter === "all") return true;
if (filter === "active") return !todo.isChecked;
if (filter === "completed") return todo.isChecked;
});
}, []);
console.log("render~");
return (
<div>
todo:{" "}
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
{filteredTodos.map((todo) => (
<div key={todo.id}>{todo.content}</div>
))}
</div>
);
}
但是,把 useMemo
的 dependency array 傳一個空陣列,這樣其實是錯的
原因為:
用一個「清空」的按鈕來說明,就知道為什麼是錯的了
我在 input 欄位旁邊放上一個「清空」的按鈕,點擊就會把 todos
變為空陣列
export default function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "aaa", isChecked: false },
{ id: 2, content: "bbb", isChecked: true },
]);
const [filter, setFilter] = useState("active");
// 可以從 filter 和 todos 來推算出 filteredTodos,所以 filteredTodos 是 derived state
const filteredTodos = useMemo(() => {
console.log("calculate filteredTodos");
return todos.filter((todo) => {
if (filter === "all") return true;
if (filter === "active") return !todo.isChecked;
if (filter === "completed") return todo.isChecked;
});
}, []);
console.log("render~");
return (
<div>
todo:{" "}
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
<button
onClick={() => {
setTodos([]);
}}
>
清空所有項目
</button>
{filteredTodos.map((todo) => (
<div key={todo.id}>{todo.content}</div>
))}
</div>
);
}
但是,當我點擊「清空」按鈕後,會發現:頁面上的 todos 還在,並沒有被清空
其實,todos
陣列已經變成空陣列了(App()
有 re-render)
但是因為在 filteredTodos
的 useMemo
的 dependency array 是空陣列,任何的 state 改變都不會讓 filteredTodos
重新計算(依然還是舊的值),所以畫面並不會改變
但是,因為 filteredTodos
是透過 filter
和 todos
計算而成的,所以當 filter
或 todos
改變時,filteredTodos
應該要跟著改變才對
useMemo
正確使用 dependency array 的做法所以,要在 useMemo
的 dependency array 裡面填入 filter, todos
,意思就是:只有當 filter
或 todos
改變時,filteredTodos
才會重新計算一次
如果 filter
或 todos
都沒有改變,那 useMemo
會幫我記住「上一次計算的值」,所以不需要再計算一次
// 可以從 filter 和 todos 來推算出 filteredTodos,所以 filteredTodos 是 derived state
const filteredTodos = useMemo(() => {
console.log("calculate filteredTodos");
return todos.filter((todo) => {
if (filter === "all") return true;
if (filter === "active") return !todo.isChecked;
if (filter === "completed") return todo.isChecked;
});
}, [filter, todos]);
useCallback
記住 functionuseCallback
錯誤使用 dependency array 的做法當我在 input 欄位輸入字時,會觸發 onChange
事件並執行 handleChange
在 handleChange
裡面會把 todos
陣列印出來
export default function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "aaa", isChecked: false },
{ id: 2, content: "bbb", isChecked: true },
]);
const [filter, setFilter] = useState("active");
// 可以從 filter 和 todos 來推算出 filteredTodos,所以 filteredTodos 是 derived state
const filteredTodos = useMemo(() => {
console.log("calculate filteredTodos");
return todos.filter((todo) => {
if (filter === "all") return true;
if (filter === "active") return !todo.isChecked;
if (filter === "completed") return todo.isChecked;
});
}, [filter, todos]);
const handleChange = useCallback((e) => {
console.log(todos);
setValue(e.target.value);
}, []);
console.log("render~");
return (
<div>
todo: <input value={value} onChange={handleChange} />
<button
onClick={() => {
setTodos([]);
}}
>
清空所有項目
</button>
{filteredTodos.map((todo) => (
<div key={todo.id}>{todo.content}</div>
))}
</div>
);
}
在 useCallback
的 dependency array 傳一個空陣列,這是錯誤的寫法
因為:
dependency array 傳一個空陣列代表「只有第一次 render 會產生新的 function,之後就會一直沿用這個 function」
useCallback
會幫我記住上一個 function,所以就算 todos
的 state 改變了,因為 handleChange
還是第一次 render 時的 function,所以在 function 裡面的 todos
就還是第一次 render 時的值,這樣就是錯的
useCallback
正確使用 dependency array 的做法因為在 handleChange
裡面有用到 todos
,所以要在 dependency array 裡面填入 todos
,這樣每一次 re-render 之後才會重新宣告一個新的 handleChange
function,才會拿到最新的 todos
的值
const handleChange = useCallback(
(e) => {
console.log(todos);
setValue(e.target.value);
},
[todos]
);
useMemo
, useCallback
, useEffect
的 dependency array 機制都是一樣的
有一個問題叫做 prop drilling
drill 就是「往下鑽」的意思
範例程式碼如下
Demo.js:
在 Demo
component 裡面,會 render 出 <DemoInner />
在 DemoInner
component 裡面,會 render 出 <DemoInnerBox />
在 DemoInnerBox
component 裡面,會 render 出 <DemoInnerBoxContent />
在 DemoInnerBoxContent
component 裡面,會 render 出 <button>Update title!</button>
這個按鈕
import React, { useState } from "react";
function DemoInnerBoxContent() {
return (
<div>
<button>Update title!</button>
</div>
);
}
function DemoInnerBox() {
return <DemoInnerBoxContent />;
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
return (
<div>
title: {title}
<DemoInner />
</div>
);
}
在 index.js 會 render 出 Demo
component:
import React from "react";
import ReactDOM from "react-dom";
import Demo from "./Demo";
import reportWebVitals from "./reportWebVitals";
import { ThemeProvider } from "styled-components";
ReactDOM.render(
<ThemeProvider theme={theme}>
<Demo />
</ThemeProvider>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
UI 長這樣:
我要做的功能是:
當我按下在 DemoInnerBoxContent
component 裡面的按鈕,就要去 update 「在 Demo
component 裡面的 title
」
做法是:
當我想要從 children 去 update parent 的 state
時,就要把這個 setTitle
這個 function 從 parent (也就是 DemoInner
component)傳下去
import React, { useState } from "react";
function DemoInnerBoxContent({ setTitle }) {
return (
<div>
<button
onClick={() => {
setTitle(Math.random());
}}
>
Update title!
</button>
</div>
);
}
function DemoInnerBox({ setTitle }) {
return <DemoInnerBoxContent setTitle={setTitle} />;
}
function DemoInner({ setTitle }) {
return <DemoInnerBox setTitle={setTitle} />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
return (
<div>
title: {title}
<DemoInner setTitle={setTitle} />
</div>
);
}
從 DemoInner
component 傳到 DemoInnerBox
component,
再從 DemoInnerBox
component 傳到 DemoInnerBoxContent
component 之後,
我才能在 DemoInnerBoxContent
component 拿到 setTitle
這個 function
遇到的問題是:
我需要傳「很多層」之後,我才可以在 DemoInnerBoxContent
component 拿到 setTitle
這個 function -> 我才能夠呼叫 setTitle
這個 function
這個問題就叫做 prop drilling:
當我的 component 有很多層的時候,我就要傳很多層才能傳到我要的地方,這樣就很麻煩,因為對於中間的那幾層(DemoInner
和 DemoInnerBox
)只是扮演一個中介者的角色,只負責把 prop
向下傳遞而已,它們根本不需要拿到 setTitle
這個 function
而且是,每一層都要傳,不然最後面的會收不到
這就是 useContext
要解決的問題
React 提供了一個方法來解決 prop drilling 的問題,這個方法就是 useContext
context 就是「上下文、脈絡」的意思
useContext
的用法如下:
useContext
, createContext
import React, { useState, useContext, createContext } from "react";
const TitleContext = createContext()
在 createContext()
裡面,要傳入的是:TitleContext
要提供的 value 初始值
在 React 裡面,可以透過 context 把「上層的東西」傳到下層
<TitleContext.Provider></TitleContext.Provider>
的 component 包住意思就是:<TitleContext.Provider></TitleContext.Provider>
這個 component 要提供「Context 的值」
因此,在 value
就可以填入「我要傳下去的 context」,也就是 setTitle
這個 function
-> 在 <TitleContext.Provider value={setTitle}></TitleContext.Provider>
這層 component,把 context 的值設為 setTitle
並傳下去
useContext
來使用 context 傳進來的值value
傳入什麼,在 useContext
就會回傳什麼TitleContext.Provider
底下的任何一層(也就是 DemoInner
, DemoInnerBox
, DemoInnerBoxContent
),都可以使用 setTitle
了在 DemoInnerBoxContent
裡面寫上 const setTitle = useContext(TitleContext)
,意思就是:我在 DemoInnerBoxContent
這層,要使用 TitleContext
這個 context 傳進來的值
透過 context 的方式,這些「中間層」就可以不用再扮演「傳遞 prop
」的角色了
Demo.js:
import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext
const TitleContext = createContext();
function DemoInnerBoxContent() {
const setTitle = useContext(TitleContext); // 我在 DemoInnerBoxContent 這層,要使用 TitleContext 這個 context 傳進來的值
return (
<div>
<button
onClick={() => {
setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
}}
>
Update title!
</button>
</div>
);
}
function DemoInnerBox() {
return <DemoInnerBoxContent />;
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
// 在 TitleContext.Provider 這層 component,把 context 的值設為 setTitle 並傳下去
return (
<TitleContext.Provider value={setTitle}>
<div>
title: {title}
<DemoInner />
</div>
</TitleContext.Provider>
);
}
value
傳下去的 context 可以是任何東西例如:可以是一個陣列
在 TitleContext.Provider
把 value
設為 [title, setTitle]
傳下去
import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext
const TitleContext = createContext();
function DemoInnerBoxContent() {
const [title, setTitle] = useContext(TitleContext); // 我在 DemoInnerBoxContent 這層,要使用 TitleContext 這個 context 傳進來的值
return (
<div>
<button
onClick={() => {
setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
}}
>
Update title!
</button>
{title}
</div>
);
}
function DemoInnerBox() {
return <DemoInnerBoxContent />;
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
// 在 TitleContext.Provider 這層 component,把 context 的值設為 setTitle 並傳下去
return (
<TitleContext.Provider value={[title, setTitle]}>
<div>
title: {title}
<DemoInner />
</div>
</TitleContext.Provider>
);
}
styled component 的 <ThemeProvider></ThemeProvider>
,背後就是用 Context 來實作的
index.js:
ReactDOM.render(
<ThemeProvider theme={theme}>
<Demo />
</ThemeProvider>,
document.getElementById("root")
);
在 TitleContext.Provider
外面再包一層 ColorContext.Provider
,所以 return 的東西就會同時有兩個 context
同樣地,在 DemoInnerBoxContent
裡面,就可以用 useContext
來使用 ColorContext
了
import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext
const TitleContext = createContext();
const ColorContext = createContext();
function DemoInnerBoxContent() {
const [title, setTitle] = useContext(TitleContext); // 我在 DemoInnerBoxContent 這層,要使用 TitleContext 這個 context 傳進來的值
const colors = useContext(ColorContext); // 使用 ColorContext 這個 context 傳進來的值
return (
<div>
<button
style={{
color: colors.primary, // 在這裡就可以使用 colors 了
}}
onClick={() => {
setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
}}
>
Update title!
</button>
{title}
</div>
);
}
function DemoInnerBox() {
return <DemoInnerBoxContent />;
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
// 在 TitleContext.Provider 這層 component,把 context 的值設為 setTitle 並傳下去
// 在 ColorContext.Provider 這層 component,把 context 的值設為一個物件並傳下去
return (
<ColorContext.Provider
value={{
primary: "#ff0000",
}}
>
<TitleContext.Provider value={[title, setTitle]}>
<div>
title: {title}
<DemoInner />
</div>
</TitleContext.Provider>
</ColorContext.Provider>
);
}
現在,我要做的功能是:
原本按鈕的字是紅色,點擊「Click me」按鈕後,字就會變成藍色
import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext
const ColorContext = createContext();
function DemoInnerBoxContent() {
const colors = useContext(ColorContext); // 使用 ColorContext 這個 context 傳進來的值
return (
<div>
<button
style={{
color: colors.primary, // 在這裡就可以使用 colors.primary 了
}}
onClick={() => {
// setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
}}
>
Update title!
</button>
</div>
);
}
function DemoInnerBox() {
return <DemoInnerBoxContent />;
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
const [colors, setColors] = useState({
primary: "#ff0000",
});
// 在 ColorContext.Provider 這層 component,把 context 的值設為一個物件並傳下去
return (
<ColorContext.Provider value={colors}>
<div>
<button
onClick={() => {
setColors({
primary: "#0000ff", // 點擊按鈕,就會切換 color
});
}}
>
Click me
</button>
title: {title}
<DemoInner />
</div>
</ColorContext.Provider>
);
}
用這樣子的方式,可以達成「dark theme / light theme」的切換
-> 我用的顏色都是 colors.primary
colors.primary
是黑色colors.primary
是白色我在 Demo
這層,primary 是 "#ff0000" (紅色)
在 DemoInner
這層,primary 還是 "#ff0000" (紅色)
但我在 DemoInnerBox
這層,把 primary 改成 "green" (綠色)
最後,在 DemoInnerBoxContent
這層,primary 就會是 "green" (綠色)
import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext
const ColorContext = createContext();
function DemoInnerBoxContent() {
const colors = useContext(ColorContext); // 使用 ColorContext 這個 context 傳進來的值
return (
<div>
<button
style={{
color: colors.primary, // 在這裡就可以使用 colors.primary 了
}}
onClick={() => {
// setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
}}
>
Update title!
</button>
</div>
);
}
function DemoInnerBox() {
return (
<ColorContext.Provider
value={{
primary: "green",
}}
>
<DemoInnerBoxContent />
</ColorContext.Provider>
);
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
const [colors, setColors] = useState({
primary: "#ff0000",
});
// 在 ColorContext.Provider 這層 component,把 context 的值設為一個物件並傳下去
return (
<ColorContext.Provider value={colors}>
<div>
<button
onClick={() => {
setColors({
primary: "#0000ff", // 點擊按鈕,就會切換 color
});
}}
>
Click me
</button>
title: {title}
<DemoInner />
</div>
</ColorContext.Provider>
);
}
原因是:
在 Demo
這層,我傳入的 context 是 primary: "#ff0000"
(red)
但是在下面的層,也就是 DemoInnerBox
,又再傳入一個 context 是 primary: "green"
(green)
因此,當我在 DemoInnerBoxContent
使用 ColorContext
時,會「往上層去找距離最近的 context」,所以就會找到 DemoInnerBox
的 context(會是 green)
那因為 <ThemeProvider>
是包在最上層,所以裡面的每一層都會拿到一樣的樣式
但其實也可以針對不同的按鈕或 component 去提供不同的 <ThemeProvider>
,讓按鈕有不同的樣式
但通常,一個 App 就只會有一個「統一的 theme」
ReactDOM.render(
<ThemeProvider theme={theme}>
<Demo />
</ThemeProvider>,
document.getElementById("root")
);
在一個 App 裡面,底下有很多個 component 都會需要用到同一個 state
假設是一個部落格網站的登入功能
在網站裡面的很多個 component 都會需要用到「使用者的登入狀態」這個 state,才能知道要 render 出什麼相對應的內容
例如:
有登入,才會 render 出「編輯文章的按鈕」
有登入,header 才會 render 出「管理後台的按鈕」
所以我就可以把「使用者的登入狀態」存在最上層的 state
,這樣下層的 component 才可以拿到
有了 context,我就可以不用每一層 component 都傳這個 state
的值,只要在「會需要用到這個 state 的那層」用 useContext
就可以拿到 context 的值了
有一個問題叫做 prop drilling
drill 就是「往下鑽」的意思
範例程式碼如下
Demo.js:
在 Demo
component 裡面,會 render 出 <DemoInner />
在 DemoInner
component 裡面,會 render 出 <DemoInnerBox />
在 DemoInnerBox
component 裡面,會 render 出 <DemoInnerBoxContent />
在 DemoInnerBoxContent
component 裡面,會 render 出 <button>Update title!</button>
這個按鈕
import React, { useState } from "react";
function DemoInnerBoxContent() {
return (
<div>
<button>Update title!</button>
</div>
);
}
function DemoInnerBox() {
return <DemoInnerBoxContent />;
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
return (
<div>
title: {title}
<DemoInner />
</div>
);
}
在 index.js 會 render 出 Demo
component:
import React from "react";
import ReactDOM from "react-dom";
import Demo from "./Demo";
import reportWebVitals from "./reportWebVitals";
import { ThemeProvider } from "styled-components";
ReactDOM.render(
<ThemeProvider theme={theme}>
<Demo />
</ThemeProvider>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
UI 長這樣:
我要做的功能是:
當我按下在 DemoInnerBoxContent
component 裡面的按鈕,就要去 update 「在 Demo
component 裡面的 title
」
做法是:
當我想要從 children 去 update parent 的 state
時,就要把這個 setTitle
這個 function 從 parent (也就是 DemoInner
component)傳下去
import React, { useState } from "react";
function DemoInnerBoxContent({ setTitle }) {
return (
<div>
<button
onClick={() => {
setTitle(Math.random());
}}
>
Update title!
</button>
</div>
);
}
function DemoInnerBox({ setTitle }) {
return <DemoInnerBoxContent setTitle={setTitle} />;
}
function DemoInner({ setTitle }) {
return <DemoInnerBox setTitle={setTitle} />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
return (
<div>
title: {title}
<DemoInner setTitle={setTitle} />
</div>
);
}
從 DemoInner
component 傳到 DemoInnerBox
component,
再從 DemoInnerBox
component 傳到 DemoInnerBoxContent
component 之後,
我才能在 DemoInnerBoxContent
component 拿到 setTitle
這個 function
遇到的問題是:
我需要傳「很多層」之後,我才可以在 DemoInnerBoxContent
component 拿到 setTitle
這個 function -> 我才能夠呼叫 setTitle
這個 function
這個問題就叫做 prop drilling:
當我的 component 有很多層的時候,我就要傳很多層才能傳到我要的地方,這樣就很麻煩,因為對於中間的那幾層(DemoInner
和 DemoInnerBox
)只是扮演一個中介者的角色,只負責把 prop
向下傳遞而已,它們根本不需要拿到 setTitle
這個 function
而且是,每一層都要傳,不然最後面的會收不到
這就是 useContext
要解決的問題
React 提供了一個方法來解決 prop drilling 的問題,這個方法就是 useContext
context 就是「上下文、脈絡」的意思
useContext
的用法如下:
useContext
, createContext
import React, { useState, useContext, createContext } from "react";
const TitleContext = createContext()
在 createContext()
裡面,要傳入的是:TitleContext
要提供的 value 初始值
在 React 裡面,可以透過 context 把「上層的東西」傳到下層
<TitleContext.Provider></TitleContext.Provider>
的 component 包住意思就是:<TitleContext.Provider></TitleContext.Provider>
這個 component 要提供「Context 的值」
因此,在 value
就可以填入「我要傳下去的 context」,也就是 setTitle
這個 function
-> 在 <TitleContext.Provider value={setTitle}></TitleContext.Provider>
這層 component,把 context 的值設為 setTitle
並傳下去
useContext
來使用 context 傳進來的值value
傳入什麼,在 useContext
就會回傳什麼TitleContext.Provider
底下的任何一層(也就是 DemoInner
, DemoInnerBox
, DemoInnerBoxContent
),都可以使用 setTitle
了在 DemoInnerBoxContent
裡面寫上 const setTitle = useContext(TitleContext)
,意思就是:我在 DemoInnerBoxContent
這層,要使用 TitleContext
這個 context 傳進來的值
透過 context 的方式,這些「中間層」就可以不用再扮演「傳遞 prop
」的角色了
Demo.js:
import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext
const TitleContext = createContext();
function DemoInnerBoxContent() {
const setTitle = useContext(TitleContext); // 我在 DemoInnerBoxContent 這層,要使用 TitleContext 這個 context 傳進來的值
return (
<div>
<button
onClick={() => {
setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
}}
>
Update title!
</button>
</div>
);
}
function DemoInnerBox() {
return <DemoInnerBoxContent />;
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
// 在 TitleContext.Provider 這層 component,把 context 的值設為 setTitle 並傳下去
return (
<TitleContext.Provider value={setTitle}>
<div>
title: {title}
<DemoInner />
</div>
</TitleContext.Provider>
);
}
value
傳下去的 context 可以是任何東西例如:可以是一個陣列
在 TitleContext.Provider
把 value
設為 [title, setTitle]
傳下去
import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext
const TitleContext = createContext();
function DemoInnerBoxContent() {
const [title, setTitle] = useContext(TitleContext); // 我在 DemoInnerBoxContent 這層,要使用 TitleContext 這個 context 傳進來的值
return (
<div>
<button
onClick={() => {
setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
}}
>
Update title!
</button>
{title}
</div>
);
}
function DemoInnerBox() {
return <DemoInnerBoxContent />;
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
// 在 TitleContext.Provider 這層 component,把 context 的值設為 setTitle 並傳下去
return (
<TitleContext.Provider value={[title, setTitle]}>
<div>
title: {title}
<DemoInner />
</div>
</TitleContext.Provider>
);
}
styled component 的 <ThemeProvider></ThemeProvider>
,背後就是用 Context 來實作的
index.js:
ReactDOM.render(
<ThemeProvider theme={theme}>
<Demo />
</ThemeProvider>,
document.getElementById("root")
);
在 TitleContext.Provider
外面再包一層 ColorContext.Provider
,所以 return 的東西就會同時有兩個 context
同樣地,在 DemoInnerBoxContent
裡面,就可以用 useContext
來使用 ColorContext
了
import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext
const TitleContext = createContext();
const ColorContext = createContext();
function DemoInnerBoxContent() {
const [title, setTitle] = useContext(TitleContext); // 我在 DemoInnerBoxContent 這層,要使用 TitleContext 這個 context 傳進來的值
const colors = useContext(ColorContext); // 使用 ColorContext 這個 context 傳進來的值
return (
<div>
<button
style={{
color: colors.primary, // 在這裡就可以使用 colors 了
}}
onClick={() => {
setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
}}
>
Update title!
</button>
{title}
</div>
);
}
function DemoInnerBox() {
return <DemoInnerBoxContent />;
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
// 在 TitleContext.Provider 這層 component,把 context 的值設為 setTitle 並傳下去
// 在 ColorContext.Provider 這層 component,把 context 的值設為一個物件並傳下去
return (
<ColorContext.Provider
value={{
primary: "#ff0000",
}}
>
<TitleContext.Provider value={[title, setTitle]}>
<div>
title: {title}
<DemoInner />
</div>
</TitleContext.Provider>
</ColorContext.Provider>
);
}
現在,我要做的功能是:
原本按鈕的字是紅色,點擊「Click me」按鈕後,字就會變成藍色
import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext
const ColorContext = createContext();
function DemoInnerBoxContent() {
const colors = useContext(ColorContext); // 使用 ColorContext 這個 context 傳進來的值
return (
<div>
<button
style={{
color: colors.primary, // 在這裡就可以使用 colors.primary 了
}}
onClick={() => {
// setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
}}
>
Update title!
</button>
</div>
);
}
function DemoInnerBox() {
return <DemoInnerBoxContent />;
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
const [colors, setColors] = useState({
primary: "#ff0000",
});
// 在 ColorContext.Provider 這層 component,把 context 的值設為一個物件並傳下去
return (
<ColorContext.Provider value={colors}>
<div>
<button
onClick={() => {
setColors({
primary: "#0000ff", // 點擊按鈕,就會切換 color
});
}}
>
Click me
</button>
title: {title}
<DemoInner />
</div>
</ColorContext.Provider>
);
}
用這樣子的方式,可以達成「dark theme / light theme」的切換
-> 我用的顏色都是 colors.primary
colors.primary
是黑色colors.primary
是白色我在 Demo
這層,primary 是 "#ff0000" (紅色)
在 DemoInner
這層,primary 還是 "#ff0000" (紅色)
但我在 DemoInnerBox
這層,把 primary 改成 "green" (綠色)
最後,在 DemoInnerBoxContent
這層,primary 就會是 "green" (綠色)
import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext
const ColorContext = createContext();
function DemoInnerBoxContent() {
const colors = useContext(ColorContext); // 使用 ColorContext 這個 context 傳進來的值
return (
<div>
<button
style={{
color: colors.primary, // 在這裡就可以使用 colors.primary 了
}}
onClick={() => {
// setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
}}
>
Update title!
</button>
</div>
);
}
function DemoInnerBox() {
return (
<ColorContext.Provider
value={{
primary: "green",
}}
>
<DemoInnerBoxContent />
</ColorContext.Provider>
);
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
const [colors, setColors] = useState({
primary: "#ff0000",
});
// 在 ColorContext.Provider 這層 component,把 context 的值設為一個物件並傳下去
return (
<ColorContext.Provider value={colors}>
<div>
<button
onClick={() => {
setColors({
primary: "#0000ff", // 點擊按鈕,就會切換 color
});
}}
>
Click me
</button>
title: {title}
<DemoInner />
</div>
</ColorContext.Provider>
);
}
原因是:
在 Demo
這層,我傳入的 context 是 primary: "#ff0000"
(red)
但是在下面的層,也就是 DemoInnerBox
,又再傳入一個 context 是 primary: "green"
(green)
因此,當我在 DemoInnerBoxContent
使用 ColorContext
時,會「往上層去找距離最近的 context」,所以就會找到 DemoInnerBox
的 context(會是 green)
那因為 <ThemeProvider>
是包在最上層,所以裡面的每一層都會拿到一樣的樣式
但其實也可以針對不同的按鈕或 component 去提供不同的 <ThemeProvider>
,讓按鈕有不同的樣式
但通常,一個 App 就只會有一個「統一的 theme」
ReactDOM.render(
<ThemeProvider theme={theme}>
<Demo />
</ThemeProvider>,
document.getElementById("root")
);
在一個 App 裡面,底下有很多個 component 都會需要用到同一個 state
假設是一個部落格網站的登入功能
在網站裡面的很多個 component 都會需要用到「使用者的登入狀態」這個 state,才能知道要 render 出什麼相對應的內容
例如:
有登入,才會 render 出「編輯文章的按鈕」
有登入,header 才會 render 出「管理後台的按鈕」
所以我就可以把「使用者的登入狀態」存在最上層的 state
,這樣下層的 component 才可以拿到
有了 context,我就可以不用每一層 component 都傳這個 state
的值,只要在「會需要用到這個 state 的那層」用 useContext
就可以拿到 context 的值了
從 React 16.8 之後,就完全不再使用 class component 了,都是使用 function component 來寫
要寫 class component,要先有幾個背景知識:
this
contructor()
this.props
可以拿到這個 component 的 props範例:用 class component 來寫 Button
要先把 React
引入進來
extends React.Component
意思是:這會是一個 React 的 componentrender()
這個 methodthis.props
可以拿到這個 component 的 props{ onclick, children }
一樣是解構的語法return
後面接上我要 render 出來的東西import React, { memo, useMemo } from "react"; // 從 React 引入 memo, useMemo
// 用 class component 來寫 Button
class Button extends React.Component {
render() {
const { onclick, children } = this.props;
return <button onClick={onclick}>{children}</button>;
}
}
讓 handleToggleClick()
和 handleDeleteClick()
變成這個 component 的一個 method
// 用 class component 來寫 TodoItem
export default class TodoItemC extends React.Component {
handleToggleClick() {
const { handleToggleIsFinished, todo } = this.props;
handleToggleIsFinished(todo.id);
}
handleDeleteClick() {
const { handleDeleteTodo, todo } = this.props;
handleDeleteTodo(todo.id);
}
render() {
const { className, size, todo } = this.props;
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent $isFinished={todo.isFinished} size={size}>
{todo.content}
</TodoContent>
<a href={window.encodeURIComponent(todo.content)}>click me!</a>
<TodoButtonWrapper>
<Button onClick={this.handleToggleClick}>
{todo.isFinished && "未完成"}
{!todo.isFinished && "已完成"}
</Button>
<RedButton onClick={this.handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
);
}
}
這樣寫完之後,頁面上會出現這樣一個錯誤:針對這行 const { handleDeleteTodo, todo } = this.props;
,Cannot read property 'props' of undefined
這個錯誤的意思就是:this.props
的 this
是 undefined,在 undefined 上面沒有 props
這個 property
因為 this
是 undefinded,所以 TodoItem 無法正常運作
this
會是 undefined 呢?<Button onClick={this.handleToggleClick}>
在 Button
onClick 的時候,傳進去一個 function 叫做 this.handleToggleClick
this
的值是什麼以下面這個 Add todo 按鈕為例:
因為在 <button></button>
裡面是直接去 call onclick()
這個 function
所以,在嚴格模式下,onclick()
這個 function 裡面拿到的 this
會是預設的 undefined
// 用 class component 來寫 Add todo 按鈕
class Button extends React.Component {
render() {
const { onclick, children } = this.props;
return <button onClick={onclick}>{children}</button>;
}
}
this.props.onclick()
來呼叫 onclick()
,那在 onclick()
裡面的 this
就會是 this.props
a.b.onclick()
來呼叫 onclick()
,那在 onclick()
裡面的 this
就會是 a.b
this
呢?有幾種方式:
constructor()
拿到 this
在 class component 裡面有一個 method 叫做 constructor()
constructor()
小括號裡面,會給我一個 props
因為物件導向的關係,要先用 super(props)
讓 React.Component
也吃到這個 props
,才能幫我做 props
的初始化
做完 props
的初始化之後,要用 bind
的方式,讓我每次在呼叫 this.handleToggleClick
時,裡面的 this
都會是現在這個 constructor 裡面的 this
--> 也就是 TodoItemC
這個 component
// 用 class component 來寫 TodoItem
export default class TodoItemC extends React.Component {
constructor(props) {
super(props);
this.handleToggleClick = this.handleToggleClick.bind(this);
this.handleDeleteClick = this.handleDeleteClick.bind(this);
}
handleToggleClick() {
const { handleToggleIsFinished, todo } = this.props;
handleToggleIsFinished(todo.id);
}
handleDeleteClick() {
const { handleDeleteTodo, todo } = this.props;
handleDeleteTodo(todo.id);
}
render() {
const { className, size, todo } = this.props;
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent $isFinished={todo.isFinished} size={size}>
{todo.content}
</TodoContent>
<a href={window.encodeURIComponent(todo.content)}>click me!</a>
<TodoButtonWrapper>
<Button onClick={this.handleToggleClick}>
{todo.isFinished && "未完成"}
{!todo.isFinished && "已完成"}
</Button>
<RedButton onClick={this.handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
);
}
}
把 handleToggleClick
和 handleDeleteClick
改為類似箭頭函式的寫法後,就會自動去幫我 bind 這個 this
當我在呼叫 handleToggleClick
和 handleDeleteClick
這兩個 function 時,裡面的 this
預設就會是 TodoItemC
這個 instance 了
// 用 class component 來寫 TodoItem
export default class TodoItemC extends React.Component {
handleToggleClick = () => {
const { handleToggleIsFinished, todo } = this.props;
handleToggleIsFinished(todo.id);
};
handleDeleteClick = () => {
const { handleDeleteTodo, todo } = this.props;
handleDeleteTodo(todo.id);
};
render() {
const { className, size, todo } = this.props;
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent $isFinished={todo.isFinished} size={size}>
{todo.content}
</TodoContent>
<a href={window.encodeURIComponent(todo.content)}>click me!</a>
<TodoButtonWrapper>
<Button onClick={this.handleToggleClick}>
{todo.isFinished && "未完成"}
{!todo.isFinished && "已完成"}
</Button>
<RedButton onClick={this.handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
);
}
}
如果要在 class component 使用 state,要這樣寫:
在 constructor(){}
裡面,記得要先用 super(props)
做初始化
constructor(){}
裡面,用 this.state
來指定 state 的初始值setState()
來設定 state設定讓 counter
等於 this.state.counter + 1
this.state
來取得「我現在的 state」// 用 class component 來寫 TodoItem
export default class TodoItemC extends React.Component {
constructor(props) {
super(props);
// 在 class component 使用 state
this.state = {
counter: 1,
};
}
handleToggleClick = () => {
const { handleToggleIsFinished, todo } = this.props;
handleToggleIsFinished(todo.id);
// 在 class component 設置 state
this.setState({
counter: this.state.counter + 1,
});
};
handleDeleteClick = () => {
const { handleDeleteTodo, todo } = this.props;
handleDeleteTodo(todo.id);
};
render() {
const { className, size, todo } = this.props;
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent $isFinished={todo.isFinished} size={size}>
{todo.content}
</TodoContent>
<a href={window.encodeURIComponent(todo.content)}>click me!</a>
<TodoButtonWrapper>
<Button onClick={this.handleToggleClick}>
{todo.isFinished && "未完成"}
{!todo.isFinished && "已完成"}
</Button>
<RedButton onClick={this.handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
);
}
}
範例:用一個 counter 的 App 來做講解
index.js:
import React from "react";
import ReactDOM from "react-dom";
import Counter from "./Counter";
import reportWebVitals from "./reportWebVitals";
import { ThemeProvider } from "styled-components";
ReactDOM.render(
<ThemeProvider theme={theme}>
<Counter />
</ThemeProvider>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
Counter.js:
import React from "react";
export default class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 1,
};
}
handleClick = () => {
this.setState({
counter: this.state.counter + 1,
});
};
render() {
const { counter } = this.state;
return (
<div>
<button onClick={this.handleClick}>+ 1</button>
Counter: {counter}
</div>
);
}
}
這樣寫好後,當我點擊按鈕,Counter 就會 +1
class component 有幾個週期,都是用 class 的 method 做的,這些 methods 就叫做 “lifecycle methods”
this
的值。this
的值就是這個 component我不需要像之前那樣,要改寫成類似箭頭函式才能拿到 this
的值
這些週期分別是:
componentDidMount
用 componentDidMount()
這個 method
componentDidMount()
就是:我在 component mount 之後,想要執行什麼事情,就寫在 componentDidMount()
這裡componentDidUpdate
componentDidUpdate
就是:我在 component update 之後,想要執行什麼事情,就寫在 componentDidUpdate()
這裡componentDidUpdate
會給我「prevProps
(前一次的 props
)」和「prevState
(前一次的 state
)」這兩個參數
componentWillUnmount
componentWillUnmount
就是:我在 component unmount 之前,想要執行什麼事情,就寫在 componentWillUnmount()
這裡把這個 component 從畫面上移除(不 render 它)時,就叫做 unmount
Counter.js:
透過在各個 lifecycle 裡面加上 console.log
,來觀察各個 lifecycle
import React from "react";
export default class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 1,
};
console.log("constructor");
}
componentDidMount() {
console.log("did mount", this.state);
}
componentDidUpdate(prevProps, prevState) {
console.log("prevState: ", prevState);
console.log("update!");
}
componentWillUnmount() {
console.log("unmount");
}
handleClick = () => {
this.setState({
counter: this.state.counter + 1,
});
};
render() {
const { counter } = this.state;
console.log("render");
return (
<div>
<button onClick={this.handleClick}>+ 1</button>
Counter: {counter}
</div>
);
}
}
componentDidMount
)當我打開網頁時,在 console 就會印出這幾行:constructor
render
did mount {counter: 1}
以下說明會印出這幾行的原因:
constructor
是因為:要先執行 constructor
來建立 componentrender
是因為:call render()
這個 functiondid mount {counter: 1}
是因為:只有在第一次 render 完,當 component mount 到畫面上之後,會執行 componentDidMount
。所以 componentDidMount
只會有一次componentDidUpdate
)當我第一次按下「+1 按鈕」時,在 console 就會印出這幾行:render
prevState: {counter: 1}
update!
以下說明會印出這幾行的原因:
render
是因為:每當 state 改變時,就會去 call render()
這個 function,所以就會印出 console.log("render")
prevState: {counter: 1}
是因為:call 完 render()
後,component 會更新,所以就會執行 componentDidUpdate
,裡面就會印出 console.log("prevState: ", prevState)
和 console.log("update!")
componentWillUnmount
)如果想要測試 componentWillUnmount
,可以另外再寫一個 Test
component,如下:
class Test extends React.Component {
componentDidMount() {
console.log("test mount");
}
componentWillUnmount() {
console.log("test unmount");
}
render() {
return <div>123</div>;
}
}
然後,在 Counter
render 出 Test
這個 component,但是是有條件的:
{counter === 1 && <Test />}
當 counter === 1
時,才會 render 出 Test
這個 component
所以,當 counter
變成 2 時,Test
就會 unmount(執行 componentWillUnmount
),在 console 就會印出 test unmount
...
render() {
const { counter } = this.state;
console.log("render");
return (
<div>
<button onClick={this.handleClick}>+ 1</button>
Counter: {counter}
{counter === 1 && <Test />}
</div>
);
}
如下圖
shouldComponentUpdate
nextProps, nextState
這兩個參數,來決定要不要 updateReact 要 update 之前,會去 call shouldComponentUpdate
這個 function
如果 return false
的話,就不會 update
如果 return true
的話,才會 update
shouldComponentUpdate(nextProps, nextState) {
if (nextState.counter > 5) return false;
return true;
}
上面這樣寫的意思是:
當 counter > 5 時,整個 component 就不會再 re-render,也不會再 update 了!
shouldComponentUpdate
更常用的用法是:自己去比對每一個 props
是否一樣(可以自己自訂比較的方式)如果每一個 props
都一樣的話,就不需要 update 了
如果有 props
裡面的屬性改變,才需要 update
想要去比對每一個 props
是否一樣,可以自己自訂比較的方式,也可以使用 React 內建的 PureComponent
PureComponent
PureComponent
跟 memo
是一樣的東西
使用 PureComponent
之後,React 會自動幫我做優化:
shouldComponentUpdate
這個 method,也就是會自動設定成:當有 props
裡面的的屬性改變時,才需要 updateexport default class Counter extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
counter: 1,
};
console.log("constructor");
}
...
constructor
render()
componentDidMount
setState()
」或是「call 了 forceUpdate()
」,都會 update(都會 re-render)componentDidUpdate
在 component unmount 之前,會 call componentWillUnmount
class component 和 function component 背後的概念、思考方式差滿多的
render()
這個 function 而已componentDidMount
, componentDidUpdate
, componentWillMount
裡面 -> 每一個階段都有對應的 life cycle,就把想做的事情寫在裡面即可class component 關注的是:我在每一個 life cycle 要做什麼事情 -> 有「life cycle」的概念
但是,function component 就很不一樣
function component 每次 re-render 時,就會「重新執行整個 function component」-> function component 並沒有 render()
這個 method,可以看成是:function component 自己本身就是 render()
這個 function
function component 的生命週期是改用另一種方式來做,就是 useEffect
useEffect
就是 -> component render 完,browser 也 paint 到畫面上之後要做什麼事情
function component 沒有分不同的 life cycle,就只剩下每一次的 render
function component 關注的不是「在某個 life cycle 要做什麼事」
function component 要關注的是「當這個 function component 重新 render 完,或是某個 state
、某個 props
改變時,要做什麼事」
基本上,在 class component 的每個 life cycle 能做到的事情,在 function component 的 useEffect
也能做到:
useEffect
如果沒有放上第二個參數(dependency array),那效果就跟 componentDidMount
差不多-> 就是「在 component mount 之後」要執行什麼事情useEffect
放上第二個參數(dependency array),就可以達成像是 componentDidUpdate
的效果如下面的程式碼,意思就是:當 todos
的 state 改變時(有 update 時),就做 useEffect
裡面寫的事情
useEffect(() => {
if (!todos) return; // 如果沒有 todos 的話,就直接 return
writeTodosToLocalStorage(todos);
}, [todos]);
useEffect
裡面 return 另一個 function,就會跟 componentWillUnmount
的效果差不多如下程式碼,在 useEffect
裡面 return 的東西,就是在 component unmount 時要做的事情
useEffect(() => {
if (!todos) return; // 如果沒有 todos 的話,就直接 return
writeTodosToLocalStorage(todos);
return () => {
};
}, []);
]]>從 React 16.8 之後,就完全不再使用 class component 了,都是使用 function component 來寫
要寫 class component,要先有幾個背景知識:
this
contructor()
this.props
可以拿到這個 component 的 props範例:用 class component 來寫 Button
要先把 React
引入進來
extends React.Component
意思是:這會是一個 React 的 componentrender()
這個 methodthis.props
可以拿到這個 component 的 props{ onclick, children }
一樣是解構的語法return
後面接上我要 render 出來的東西import React, { memo, useMemo } from "react"; // 從 React 引入 memo, useMemo
// 用 class component 來寫 Button
class Button extends React.Component {
render() {
const { onclick, children } = this.props;
return <button onClick={onclick}>{children}</button>;
}
}
讓 handleToggleClick()
和 handleDeleteClick()
變成這個 component 的一個 method
// 用 class component 來寫 TodoItem
export default class TodoItemC extends React.Component {
handleToggleClick() {
const { handleToggleIsFinished, todo } = this.props;
handleToggleIsFinished(todo.id);
}
handleDeleteClick() {
const { handleDeleteTodo, todo } = this.props;
handleDeleteTodo(todo.id);
}
render() {
const { className, size, todo } = this.props;
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent $isFinished={todo.isFinished} size={size}>
{todo.content}
</TodoContent>
<a href={window.encodeURIComponent(todo.content)}>click me!</a>
<TodoButtonWrapper>
<Button onClick={this.handleToggleClick}>
{todo.isFinished && "未完成"}
{!todo.isFinished && "已完成"}
</Button>
<RedButton onClick={this.handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
);
}
}
這樣寫完之後,頁面上會出現這樣一個錯誤:針對這行 const { handleDeleteTodo, todo } = this.props;
,Cannot read property 'props' of undefined
這個錯誤的意思就是:this.props
的 this
是 undefined,在 undefined 上面沒有 props
這個 property
因為 this
是 undefinded,所以 TodoItem 無法正常運作
this
會是 undefined 呢?<Button onClick={this.handleToggleClick}>
在 Button
onClick 的時候,傳進去一個 function 叫做 this.handleToggleClick
this
的值是什麼以下面這個 Add todo 按鈕為例:
因為在 <button></button>
裡面是直接去 call onclick()
這個 function
所以,在嚴格模式下,onclick()
這個 function 裡面拿到的 this
會是預設的 undefined
// 用 class component 來寫 Add todo 按鈕
class Button extends React.Component {
render() {
const { onclick, children } = this.props;
return <button onClick={onclick}>{children}</button>;
}
}
this.props.onclick()
來呼叫 onclick()
,那在 onclick()
裡面的 this
就會是 this.props
a.b.onclick()
來呼叫 onclick()
,那在 onclick()
裡面的 this
就會是 a.b
this
呢?有幾種方式:
constructor()
拿到 this
在 class component 裡面有一個 method 叫做 constructor()
constructor()
小括號裡面,會給我一個 props
因為物件導向的關係,要先用 super(props)
讓 React.Component
也吃到這個 props
,才能幫我做 props
的初始化
做完 props
的初始化之後,要用 bind
的方式,讓我每次在呼叫 this.handleToggleClick
時,裡面的 this
都會是現在這個 constructor 裡面的 this
--> 也就是 TodoItemC
這個 component
// 用 class component 來寫 TodoItem
export default class TodoItemC extends React.Component {
constructor(props) {
super(props);
this.handleToggleClick = this.handleToggleClick.bind(this);
this.handleDeleteClick = this.handleDeleteClick.bind(this);
}
handleToggleClick() {
const { handleToggleIsFinished, todo } = this.props;
handleToggleIsFinished(todo.id);
}
handleDeleteClick() {
const { handleDeleteTodo, todo } = this.props;
handleDeleteTodo(todo.id);
}
render() {
const { className, size, todo } = this.props;
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent $isFinished={todo.isFinished} size={size}>
{todo.content}
</TodoContent>
<a href={window.encodeURIComponent(todo.content)}>click me!</a>
<TodoButtonWrapper>
<Button onClick={this.handleToggleClick}>
{todo.isFinished && "未完成"}
{!todo.isFinished && "已完成"}
</Button>
<RedButton onClick={this.handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
);
}
}
把 handleToggleClick
和 handleDeleteClick
改為類似箭頭函式的寫法後,就會自動去幫我 bind 這個 this
當我在呼叫 handleToggleClick
和 handleDeleteClick
這兩個 function 時,裡面的 this
預設就會是 TodoItemC
這個 instance 了
// 用 class component 來寫 TodoItem
export default class TodoItemC extends React.Component {
handleToggleClick = () => {
const { handleToggleIsFinished, todo } = this.props;
handleToggleIsFinished(todo.id);
};
handleDeleteClick = () => {
const { handleDeleteTodo, todo } = this.props;
handleDeleteTodo(todo.id);
};
render() {
const { className, size, todo } = this.props;
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent $isFinished={todo.isFinished} size={size}>
{todo.content}
</TodoContent>
<a href={window.encodeURIComponent(todo.content)}>click me!</a>
<TodoButtonWrapper>
<Button onClick={this.handleToggleClick}>
{todo.isFinished && "未完成"}
{!todo.isFinished && "已完成"}
</Button>
<RedButton onClick={this.handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
);
}
}
如果要在 class component 使用 state,要這樣寫:
在 constructor(){}
裡面,記得要先用 super(props)
做初始化
constructor(){}
裡面,用 this.state
來指定 state 的初始值setState()
來設定 state設定讓 counter
等於 this.state.counter + 1
this.state
來取得「我現在的 state」// 用 class component 來寫 TodoItem
export default class TodoItemC extends React.Component {
constructor(props) {
super(props);
// 在 class component 使用 state
this.state = {
counter: 1,
};
}
handleToggleClick = () => {
const { handleToggleIsFinished, todo } = this.props;
handleToggleIsFinished(todo.id);
// 在 class component 設置 state
this.setState({
counter: this.state.counter + 1,
});
};
handleDeleteClick = () => {
const { handleDeleteTodo, todo } = this.props;
handleDeleteTodo(todo.id);
};
render() {
const { className, size, todo } = this.props;
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent $isFinished={todo.isFinished} size={size}>
{todo.content}
</TodoContent>
<a href={window.encodeURIComponent(todo.content)}>click me!</a>
<TodoButtonWrapper>
<Button onClick={this.handleToggleClick}>
{todo.isFinished && "未完成"}
{!todo.isFinished && "已完成"}
</Button>
<RedButton onClick={this.handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
);
}
}
範例:用一個 counter 的 App 來做講解
index.js:
import React from "react";
import ReactDOM from "react-dom";
import Counter from "./Counter";
import reportWebVitals from "./reportWebVitals";
import { ThemeProvider } from "styled-components";
ReactDOM.render(
<ThemeProvider theme={theme}>
<Counter />
</ThemeProvider>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
Counter.js:
import React from "react";
export default class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 1,
};
}
handleClick = () => {
this.setState({
counter: this.state.counter + 1,
});
};
render() {
const { counter } = this.state;
return (
<div>
<button onClick={this.handleClick}>+ 1</button>
Counter: {counter}
</div>
);
}
}
這樣寫好後,當我點擊按鈕,Counter 就會 +1
class component 有幾個週期,都是用 class 的 method 做的,這些 methods 就叫做 “lifecycle methods”
this
的值。this
的值就是這個 component我不需要像之前那樣,要改寫成類似箭頭函式才能拿到 this
的值
這些週期分別是:
componentDidMount
用 componentDidMount()
這個 method
componentDidMount()
就是:我在 component mount 之後,想要執行什麼事情,就寫在 componentDidMount()
這裡componentDidUpdate
componentDidUpdate
就是:我在 component update 之後,想要執行什麼事情,就寫在 componentDidUpdate()
這裡componentDidUpdate
會給我「prevProps
(前一次的 props
)」和「prevState
(前一次的 state
)」這兩個參數
componentWillUnmount
componentWillUnmount
就是:我在 component unmount 之前,想要執行什麼事情,就寫在 componentWillUnmount()
這裡把這個 component 從畫面上移除(不 render 它)時,就叫做 unmount
Counter.js:
透過在各個 lifecycle 裡面加上 console.log
,來觀察各個 lifecycle
import React from "react";
export default class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 1,
};
console.log("constructor");
}
componentDidMount() {
console.log("did mount", this.state);
}
componentDidUpdate(prevProps, prevState) {
console.log("prevState: ", prevState);
console.log("update!");
}
componentWillUnmount() {
console.log("unmount");
}
handleClick = () => {
this.setState({
counter: this.state.counter + 1,
});
};
render() {
const { counter } = this.state;
console.log("render");
return (
<div>
<button onClick={this.handleClick}>+ 1</button>
Counter: {counter}
</div>
);
}
}
componentDidMount
)當我打開網頁時,在 console 就會印出這幾行:constructor
render
did mount {counter: 1}
以下說明會印出這幾行的原因:
constructor
是因為:要先執行 constructor
來建立 componentrender
是因為:call render()
這個 functiondid mount {counter: 1}
是因為:只有在第一次 render 完,當 component mount 到畫面上之後,會執行 componentDidMount
。所以 componentDidMount
只會有一次componentDidUpdate
)當我第一次按下「+1 按鈕」時,在 console 就會印出這幾行:render
prevState: {counter: 1}
update!
以下說明會印出這幾行的原因:
render
是因為:每當 state 改變時,就會去 call render()
這個 function,所以就會印出 console.log("render")
prevState: {counter: 1}
是因為:call 完 render()
後,component 會更新,所以就會執行 componentDidUpdate
,裡面就會印出 console.log("prevState: ", prevState)
和 console.log("update!")
componentWillUnmount
)如果想要測試 componentWillUnmount
,可以另外再寫一個 Test
component,如下:
class Test extends React.Component {
componentDidMount() {
console.log("test mount");
}
componentWillUnmount() {
console.log("test unmount");
}
render() {
return <div>123</div>;
}
}
然後,在 Counter
render 出 Test
這個 component,但是是有條件的:
{counter === 1 && <Test />}
當 counter === 1
時,才會 render 出 Test
這個 component
所以,當 counter
變成 2 時,Test
就會 unmount(執行 componentWillUnmount
),在 console 就會印出 test unmount
...
render() {
const { counter } = this.state;
console.log("render");
return (
<div>
<button onClick={this.handleClick}>+ 1</button>
Counter: {counter}
{counter === 1 && <Test />}
</div>
);
}
如下圖
shouldComponentUpdate
nextProps, nextState
這兩個參數,來決定要不要 updateReact 要 update 之前,會去 call shouldComponentUpdate
這個 function
如果 return false
的話,就不會 update
如果 return true
的話,才會 update
shouldComponentUpdate(nextProps, nextState) {
if (nextState.counter > 5) return false;
return true;
}
上面這樣寫的意思是:
當 counter > 5 時,整個 component 就不會再 re-render,也不會再 update 了!
shouldComponentUpdate
更常用的用法是:自己去比對每一個 props
是否一樣(可以自己自訂比較的方式)如果每一個 props
都一樣的話,就不需要 update 了
如果有 props
裡面的屬性改變,才需要 update
想要去比對每一個 props
是否一樣,可以自己自訂比較的方式,也可以使用 React 內建的 PureComponent
PureComponent
PureComponent
跟 memo
是一樣的東西
使用 PureComponent
之後,React 會自動幫我做優化:
shouldComponentUpdate
這個 method,也就是會自動設定成:當有 props
裡面的的屬性改變時,才需要 updateexport default class Counter extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
counter: 1,
};
console.log("constructor");
}
...
constructor
render()
componentDidMount
setState()
」或是「call 了 forceUpdate()
」,都會 update(都會 re-render)componentDidUpdate
在 component unmount 之前,會 call componentWillUnmount
class component 和 function component 背後的概念、思考方式差滿多的
render()
這個 function 而已componentDidMount
, componentDidUpdate
, componentWillMount
裡面 -> 每一個階段都有對應的 life cycle,就把想做的事情寫在裡面即可class component 關注的是:我在每一個 life cycle 要做什麼事情 -> 有「life cycle」的概念
但是,function component 就很不一樣
function component 每次 re-render 時,就會「重新執行整個 function component」-> function component 並沒有 render()
這個 method,可以看成是:function component 自己本身就是 render()
這個 function
function component 的生命週期是改用另一種方式來做,就是 useEffect
useEffect
就是 -> component render 完,browser 也 paint 到畫面上之後要做什麼事情
function component 沒有分不同的 life cycle,就只剩下每一次的 render
function component 關注的不是「在某個 life cycle 要做什麼事」
function component 要關注的是「當這個 function component 重新 render 完,或是某個 state
、某個 props
改變時,要做什麼事」
基本上,在 class component 的每個 life cycle 能做到的事情,在 function component 的 useEffect
也能做到:
useEffect
如果沒有放上第二個參數(dependency array),那效果就跟 componentDidMount
差不多-> 就是「在 component mount 之後」要執行什麼事情useEffect
放上第二個參數(dependency array),就可以達成像是 componentDidUpdate
的效果如下面的程式碼,意思就是:當 todos
的 state 改變時(有 update 時),就做 useEffect
裡面寫的事情
useEffect(() => {
if (!todos) return; // 如果沒有 todos 的話,就直接 return
writeTodosToLocalStorage(todos);
}, [todos]);
useEffect
裡面 return 另一個 function,就會跟 componentWillUnmount
的效果差不多如下程式碼,在 useEffect
裡面 return 的東西,就是在 component unmount 時要做的事情
useEffect(() => {
if (!todos) return; // 如果沒有 todos 的話,就直接 return
writeTodosToLocalStorage(todos);
return () => {
};
}, []);
]]>
React 的 component 在 render 的時候,並不會直接產生出一個真的 DOM,而是 render 出一個 virtual DOM(會是一個 JS 的 object)
例如下面的 component:
<div className="App">
<input
type="text"
placeholder="add todo"
value={value}
onChange={handleChange}
/>
<button onClick={handleButtonClick}>Add todo</button>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
handleDeleteTodo={handleDeleteTodo}
handleToggleIsFinished={handleToggleIsFinished}
/>
))}
</div>
render 時產生的 virtual DOM 會是這樣:(就是一個 JS 的 object)
Reace 會把 tag: 'div'
這行 render 成 <div></div>
{
tag: 'div',
props: {
className: 'App'
}
children: {
}
}
找出「前一個 virtual DOM」和「後一個 virtual DOM」之間的差異後,再把這些「需要改變的地方」應用到真的 DOM 上面
reconciliation 就是:調和、比對差異的過程
因為有了這層 virtual DOM,讓 React 可以透過「比對 virtual DOM 的差異(Reconciliation)」,快速找出需要改變的地方,就不用每次改變 state 時就要全部清空重新 render(很沒效率),只需要 re-render 有改變的地方即可
前面有提到 virtual DOM 是一個 JS 的 object(如下程式碼),React 會把這個 object render 成 HTML DOM
{
tag: 'div',
props: {
className: 'App'
}
children: {
}
}
其實這個 virtual DOM 不只可以 render 成 HTML DOM(做成網站),也就是說:不一定要呈現在瀏覽器上面
tag: 'div'
render 成 ## div
Virtual DOM | 為了瞭解原理,那就來實作一個簡易 Virtual DOM 吧!
從頭打造一個簡單的 Virtual DOM
第一個層面是:
當 component 的 state 改變時,就會再 call 一次 function component
「再 call 一次 function component」這個行為就稱為 re-render
第二個層面是:
找出 DOM diff 之後,把有差異的地方應用到真正的 DOM 上面 -> 這個行為也稱為 re-render
所以,有可能發生的事情是:
state 改變了 -> 有 re-render(再 call 一次 function component)
這個改變的 state 並不會影響到 UI -> 在 virtual DOM 會做一次 DOM diff,但是因為 virtual DOM 並沒有變,所以最後並不會有東西被應用到真的 DOM 上面去
雖然,上面這樣做的效能已經比「每次 state 改變都把畫面清空,再真的把東西放到 DOM 上面去」還要好
但是其實,因為畫面根本不會改變,所以根本就不需要 re-render
那要怎麼解決這個 re-render 的問題呢?
首先,來示範一下這個 re-render 的問題
先把 Add todo 按鈕變成一個 component
每一次 render Button
時,都會在 console 印出 "render button"
// Add todo 按鈕
function Button({ onclick, children }) {
console.log("render button");
return <button onClick={onclick}>{children}</button>;
}
function App() {
// 用解構的語法,拿到「從各個 hooks 回傳的東西」
const {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
} = useTodos();
// UI
return (
<div className="App">
<input
type="text"
placeholder="add todo"
value={value}
onChange={handleChange}
/>
<Button onclick={handleButtonClick}>Add todo</Button>
...
這個 re-render 的問題就是:
我只要在 input 欄位打一個字母,Button
就會 re-render 一次
可是,我打這些字母跟 Button
沒有任何關係,Button
不應該 re-render 啊
那要怎麼讓 Button
不要 re-render 呢?
memo()
把 component 包起來React 有提供一個 hook 叫做 memo
,把 Button
這個 component 用 memo()
包起來
memo()
把 component 包起來之後,React 就會自動檢測:如果傳給這個 component 的 props 都沒有變的話,就不會 re-render在此範例中,傳給 Button
的 props 就是 { onclick, children }
,只有當 onclick
或 children
有變的話,才會 re-render
...
import { memo } from "react"; // 從 React 引入 memo
// Add todo 按鈕
function Button({ onclick, children }) {
console.log("render button");
return <button onClick={onclick}>{children}</button>;
}
const MemoButton = memo(Button); // 把 Button 這個 component 用 memo 包起來
function App() {
// 用解構的語法,拿到「從各個 hooks 回傳的東西」
const {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
} = useTodos();
// UI
return (
<div className="App">
<input
type="text"
placeholder="add todo"
value={value}
onChange={handleChange}
/>
<MemoButton onclick={handleButtonClick}>Add todo</MemoButton>
...
改成 MemoButton
之後,會發現:當我在 input 欄位輸入字母,還是會一直 re-render 啊?原因是:
Button
的 props 有 onclick
和 children
children
(也就是 Add todo)沒有變,但是 onclick
會變
因為 onclick={handleButtonClick}
裡面的 handleButtonClick
會變,為什麼呢?
因為:
每當我在 input 欄位輸入一個字母,function App()
都會重新執行一次 -> 在 function App()
裡面的 useTodos()
也會重新執行一次 -> 在 useTodos()
這個 hook 裡面,handleButtonClick
是用 const
宣告的:
// 新增 todo
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
};
也就是說,每執行一次 function useTodos()
,就會重新宣告一次 handleButtonClick
(重新產生一個新的 function)
例如我執行了兩次 useTodos()
useTodos();
useTodos();
就等於是宣告了兩個 handleButtonClick
:
這兩個 handleButtonClick
是不同的兩個 function(指向到不同的記憶體位置),所以執行第一次和第二次的 handleButtonClick
是會變的
// 新增 todo
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
};
// 新增 todo
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
};
「指向到不同的記憶體位置」這個概念就是例如 {} === {}
會是 false
那要怎麼解決這個「每次 render 都會產生新的 function」的問題呢?
useCallback()
把一個 function 記起來useCallback 背後就是用 useRef 做的
用 useCallback
這個 hook 來解決「每次 render 都會產生新的 function」的問題
把 handleButtonClick
的 function 用 useCallback()
包起來
要傳入第二個參數,第二個參數會是一個 dependency array,就是當這個 array 裡面的東西改變時,handleButtonClick
才會改變
在 dependency array 傳空陣列代表:因為是空的,永遠不會有東西改變,所以 handleButtonClick
這個 function 也永遠都不會變
只有在第一次執行的時候會執行到 useCallback(...)
小括號裡面的程式碼,然後 useCallback
就會幫我把 handleButtonClick
這個 function 記起來,第二次執行時就直接用 useCallback
記起來的那個版本 -> 也就是說,不會再有新的 handleButtonClick
產生了(handleButtonClick
永遠都會是同一個 function)
useTodos.js:
要記得先引入 useCallback
import { useState, useEffect, useRef, useCallback } from "react";
// 新增 todo
const handleButtonClick = useCallback(() => {
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
}, []);
改成這樣之後,當我在 input 欄位輸入字母時,Button
就不會 re-render 了
es-lint 會跳出一個提醒:React Hook useCallback has missing dependencies...
因為在 handleButtonClick
裡面有用到 setTodos
這個 function
照理來說,當 setTodos
改變了,handleButtonClick
就要產生一個新的 function 才對
當我在 handleButtonClick
想要印出 console.log(value)
,為什麼印出的都是空值呢?
// 新增 todo
const handleButtonClick = useCallback(() => {
console.log(value);
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
}, []);
原因為:
我用 useCallback
騙 React 說「handleButtonClick
永遠都不會改變」
value
的初始值是空字串,所以就會印出空字串value
的值會變成 123(第二次 render 跟第一次 render 的 value
會是不同的兩個變數,在不同的 scope 裡面),但是因為我用 useCallback
把第一次的 handleButtonClick
記起來了,所以 value
永遠都會是第一次 render 的 scope 裡面的那個空字串,所以還是會印出空字串這樣是不正確的,因為這樣每次 render 都不會更新,永遠都會是第一次 render 的值
那怎麼辦呢?
就按照 es-lint 提示我的:
handleButtonClick
裡面有用到的 setTodos, setValue, value, todos
都放入 useCallback
的第二個參數這樣當 setTodos, setValue, value, todos
其中一個有改變時,就會產生一個新的 handleButtonClick
function,這樣在 handleButtonClick
裡面的 setTodos, setValue, value, todos
值才會是對的,這樣 render 出來的東西才會是對的
// 新增 todo
const handleButtonClick = useCallback(() => {
console.log(value);
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
}, [setTodos, setValue, value, todos]);
前面其實講錯了,在 input 輸入字母時,Button
會需要 re-render,因為 handleButtonClick
會用到 setTodos, setValue, value, todos
這些值,而在 input 輸入字母時,setTodos, setValue, value, todos
這些值都會改變,所以 Button
一定得 re-render
有時候,會需要傳一個物件進去
function Test({ style }) {
console.log("test render");
return <div style={style}>test</div>;
}
function App() {
// 用解構的語法,拿到「從各個 hooks 回傳的東西」
const {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
} = useTodos();
// UI
return (
<div className="App">
<Test style={{ color: "red" }} />
<input
type="text"
placeholder="add todo"
value={value}
onChange={handleChange}
/>
<MemoButton onclick={handleButtonClick}>Add todo</MemoButton>
當我在 input 欄位輸入字母時,會發現:Test
component 也會一直 re-render
原因為:
在 <Test style={{ color: "red" }} />
傳入的這個 object { color: "red" }
在每一次的 render,{ color: "red" }
這個 object 都會是「不同的 object」
就等於是這樣寫:
第一次 render 的 const s = { color: "red" }
和第二次 render 的 const s = { color: "red" }
會是不同的兩個 object
{ color: "red" } === { color: "red" }
會是 false,雖然內容一樣,但是是在不同的記憶體位置
function App() {
// 用解構的語法,拿到「從各個 hooks 回傳的東西」
const {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
} = useTodos();
const s = { color: "red" };
// UI
return (
<div className="App">
<Test style={s} />
那要怎麼讓 Test
component 不會一直 re-render 呢?
const s = { color: "red" }
移到 function App()
外面這樣每次 render 都是用到同一個 object,不會因為重新執行 function App()
而產生新的 object
function Test({ style }) {
console.log("test render");
return <div style={style}>test</div>;
}
const s = { color: "red" }; // 把 object 移到 function App 外面
function App() {
// 用解構的語法,拿到「從各個 hooks 回傳的東西」
const {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
} = useTodos();
// UI
return (
<div className="App">
<Test style={s} />
有時候,這個 s
會根據 value
的不同而改變,因此一定要寫在 function App()
裡面,例如:
當 value
有值時會是 red,沒有值的話會是 blue
function Test({ style }) {
console.log("test render");
return <div style={style}>test</div>;
}
function App() {
// 用解構的語法,拿到「從各個 hooks 回傳的東西」
const {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
} = useTodos();
const s = {
color: value ? "red" : "blue",
};
// UI
return (
<div className="App">
<Test style={s} />
useMemo
這個 hook 來處理「會需要複雜計算的資料」memo
是給 function component 用的,useMemo
是給資料用的在 useMemo()
裡面傳入一個 function,讓這個 function 回傳 s
的值
在 useMemo()
裡面傳入第二個參數,第二個參數會是一個 dependency array,在 array 裡面填入 value
意思就是:只有當 value
改變時,才會重新執行一次 useMemo()
裡面的程式碼,也就是:才會重新計算 s
的值是什麼,並且回傳計算後的值
當 s
的值會需要複雜計算時,用 useMemo
就可以節省很多效能,因為只有在 value
有改變時才會重新計算 s
的值
import { memo, useMemo } from "react"; // 從 React 引入 memo, useMemo
// Add todo 按鈕
function Button({ onclick, children }) {
console.log("render button");
return <button onClick={onclick}>{children}</button>;
}
const MemoButton = memo(Button); // 把 Button 這個 component 用 memo 包起來
function Test({ style }) {
console.log("test render");
return <div style={style}>test</div>;
}
const redStyle = {
color: "red",
};
const blueStyle = {
color: "blue",
};
function App() {
// 用解構的語法,拿到「從各個 hooks 回傳的東西」
const {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
} = useTodos();
const s = useMemo(() => {
console.log("calculate s");
return value ? redStyle : blueStyle;
}, [value]);
// UI
return (
<div className="App">
<Test style={s} />
因此,會發現:當我在 input 欄位輸入字母時,因為 value
改變了,所以會重新計算 s
的值(會看到有印出 "calculate s")。
但是如果我是把 todo 改為「已完成」,value
並沒有改變,就不會重新計算 s
的值(不會印出 “calculate s”)
以下面這個按鈕為例:
<MemoButton onclick={handleButtonClick}>Add todo</MemoButton>
在按鈕上面加上 onClick
事件,但其實這個 onClick
事件「並不是」放在 <button></button>
這個 DOM 上面喔!
在按鈕上面按右鍵「檢查」,在 Event Listeners 裡面可以看到:
雖然在 button
上面有這個 click 的事件,但是把它 remove 後,按鈕還是可以正常運作(功能還是一樣)
原因為:
這個按鈕以及頁面上的所有元素,它們的 event listener 都是用「事件代理」的方式被綁在最上層的 <div id="root"></div>
這個節點上面(寫在 index.html)
在 index.js,todo list 這整個 app 就是被放在 <div id="root"></div>
裡面:
ReactDOM.render(
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>,
document.getElementById("root")
);
會用事件代理的方式有兩個原因:
React 的 component 在 render 的時候,並不會直接產生出一個真的 DOM,而是 render 出一個 virtual DOM(會是一個 JS 的 object)
例如下面的 component:
<div className="App">
<input
type="text"
placeholder="add todo"
value={value}
onChange={handleChange}
/>
<button onClick={handleButtonClick}>Add todo</button>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
handleDeleteTodo={handleDeleteTodo}
handleToggleIsFinished={handleToggleIsFinished}
/>
))}
</div>
render 時產生的 virtual DOM 會是這樣:(就是一個 JS 的 object)
Reace 會把 tag: 'div'
這行 render 成 <div></div>
{
tag: 'div',
props: {
className: 'App'
}
children: {
}
}
找出「前一個 virtual DOM」和「後一個 virtual DOM」之間的差異後,再把這些「需要改變的地方」應用到真的 DOM 上面
reconciliation 就是:調和、比對差異的過程
因為有了這層 virtual DOM,讓 React 可以透過「比對 virtual DOM 的差異(Reconciliation)」,快速找出需要改變的地方,就不用每次改變 state 時就要全部清空重新 render(很沒效率),只需要 re-render 有改變的地方即可
前面有提到 virtual DOM 是一個 JS 的 object(如下程式碼),React 會把這個 object render 成 HTML DOM
{
tag: 'div',
props: {
className: 'App'
}
children: {
}
}
其實這個 virtual DOM 不只可以 render 成 HTML DOM(做成網站),也就是說:不一定要呈現在瀏覽器上面
tag: 'div'
render 成 ## div
Virtual DOM | 為了瞭解原理,那就來實作一個簡易 Virtual DOM 吧!
從頭打造一個簡單的 Virtual DOM
第一個層面是:
當 component 的 state 改變時,就會再 call 一次 function component
「再 call 一次 function component」這個行為就稱為 re-render
第二個層面是:
找出 DOM diff 之後,把有差異的地方應用到真正的 DOM 上面 -> 這個行為也稱為 re-render
所以,有可能發生的事情是:
state 改變了 -> 有 re-render(再 call 一次 function component)
這個改變的 state 並不會影響到 UI -> 在 virtual DOM 會做一次 DOM diff,但是因為 virtual DOM 並沒有變,所以最後並不會有東西被應用到真的 DOM 上面去
雖然,上面這樣做的效能已經比「每次 state 改變都把畫面清空,再真的把東西放到 DOM 上面去」還要好
但是其實,因為畫面根本不會改變,所以根本就不需要 re-render
那要怎麼解決這個 re-render 的問題呢?
首先,來示範一下這個 re-render 的問題
先把 Add todo 按鈕變成一個 component
每一次 render Button
時,都會在 console 印出 "render button"
// Add todo 按鈕
function Button({ onclick, children }) {
console.log("render button");
return <button onClick={onclick}>{children}</button>;
}
function App() {
// 用解構的語法,拿到「從各個 hooks 回傳的東西」
const {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
} = useTodos();
// UI
return (
<div className="App">
<input
type="text"
placeholder="add todo"
value={value}
onChange={handleChange}
/>
<Button onclick={handleButtonClick}>Add todo</Button>
...
這個 re-render 的問題就是:
我只要在 input 欄位打一個字母,Button
就會 re-render 一次
可是,我打這些字母跟 Button
沒有任何關係,Button
不應該 re-render 啊
那要怎麼讓 Button
不要 re-render 呢?
memo()
把 component 包起來React 有提供一個 hook 叫做 memo
,把 Button
這個 component 用 memo()
包起來
memo()
把 component 包起來之後,React 就會自動檢測:如果傳給這個 component 的 props 都沒有變的話,就不會 re-render在此範例中,傳給 Button
的 props 就是 { onclick, children }
,只有當 onclick
或 children
有變的話,才會 re-render
...
import { memo } from "react"; // 從 React 引入 memo
// Add todo 按鈕
function Button({ onclick, children }) {
console.log("render button");
return <button onClick={onclick}>{children}</button>;
}
const MemoButton = memo(Button); // 把 Button 這個 component 用 memo 包起來
function App() {
// 用解構的語法,拿到「從各個 hooks 回傳的東西」
const {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
} = useTodos();
// UI
return (
<div className="App">
<input
type="text"
placeholder="add todo"
value={value}
onChange={handleChange}
/>
<MemoButton onclick={handleButtonClick}>Add todo</MemoButton>
...
改成 MemoButton
之後,會發現:當我在 input 欄位輸入字母,還是會一直 re-render 啊?原因是:
Button
的 props 有 onclick
和 children
children
(也就是 Add todo)沒有變,但是 onclick
會變
因為 onclick={handleButtonClick}
裡面的 handleButtonClick
會變,為什麼呢?
因為:
每當我在 input 欄位輸入一個字母,function App()
都會重新執行一次 -> 在 function App()
裡面的 useTodos()
也會重新執行一次 -> 在 useTodos()
這個 hook 裡面,handleButtonClick
是用 const
宣告的:
// 新增 todo
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
};
也就是說,每執行一次 function useTodos()
,就會重新宣告一次 handleButtonClick
(重新產生一個新的 function)
例如我執行了兩次 useTodos()
useTodos();
useTodos();
就等於是宣告了兩個 handleButtonClick
:
這兩個 handleButtonClick
是不同的兩個 function(指向到不同的記憶體位置),所以執行第一次和第二次的 handleButtonClick
是會變的
// 新增 todo
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
};
// 新增 todo
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
};
「指向到不同的記憶體位置」這個概念就是例如 {} === {}
會是 false
那要怎麼解決這個「每次 render 都會產生新的 function」的問題呢?
useCallback()
把一個 function 記起來useCallback 背後就是用 useRef 做的
用 useCallback
這個 hook 來解決「每次 render 都會產生新的 function」的問題
把 handleButtonClick
的 function 用 useCallback()
包起來
要傳入第二個參數,第二個參數會是一個 dependency array,就是當這個 array 裡面的東西改變時,handleButtonClick
才會改變
在 dependency array 傳空陣列代表:因為是空的,永遠不會有東西改變,所以 handleButtonClick
這個 function 也永遠都不會變
只有在第一次執行的時候會執行到 useCallback(...)
小括號裡面的程式碼,然後 useCallback
就會幫我把 handleButtonClick
這個 function 記起來,第二次執行時就直接用 useCallback
記起來的那個版本 -> 也就是說,不會再有新的 handleButtonClick
產生了(handleButtonClick
永遠都會是同一個 function)
useTodos.js:
要記得先引入 useCallback
import { useState, useEffect, useRef, useCallback } from "react";
// 新增 todo
const handleButtonClick = useCallback(() => {
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
}, []);
改成這樣之後,當我在 input 欄位輸入字母時,Button
就不會 re-render 了
es-lint 會跳出一個提醒:React Hook useCallback has missing dependencies...
因為在 handleButtonClick
裡面有用到 setTodos
這個 function
照理來說,當 setTodos
改變了,handleButtonClick
就要產生一個新的 function 才對
當我在 handleButtonClick
想要印出 console.log(value)
,為什麼印出的都是空值呢?
// 新增 todo
const handleButtonClick = useCallback(() => {
console.log(value);
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
}, []);
原因為:
我用 useCallback
騙 React 說「handleButtonClick
永遠都不會改變」
value
的初始值是空字串,所以就會印出空字串value
的值會變成 123(第二次 render 跟第一次 render 的 value
會是不同的兩個變數,在不同的 scope 裡面),但是因為我用 useCallback
把第一次的 handleButtonClick
記起來了,所以 value
永遠都會是第一次 render 的 scope 裡面的那個空字串,所以還是會印出空字串這樣是不正確的,因為這樣每次 render 都不會更新,永遠都會是第一次 render 的值
那怎麼辦呢?
就按照 es-lint 提示我的:
handleButtonClick
裡面有用到的 setTodos, setValue, value, todos
都放入 useCallback
的第二個參數這樣當 setTodos, setValue, value, todos
其中一個有改變時,就會產生一個新的 handleButtonClick
function,這樣在 handleButtonClick
裡面的 setTodos, setValue, value, todos
值才會是對的,這樣 render 出來的東西才會是對的
// 新增 todo
const handleButtonClick = useCallback(() => {
console.log(value);
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
}, [setTodos, setValue, value, todos]);
前面其實講錯了,在 input 輸入字母時,Button
會需要 re-render,因為 handleButtonClick
會用到 setTodos, setValue, value, todos
這些值,而在 input 輸入字母時,setTodos, setValue, value, todos
這些值都會改變,所以 Button
一定得 re-render
有時候,會需要傳一個物件進去
function Test({ style }) {
console.log("test render");
return <div style={style}>test</div>;
}
function App() {
// 用解構的語法,拿到「從各個 hooks 回傳的東西」
const {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
} = useTodos();
// UI
return (
<div className="App">
<Test style={{ color: "red" }} />
<input
type="text"
placeholder="add todo"
value={value}
onChange={handleChange}
/>
<MemoButton onclick={handleButtonClick}>Add todo</MemoButton>
當我在 input 欄位輸入字母時,會發現:Test
component 也會一直 re-render
原因為:
在 <Test style={{ color: "red" }} />
傳入的這個 object { color: "red" }
在每一次的 render,{ color: "red" }
這個 object 都會是「不同的 object」
就等於是這樣寫:
第一次 render 的 const s = { color: "red" }
和第二次 render 的 const s = { color: "red" }
會是不同的兩個 object
{ color: "red" } === { color: "red" }
會是 false,雖然內容一樣,但是是在不同的記憶體位置
function App() {
// 用解構的語法,拿到「從各個 hooks 回傳的東西」
const {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
} = useTodos();
const s = { color: "red" };
// UI
return (
<div className="App">
<Test style={s} />
那要怎麼讓 Test
component 不會一直 re-render 呢?
const s = { color: "red" }
移到 function App()
外面這樣每次 render 都是用到同一個 object,不會因為重新執行 function App()
而產生新的 object
function Test({ style }) {
console.log("test render");
return <div style={style}>test</div>;
}
const s = { color: "red" }; // 把 object 移到 function App 外面
function App() {
// 用解構的語法,拿到「從各個 hooks 回傳的東西」
const {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
} = useTodos();
// UI
return (
<div className="App">
<Test style={s} />
有時候,這個 s
會根據 value
的不同而改變,因此一定要寫在 function App()
裡面,例如:
當 value
有值時會是 red,沒有值的話會是 blue
function Test({ style }) {
console.log("test render");
return <div style={style}>test</div>;
}
function App() {
// 用解構的語法,拿到「從各個 hooks 回傳的東西」
const {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
} = useTodos();
const s = {
color: value ? "red" : "blue",
};
// UI
return (
<div className="App">
<Test style={s} />
useMemo
這個 hook 來處理「會需要複雜計算的資料」memo
是給 function component 用的,useMemo
是給資料用的在 useMemo()
裡面傳入一個 function,讓這個 function 回傳 s
的值
在 useMemo()
裡面傳入第二個參數,第二個參數會是一個 dependency array,在 array 裡面填入 value
意思就是:只有當 value
改變時,才會重新執行一次 useMemo()
裡面的程式碼,也就是:才會重新計算 s
的值是什麼,並且回傳計算後的值
當 s
的值會需要複雜計算時,用 useMemo
就可以節省很多效能,因為只有在 value
有改變時才會重新計算 s
的值
import { memo, useMemo } from "react"; // 從 React 引入 memo, useMemo
// Add todo 按鈕
function Button({ onclick, children }) {
console.log("render button");
return <button onClick={onclick}>{children}</button>;
}
const MemoButton = memo(Button); // 把 Button 這個 component 用 memo 包起來
function Test({ style }) {
console.log("test render");
return <div style={style}>test</div>;
}
const redStyle = {
color: "red",
};
const blueStyle = {
color: "blue",
};
function App() {
// 用解構的語法,拿到「從各個 hooks 回傳的東西」
const {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
} = useTodos();
const s = useMemo(() => {
console.log("calculate s");
return value ? redStyle : blueStyle;
}, [value]);
// UI
return (
<div className="App">
<Test style={s} />
因此,會發現:當我在 input 欄位輸入字母時,因為 value
改變了,所以會重新計算 s
的值(會看到有印出 "calculate s")。
但是如果我是把 todo 改為「已完成」,value
並沒有改變,就不會重新計算 s
的值(不會印出 “calculate s”)
以下面這個按鈕為例:
<MemoButton onclick={handleButtonClick}>Add todo</MemoButton>
在按鈕上面加上 onClick
事件,但其實這個 onClick
事件「並不是」放在 <button></button>
這個 DOM 上面喔!
在按鈕上面按右鍵「檢查」,在 Event Listeners 裡面可以看到:
雖然在 button
上面有這個 click 的事件,但是把它 remove 後,按鈕還是可以正常運作(功能還是一樣)
原因為:
這個按鈕以及頁面上的所有元素,它們的 event listener 都是用「事件代理」的方式被綁在最上層的 <div id="root"></div>
這個節點上面(寫在 index.html)
在 index.js,todo list 這整個 app 就是被放在 <div id="root"></div>
裡面:
ReactDOM.render(
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>,
document.getElementById("root")
);
會用事件代理的方式有兩個原因:
除了 useState 之外,另一個很重要的 hook 就是 useEffect
假設:
todo list 的初始值是從 api 來的,我需要在第一次 render 時 call api
錯誤寫法
把 call api 的程式碼寫在 function component 裡面,錯誤的原因是:我每次重新 render 都會重新發一次 request,但我只想要在「第一次 render 之後」call api 就好
function App() {
fetch(...).then()...
const [value, setValue] = useState("");
因此,React 提供了 userEffect 這個內建的 hook
要先在上方引入 useEffect
useEffect 裡面要傳入的是一個 function
import { useState, useRef, useEffect } from "react"; // 用解構的語法
function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "abc", isFinished: true },
{ id: 2, content: "not done", isFinished: false },
]);
const id = useRef(3);
useEffect(() => {
console.log("after render!");
});
...
例如:
我想要把 todos
存到 localStorage 裡面
在 App
函式的外面,宣告一個 writeTodosToLocalStorage
function:
function writeTodosToLocalStorage(todos) {
window.localStorage.setItem("todos", JSON.stringify(todos));
}
function App() {
然後,在每次更新 state 時(新增 todo、刪除 todo、標示已完成/未完成),我都要呼叫一次 writeTodosToLocalStorage
function 來把最新的 todos
存到 localStorage 裡面
要注意的是,下面不可以直接寫 writeTodosToLocalStorage(todos)
,原因為:
setTodos()
是「非同步」,所以 todos
陣列不會即時更新。如果直接寫 writeTodosToLocalStorage(todos)
就會把舊的 todos
存進 localStorage 了
例如在新增 todo 時
// 新增 todo
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
writeTodosToLocalStorage([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
};
useEffect
的作法如下首先,我在 useEffect
裡面寫好「呼叫 writeTodosToLocalStorage()
」的程式碼
因為 input
的 value
有放在 state 裡面,所以每當我在欄位輸入字母時,value
的 state 都會改變,就會重新 render 一次,那麼 useEffect
就會執行一次
但是,如果我只有在欄位輸入字母,我並不希望執行 useEffect
(因為 todos
並沒有改變),我只希望在「todos
的 state 有改變時」(也就是:對 todo 做新增、刪除、標示已完成/未完成),才執行 useEffect
React 在 useEffect
提供了第二個參數(是一個陣列),陣列裡面可以放「我關注的資料」
加上第二個參數 todos
就代表:
第一次 render 完後(Mount 之後),會執行 useEffect
之後,就只有在 todos
有改變時,才會重新執行 useEffect
function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "abc", isFinished: true },
{ id: 2, content: "not done", isFinished: false },
]);
const id = useRef(3);
useEffect(() => {
writeTodosToLocalStorage(todos);
console.log(JSON.stringify(todos));
}, [todos]);
...
todos
放到「function App()
裡面 todos
的 state 裡面去」作法如下:
我在 useEffect
的第二個參數放一個「空陣列 []
」,代表說:只有「在陣列裡面的東西」改變時,才會重新執行 useEffect
。但因為陣列裡面是空的,所以永遠都不會有東西改變 -> 這就代表:只有在「頁面第一次 render 時」會執行 useEffect
,之後就再也不會執行了
function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "abc", isFinished: true },
{ id: 2, content: "not done", isFinished: false },
]);
const id = useRef(3);
// 把 localStorage 裡面存的 todos 放到「function App() 裡面 todos 的 state 裡面去」
useEffect(() => {
const todosData = window.localStorage.getItem("todos") || "";
if (todosData) {
setTodos(JSON.parse(todosData));
}
}, []);
// 把 todos 存到 localStorage 裡面去
useEffect(() => {
writeTodosToLocalStorage(todos);
}, [todos]);
原因為:
畫面第一次 render 時,會 render 出的是 todos
的初始值,也就是下面那兩個 todo:
const [todos, setTodos] = useState([
{ id: 1, content: "abc", isFinished: true },
{ id: 2, content: "not done", isFinished: false },
]);
在「browser 畫出畫面之後(使用者也已經看到畫面了)」才會執行 useEffect
,才會把 localStorage 裡面存的 todos
放到「function App()
裡面 todos
的 state 裡面去」,然後才會第二次 render。所以我們最後看到的是第二次 render 後的畫面
第一次 render 和第二次 render 之間,就會感覺閃了一下
解決方法請往下看
要解決「畫面閃一下」的問題,有兩個解法:
todos
陣列的初始值直接設為「從 localStorage 拿出來的 todos」useLayoutEffect 用法如下:
setTodos(JSON.parse(todosData))
那段改為使用 useLayoutEffectimport { useState, useRef, useEffect, useLayoutEffect } from "react"; // 用解構的語法
function writeTodosToLocalStorage(todos) {
window.localStorage.setItem("todos", JSON.stringify(todos));
}
function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "abc", isFinished: true },
{ id: 2, content: "not done", isFinished: false },
]);
const id = useRef(3);
// 把 localStorage 裡面存的 todos 放到「function App() 裡面 todos 的 state 裡面去」
useLayoutEffect(() => {
const todosData = window.localStorage.getItem("todos") || "";
if (todosData) {
setTodos(JSON.parse(todosData));
}
}, []);
// 把 todos 存到 localStorage 裡面去
useEffect(() => {
writeTodosToLocalStorage(todos);
}, [todos]);
這樣就不會閃一下的原因為:
這個 Hook 的流程圖 Flow Diagram 很重要,要記起來!
流程如下:
只要每次更新 state,就會重新跑一次 Update 的流程(重新 render 一次)
Unmount 就是:把所有 Effects 都清掉
在 browser 畫畫面之前,我就先用 useLayoutEffect
提早改變了 state(並且 updates DOM)
因此,使用者就不會看到「第一次 render 初始值」的畫面了
todos
陣列的初始值直接設為「從 localStorage 拿出來的 todos」function App() {
const [value, setValue] = useState("");
const todosData = window.localStorage.getItem("todos") || "";
const [todos, setTodos] = useState(JSON.parse(todosData) || []); // 把 todos 的初始值直接設為「從 localStorage 拿出來的 todos」
const id = useRef(3);
// 把 todos 存到 localStorage 裡面去
useEffect(() => {
writeTodosToLocalStorage(todos);
}, [todos]);
const [todos, setTodos] = useState(JSON.parse(todosData) || []);
因為這裡的 useState()
小括號裡面傳的是初始值,所以就只有在第一次 render(頁面重新整理) 時才會吃到這個值
每次重新 render 時,const todosData = window.localStorage.getItem("todos") || "";
這行還是會做事情(還是會把最新的 todos 從 localStorage 拿出來)
然後,const [todos, setTodos] = useState(JSON.parse(todosData) || []);
這行也還是會做事情(把剛拿到的 todosData
放到 useState()
裡面)
但是,只有「第一次」放到 useState()
裡面的值會被撈出來當作 todos
陣列的初始值,從第二次開始每次放到 useState()
裡面的值 React 都會自動忽略,因此,這樣就造成了效能的浪費,因為根本就不需要每次 render 都把最新的 JSON.parse(todosData)
放到 useState()
裡面
Mount 的 Run lazy initializers 只有在第一次 render 時才會執行
useState()
裡面可以放一個 function
這個 function 就是 lazy initializers
這個 function 只有在第一次 render (頁面重新整理)時會執行
這個 function return 的東西就會是「初始值」
把整個 window.localStorage.getItem
這些複雜的運算都搬到 lazy initializers 裡面,這樣就只有在頁面第一次 render 時會做運算,之後就不會再重新運算,就不會造成效能的浪費了
function App() {
const id = useRef(1);
const [value, setValue] = useState("");
const [todos, setTodos] = useState(() => {
console.log("init");
let todosData = window.localStorage.getItem("todos") || "";
if (todosData) {
todosData = JSON.parse(todosData); // 把 todos 的初始值直接設為「從 localStorage 拿出來的 todos」
id.current = todosData[0].id + 1; // 把 id 設為「localStorage 裡面最大的 id 再加 1」
} else {
todosData = [];
}
return todosData;
});
// 把 todos 存到 localStorage 裡面去
useEffect(() => {
writeTodosToLocalStorage(todos);
}, [todos]);
推薦閱讀文章 A Complete Guide to useEffect 和 How Are Function Components Different from Classes?
第一次 render 的 todos
就永遠都不會改變了
setTodos()
只是在「調整下一次 render 的 todos
要長什麼樣子」,並不會去改變上一次 render 的 todos
以官網為範例:
使用者一進入網站時,就連到他的 userId 去拿資料 > 拿到這個 userId 的資料後就可以進行處理
當今天換了一個 userId 進入網站時,我要先用 cleanup function 斷掉上一個的 connection,再重新連到下一個 userId 的資料
useEffect(() => {
WebSocket.CONNECTING(userId).subscribe(() => {
// ...
});
// cleanup function
return () => {
WebSocket.disconnect(userId);
};
}, [userId]);
所以,當我想要在 component 不見時做什麼事情,就可以寫在 cleanup function
下面的程式碼中,把 useEffect 的第二個參數放上一個「空陣列」,
如此一來,這裡的 cleanup function 就只有在 Unmount 時會執行
原因為:
這個 useEffects 只有在第一次 render 時會執行,因此不會發生第一個時機點的情況(不會第二次執行 useEffects),所以就只有在 Unmount 時會執行
useEffect(() => {
// component 要 mount 時想要做什麼事情,就寫在這裡
console.log("Mount");
return () => {
// cleanup function(component 要 unmount 時想要做什麼事情,就寫在這裡)
console.log("Unmount");
};
}, []);
意思就是:
在 component 底下,不能把 hooks 包在 if...else
之類的條件判斷裡面,例如
// 把 todos 存到 localStorage 裡面去
if (true) {
useEffect(() => {
writeTodosToLocalStorage(todos);
console.log("useEffect: todos", todos);
// cleanup function
return () => {
console.log("clearEffect: todos", todos);
};
}, [todos]);
}
上面這樣寫的話,React 就會報錯:
React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render
舉另一個例子,我想要做的功能是:如果沒有 todos
我就不要用這個 hook
錯誤寫法:用 if...else
包住 hook
if (!todos) {
useEffect(() => {
writeTodosToLocalStorage(todos);
console.log("useEffect: todos", todos);
// cleanup function
return () => {
console.log("clearEffect: todos", todos);
};
}, [todos]);
}
正確寫法:在 hook 裡面做判斷
在最前面加上 if (!todos) return
// 把 todos 存到 localStorage 裡面去
useEffect(() => {
if (!todos) return; // 如果沒有 todos 的話,就直接 return
writeTodosToLocalStorage(todos);
console.log("useEffect: todos", todos);
// cleanup function
return () => {
console.log("clearEffect: todos", todos);
};
}, [todos]);
自己定義的 hook 就叫做 custom hook
custom hook 一定要用 use 開頭
onChange
時會 setValue()
這整件事情」包成一個 useInput
hook作法如下:
新增一個檔案叫做 useInput.js
useInput
function 並且 export 出去,這個 useInput
function 就是我自定義的 hookvalue
, setValue
, handleChange
useInput.js:
import { useState } from "react";
export default function useInput() {
const [value, setValue] = useState("");
const handleChange = (e) => {
setValue(e.target.value);
};
return {
value,
setValue,
handleChange,
};
}
然後,在 App.js 就可以這樣使用這個 useInput
的 hook:
先把 useInput
import 進來
import useInput from "./useInput";
然後,在 function App()
裡面,就可以用解構的語法,拿到 useInput()
回傳的 value
, setValue
, handleChange
hook 其實就是一個 function,所以呼叫 useInput()
來執行這個 hook
const { value, setValue, handleChange } = useInput();
在 input
的 onChange
就可以直接寫 handleChange
了
<input
type="text"
placeholder="add todo"
value={value}
onChange={handleChange}
/>
App.js:
import TodoItem from "./TodoItem";
import { useState, useRef, useEffect, useLayoutEffect } from "react"; // 用解構的語法
import useInput from "./useInput";
function writeTodosToLocalStorage(todos) {
window.localStorage.setItem("todos", JSON.stringify(todos));
}
function App() {
const id = useRef(1);
const [todos, setTodos] = useState(() => {
console.log("init");
let todosData = window.localStorage.getItem("todos") || "";
if (todosData) {
todosData = JSON.parse(todosData); // 把 todos 的初始值直接設為「從 localStorage 拿出來的 todos」
id.current = todosData[0].id + 1; // 把 id 設為「localStorage 裡面最大的 id 再加 1」
} else {
todosData = [];
}
return todosData;
});
const { value, setValue, handleChange } = useInput(); // 用解構的語法,拿到 useInput() 回傳的 value, setValue, handleChange
// 把 todos 存到 localStorage 裡面去
useEffect(() => {
if (!todos) return; // 如果沒有 todos 的話,就直接 return
writeTodosToLocalStorage(todos);
console.log("useEffect: todos", todos);
// cleanup function
return () => {
console.log("clearEffect: todos", todos);
};
}, [todos]);
// 新增 todo
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
};
// 刪除 todo
const handleDeleteTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
// 切換已完成/未完成
const handleToggleIsFinished = (id) => {
setTodos(
todos.map((todo) => {
if (todo.id !== id) return todo;
return {
...todo,
isFinished: !todo.isFinished,
};
})
);
};
return (
<div className="App">
<input
type="text"
placeholder="add todo"
value={value}
onChange={handleChange}
/>
<button onClick={handleButtonClick}>Add todo</button>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
handleDeleteTodo={handleDeleteTodo}
handleToggleIsFinished={handleToggleIsFinished}
/>
))}
</div>
);
}
export default App;
改寫之後,我就不需要寫下面這兩段:
const [value, setValue] = useState("");
const handleInputChange = (e) => {
setValue(e.target.value);
};
這樣改寫完後,todo list 一樣可以照常跑
例如:
假設我有第二個 input
,我就可以共用 useInput.js 裡面的程式碼
只要將傳進來的三個東西,分別取一個新的名字即可(用 ES6 的語法)
value
改名叫 todoName
setValue
改名叫 setTodoName
handleChange
改名叫 handleTodoNameChange
const { value, setValue, handleChange } = useInput(); // 用解構的語法,拿到 useInput() 回傳的 value, setValue, handleChange
const {
// 將傳進來的三個東西,分別取一個新的名稱
value: todoName,
setValue: setTodoName,
handleChange: handleTodoNameChange,
} = useInput();
useTodos
的 hook把所有跟 todos
相關的邏輯都抽出來,放到另一個 useTodos.js 檔案
把 useInput
這個 hook 也整合到 useTodos
裡面
useInput.js:
import { useState } from "react";
export default function useInput() {
const [value, setValue] = useState("");
const handleChange = (e) => {
setValue(e.target.value);
};
return {
value,
setValue,
handleChange,
};
}
useTodos.js:
import { useState, useEffect, useRef } from "react";
import useInput from "./useInput";
function writeTodosToLocalStorage(todos) {
window.localStorage.setItem("todos", JSON.stringify(todos));
}
export default function useTodos() {
const id = useRef(1);
const { value, setValue, handleChange } = useInput();
const [todos, setTodos] = useState(() => {
console.log("init");
let todosData = window.localStorage.getItem("todos") || "";
if (todosData) {
todosData = JSON.parse(todosData); // 把 todos 的初始值直接設為「從 localStorage 拿出來的 todos」
id.current = todosData[0].id + 1; // 把 id 設為「localStorage 裡面最大的 id 再加 1」
} else {
todosData = [];
}
return todosData;
});
// 把 todos 存到 localStorage 裡面去
useEffect(() => {
if (!todos) return; // 如果沒有 todos 的話,就直接 return
writeTodosToLocalStorage(todos);
}, [todos]);
// 新增 todo
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
};
// 刪除 todo
const handleDeleteTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
// 切換已完成/未完成
const handleToggleIsFinished = (id) => {
setTodos(
todos.map((todo) => {
if (todo.id !== id) return todo;
return {
...todo,
isFinished: !todo.isFinished,
};
})
);
};
return {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
};
}
App.js:
import TodoItem from "./TodoItem";
import useTodos from "./useTodos";
function App() {
// 用解構的語法,拿到「從各個 hooks 回傳的東西」
const {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
} = useTodos();
// UI
return (
<div className="App">
<input
type="text"
placeholder="add todo"
value={value}
onChange={handleChange}
/>
<button onClick={handleButtonClick}>Add todo</button>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
handleDeleteTodo={handleDeleteTodo}
handleToggleIsFinished={handleToggleIsFinished}
/>
))}
</div>
);
}
export default App;
邏輯:會在各個 hooks
UI:會在 App.js
因為「把邏輯都抽出來寫到 hooks 裡面去了」,在 App.js 裡面就只會剩下:「從各個 hooks 拿東西」和「UI」而已
hooks 有分為幾種:
基本上,要做什麼事情都會用 useEffect 來寫,並不會把要做的事情直接寫在 function component 的第一層,如下:
function App() {
localStorage.setItem(...) // 把要做的事情直接寫在 function component 的第一層
原因有兩個:
這邊假設我要做的事情是「把 todos
儲存到 localStorage 裡面去」
todos
的 state 有改變時再儲存就好。因此,要把「儲存到 localStorage」寫在 useEffect 裡面,這樣我就可以在 useEffect 的第二個參數傳入 todos
去設定「在 todos
(指定的 state)改變時,才會去執行這個 useEffect」]]>
除了 useState 之外,另一個很重要的 hook 就是 useEffect
假設:
todo list 的初始值是從 api 來的,我需要在第一次 render 時 call api
錯誤寫法
把 call api 的程式碼寫在 function component 裡面,錯誤的原因是:我每次重新 render 都會重新發一次 request,但我只想要在「第一次 render 之後」call api 就好
function App() {
fetch(...).then()...
const [value, setValue] = useState("");
因此,React 提供了 userEffect 這個內建的 hook
要先在上方引入 useEffect
useEffect 裡面要傳入的是一個 function
import { useState, useRef, useEffect } from "react"; // 用解構的語法
function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "abc", isFinished: true },
{ id: 2, content: "not done", isFinished: false },
]);
const id = useRef(3);
useEffect(() => {
console.log("after render!");
});
...
例如:
我想要把 todos
存到 localStorage 裡面
在 App
函式的外面,宣告一個 writeTodosToLocalStorage
function:
function writeTodosToLocalStorage(todos) {
window.localStorage.setItem("todos", JSON.stringify(todos));
}
function App() {
然後,在每次更新 state 時(新增 todo、刪除 todo、標示已完成/未完成),我都要呼叫一次 writeTodosToLocalStorage
function 來把最新的 todos
存到 localStorage 裡面
要注意的是,下面不可以直接寫 writeTodosToLocalStorage(todos)
,原因為:
setTodos()
是「非同步」,所以 todos
陣列不會即時更新。如果直接寫 writeTodosToLocalStorage(todos)
就會把舊的 todos
存進 localStorage 了
例如在新增 todo 時
// 新增 todo
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
writeTodosToLocalStorage([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
};
useEffect
的作法如下首先,我在 useEffect
裡面寫好「呼叫 writeTodosToLocalStorage()
」的程式碼
因為 input
的 value
有放在 state 裡面,所以每當我在欄位輸入字母時,value
的 state 都會改變,就會重新 render 一次,那麼 useEffect
就會執行一次
但是,如果我只有在欄位輸入字母,我並不希望執行 useEffect
(因為 todos
並沒有改變),我只希望在「todos
的 state 有改變時」(也就是:對 todo 做新增、刪除、標示已完成/未完成),才執行 useEffect
React 在 useEffect
提供了第二個參數(是一個陣列),陣列裡面可以放「我關注的資料」
加上第二個參數 todos
就代表:
第一次 render 完後(Mount 之後),會執行 useEffect
之後,就只有在 todos
有改變時,才會重新執行 useEffect
function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "abc", isFinished: true },
{ id: 2, content: "not done", isFinished: false },
]);
const id = useRef(3);
useEffect(() => {
writeTodosToLocalStorage(todos);
console.log(JSON.stringify(todos));
}, [todos]);
...
todos
放到「function App()
裡面 todos
的 state 裡面去」作法如下:
我在 useEffect
的第二個參數放一個「空陣列 []
」,代表說:只有「在陣列裡面的東西」改變時,才會重新執行 useEffect
。但因為陣列裡面是空的,所以永遠都不會有東西改變 -> 這就代表:只有在「頁面第一次 render 時」會執行 useEffect
,之後就再也不會執行了
function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "abc", isFinished: true },
{ id: 2, content: "not done", isFinished: false },
]);
const id = useRef(3);
// 把 localStorage 裡面存的 todos 放到「function App() 裡面 todos 的 state 裡面去」
useEffect(() => {
const todosData = window.localStorage.getItem("todos") || "";
if (todosData) {
setTodos(JSON.parse(todosData));
}
}, []);
// 把 todos 存到 localStorage 裡面去
useEffect(() => {
writeTodosToLocalStorage(todos);
}, [todos]);
原因為:
畫面第一次 render 時,會 render 出的是 todos
的初始值,也就是下面那兩個 todo:
const [todos, setTodos] = useState([
{ id: 1, content: "abc", isFinished: true },
{ id: 2, content: "not done", isFinished: false },
]);
在「browser 畫出畫面之後(使用者也已經看到畫面了)」才會執行 useEffect
,才會把 localStorage 裡面存的 todos
放到「function App()
裡面 todos
的 state 裡面去」,然後才會第二次 render。所以我們最後看到的是第二次 render 後的畫面
第一次 render 和第二次 render 之間,就會感覺閃了一下
解決方法請往下看
要解決「畫面閃一下」的問題,有兩個解法:
todos
陣列的初始值直接設為「從 localStorage 拿出來的 todos」useLayoutEffect 用法如下:
setTodos(JSON.parse(todosData))
那段改為使用 useLayoutEffectimport { useState, useRef, useEffect, useLayoutEffect } from "react"; // 用解構的語法
function writeTodosToLocalStorage(todos) {
window.localStorage.setItem("todos", JSON.stringify(todos));
}
function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "abc", isFinished: true },
{ id: 2, content: "not done", isFinished: false },
]);
const id = useRef(3);
// 把 localStorage 裡面存的 todos 放到「function App() 裡面 todos 的 state 裡面去」
useLayoutEffect(() => {
const todosData = window.localStorage.getItem("todos") || "";
if (todosData) {
setTodos(JSON.parse(todosData));
}
}, []);
// 把 todos 存到 localStorage 裡面去
useEffect(() => {
writeTodosToLocalStorage(todos);
}, [todos]);
這樣就不會閃一下的原因為:
這個 Hook 的流程圖 Flow Diagram 很重要,要記起來!
流程如下:
只要每次更新 state,就會重新跑一次 Update 的流程(重新 render 一次)
Unmount 就是:把所有 Effects 都清掉
在 browser 畫畫面之前,我就先用 useLayoutEffect
提早改變了 state(並且 updates DOM)
因此,使用者就不會看到「第一次 render 初始值」的畫面了
todos
陣列的初始值直接設為「從 localStorage 拿出來的 todos」function App() {
const [value, setValue] = useState("");
const todosData = window.localStorage.getItem("todos") || "";
const [todos, setTodos] = useState(JSON.parse(todosData) || []); // 把 todos 的初始值直接設為「從 localStorage 拿出來的 todos」
const id = useRef(3);
// 把 todos 存到 localStorage 裡面去
useEffect(() => {
writeTodosToLocalStorage(todos);
}, [todos]);
const [todos, setTodos] = useState(JSON.parse(todosData) || []);
因為這裡的 useState()
小括號裡面傳的是初始值,所以就只有在第一次 render(頁面重新整理) 時才會吃到這個值
每次重新 render 時,const todosData = window.localStorage.getItem("todos") || "";
這行還是會做事情(還是會把最新的 todos 從 localStorage 拿出來)
然後,const [todos, setTodos] = useState(JSON.parse(todosData) || []);
這行也還是會做事情(把剛拿到的 todosData
放到 useState()
裡面)
但是,只有「第一次」放到 useState()
裡面的值會被撈出來當作 todos
陣列的初始值,從第二次開始每次放到 useState()
裡面的值 React 都會自動忽略,因此,這樣就造成了效能的浪費,因為根本就不需要每次 render 都把最新的 JSON.parse(todosData)
放到 useState()
裡面
Mount 的 Run lazy initializers 只有在第一次 render 時才會執行
useState()
裡面可以放一個 function
這個 function 就是 lazy initializers
這個 function 只有在第一次 render (頁面重新整理)時會執行
這個 function return 的東西就會是「初始值」
把整個 window.localStorage.getItem
這些複雜的運算都搬到 lazy initializers 裡面,這樣就只有在頁面第一次 render 時會做運算,之後就不會再重新運算,就不會造成效能的浪費了
function App() {
const id = useRef(1);
const [value, setValue] = useState("");
const [todos, setTodos] = useState(() => {
console.log("init");
let todosData = window.localStorage.getItem("todos") || "";
if (todosData) {
todosData = JSON.parse(todosData); // 把 todos 的初始值直接設為「從 localStorage 拿出來的 todos」
id.current = todosData[0].id + 1; // 把 id 設為「localStorage 裡面最大的 id 再加 1」
} else {
todosData = [];
}
return todosData;
});
// 把 todos 存到 localStorage 裡面去
useEffect(() => {
writeTodosToLocalStorage(todos);
}, [todos]);
推薦閱讀文章 A Complete Guide to useEffect 和 How Are Function Components Different from Classes?
第一次 render 的 todos
就永遠都不會改變了
setTodos()
只是在「調整下一次 render 的 todos
要長什麼樣子」,並不會去改變上一次 render 的 todos
以官網為範例:
使用者一進入網站時,就連到他的 userId 去拿資料 > 拿到這個 userId 的資料後就可以進行處理
當今天換了一個 userId 進入網站時,我要先用 cleanup function 斷掉上一個的 connection,再重新連到下一個 userId 的資料
useEffect(() => {
WebSocket.CONNECTING(userId).subscribe(() => {
// ...
});
// cleanup function
return () => {
WebSocket.disconnect(userId);
};
}, [userId]);
所以,當我想要在 component 不見時做什麼事情,就可以寫在 cleanup function
下面的程式碼中,把 useEffect 的第二個參數放上一個「空陣列」,
如此一來,這裡的 cleanup function 就只有在 Unmount 時會執行
原因為:
這個 useEffects 只有在第一次 render 時會執行,因此不會發生第一個時機點的情況(不會第二次執行 useEffects),所以就只有在 Unmount 時會執行
useEffect(() => {
// component 要 mount 時想要做什麼事情,就寫在這裡
console.log("Mount");
return () => {
// cleanup function(component 要 unmount 時想要做什麼事情,就寫在這裡)
console.log("Unmount");
};
}, []);
意思就是:
在 component 底下,不能把 hooks 包在 if...else
之類的條件判斷裡面,例如
// 把 todos 存到 localStorage 裡面去
if (true) {
useEffect(() => {
writeTodosToLocalStorage(todos);
console.log("useEffect: todos", todos);
// cleanup function
return () => {
console.log("clearEffect: todos", todos);
};
}, [todos]);
}
上面這樣寫的話,React 就會報錯:
React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render
舉另一個例子,我想要做的功能是:如果沒有 todos
我就不要用這個 hook
錯誤寫法:用 if...else
包住 hook
if (!todos) {
useEffect(() => {
writeTodosToLocalStorage(todos);
console.log("useEffect: todos", todos);
// cleanup function
return () => {
console.log("clearEffect: todos", todos);
};
}, [todos]);
}
正確寫法:在 hook 裡面做判斷
在最前面加上 if (!todos) return
// 把 todos 存到 localStorage 裡面去
useEffect(() => {
if (!todos) return; // 如果沒有 todos 的話,就直接 return
writeTodosToLocalStorage(todos);
console.log("useEffect: todos", todos);
// cleanup function
return () => {
console.log("clearEffect: todos", todos);
};
}, [todos]);
自己定義的 hook 就叫做 custom hook
custom hook 一定要用 use 開頭
onChange
時會 setValue()
這整件事情」包成一個 useInput
hook作法如下:
新增一個檔案叫做 useInput.js
useInput
function 並且 export 出去,這個 useInput
function 就是我自定義的 hookvalue
, setValue
, handleChange
useInput.js:
import { useState } from "react";
export default function useInput() {
const [value, setValue] = useState("");
const handleChange = (e) => {
setValue(e.target.value);
};
return {
value,
setValue,
handleChange,
};
}
然後,在 App.js 就可以這樣使用這個 useInput
的 hook:
先把 useInput
import 進來
import useInput from "./useInput";
然後,在 function App()
裡面,就可以用解構的語法,拿到 useInput()
回傳的 value
, setValue
, handleChange
hook 其實就是一個 function,所以呼叫 useInput()
來執行這個 hook
const { value, setValue, handleChange } = useInput();
在 input
的 onChange
就可以直接寫 handleChange
了
<input
type="text"
placeholder="add todo"
value={value}
onChange={handleChange}
/>
App.js:
import TodoItem from "./TodoItem";
import { useState, useRef, useEffect, useLayoutEffect } from "react"; // 用解構的語法
import useInput from "./useInput";
function writeTodosToLocalStorage(todos) {
window.localStorage.setItem("todos", JSON.stringify(todos));
}
function App() {
const id = useRef(1);
const [todos, setTodos] = useState(() => {
console.log("init");
let todosData = window.localStorage.getItem("todos") || "";
if (todosData) {
todosData = JSON.parse(todosData); // 把 todos 的初始值直接設為「從 localStorage 拿出來的 todos」
id.current = todosData[0].id + 1; // 把 id 設為「localStorage 裡面最大的 id 再加 1」
} else {
todosData = [];
}
return todosData;
});
const { value, setValue, handleChange } = useInput(); // 用解構的語法,拿到 useInput() 回傳的 value, setValue, handleChange
// 把 todos 存到 localStorage 裡面去
useEffect(() => {
if (!todos) return; // 如果沒有 todos 的話,就直接 return
writeTodosToLocalStorage(todos);
console.log("useEffect: todos", todos);
// cleanup function
return () => {
console.log("clearEffect: todos", todos);
};
}, [todos]);
// 新增 todo
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
};
// 刪除 todo
const handleDeleteTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
// 切換已完成/未完成
const handleToggleIsFinished = (id) => {
setTodos(
todos.map((todo) => {
if (todo.id !== id) return todo;
return {
...todo,
isFinished: !todo.isFinished,
};
})
);
};
return (
<div className="App">
<input
type="text"
placeholder="add todo"
value={value}
onChange={handleChange}
/>
<button onClick={handleButtonClick}>Add todo</button>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
handleDeleteTodo={handleDeleteTodo}
handleToggleIsFinished={handleToggleIsFinished}
/>
))}
</div>
);
}
export default App;
改寫之後,我就不需要寫下面這兩段:
const [value, setValue] = useState("");
const handleInputChange = (e) => {
setValue(e.target.value);
};
這樣改寫完後,todo list 一樣可以照常跑
例如:
假設我有第二個 input
,我就可以共用 useInput.js 裡面的程式碼
只要將傳進來的三個東西,分別取一個新的名字即可(用 ES6 的語法)
value
改名叫 todoName
setValue
改名叫 setTodoName
handleChange
改名叫 handleTodoNameChange
const { value, setValue, handleChange } = useInput(); // 用解構的語法,拿到 useInput() 回傳的 value, setValue, handleChange
const {
// 將傳進來的三個東西,分別取一個新的名稱
value: todoName,
setValue: setTodoName,
handleChange: handleTodoNameChange,
} = useInput();
useTodos
的 hook把所有跟 todos
相關的邏輯都抽出來,放到另一個 useTodos.js 檔案
把 useInput
這個 hook 也整合到 useTodos
裡面
useInput.js:
import { useState } from "react";
export default function useInput() {
const [value, setValue] = useState("");
const handleChange = (e) => {
setValue(e.target.value);
};
return {
value,
setValue,
handleChange,
};
}
useTodos.js:
import { useState, useEffect, useRef } from "react";
import useInput from "./useInput";
function writeTodosToLocalStorage(todos) {
window.localStorage.setItem("todos", JSON.stringify(todos));
}
export default function useTodos() {
const id = useRef(1);
const { value, setValue, handleChange } = useInput();
const [todos, setTodos] = useState(() => {
console.log("init");
let todosData = window.localStorage.getItem("todos") || "";
if (todosData) {
todosData = JSON.parse(todosData); // 把 todos 的初始值直接設為「從 localStorage 拿出來的 todos」
id.current = todosData[0].id + 1; // 把 id 設為「localStorage 裡面最大的 id 再加 1」
} else {
todosData = [];
}
return todosData;
});
// 把 todos 存到 localStorage 裡面去
useEffect(() => {
if (!todos) return; // 如果沒有 todos 的話,就直接 return
writeTodosToLocalStorage(todos);
}, [todos]);
// 新增 todo
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value,
isFinished: false,
},
...todos,
]);
setValue("");
id.current++;
};
// 刪除 todo
const handleDeleteTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
// 切換已完成/未完成
const handleToggleIsFinished = (id) => {
setTodos(
todos.map((todo) => {
if (todo.id !== id) return todo;
return {
...todo,
isFinished: !todo.isFinished,
};
})
);
};
return {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
};
}
App.js:
import TodoItem from "./TodoItem";
import useTodos from "./useTodos";
function App() {
// 用解構的語法,拿到「從各個 hooks 回傳的東西」
const {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsFinished,
value,
setValue,
handleChange,
} = useTodos();
// UI
return (
<div className="App">
<input
type="text"
placeholder="add todo"
value={value}
onChange={handleChange}
/>
<button onClick={handleButtonClick}>Add todo</button>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
handleDeleteTodo={handleDeleteTodo}
handleToggleIsFinished={handleToggleIsFinished}
/>
))}
</div>
);
}
export default App;
邏輯:會在各個 hooks
UI:會在 App.js
因為「把邏輯都抽出來寫到 hooks 裡面去了」,在 App.js 裡面就只會剩下:「從各個 hooks 拿東西」和「UI」而已
hooks 有分為幾種:
基本上,要做什麼事情都會用 useEffect 來寫,並不會把要做的事情直接寫在 function component 的第一層,如下:
function App() {
localStorage.setItem(...) // 把要做的事情直接寫在 function component 的第一層
原因有兩個:
這邊假設我要做的事情是「把 todos
儲存到 localStorage 裡面去」
todos
的 state 有改變時再儲存就好。因此,要把「儲存到 localStorage」寫在 useEffect 裡面,這樣我就可以在 useEffect 的第二個參數傳入 todos
去設定「在 todos
(指定的 state)改變時,才會去執行這個 useEffect」]]>
首先,輸入以下指令來安裝 husky
, lint-staged
和 prettier
npm install --save husky lint-staged prettier
安裝好後,在 package.json 的任意位置加上這段:
husky 也是一個套件,lint-staged 也是一個套件
我在 commit 之前(pre-commit),我要執行 lint-staged
在執行 lint-staged
時,針對符合 src/**/*.{js,jsx,ts,tsx,json,css,scss,md}
這些規則的檔案,我要跑 prettier --write
(針對要 commit 的這些檔案執行 prettier --write
)
因為我不希望每次 commit 都對所有檔案做 prettier
,如果是已經 commit 過的檔案,就不需要再對它執行 prettier
了(已經 prettier
好了)
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/*.{js,jsx,ts,tsx,json,css,scss,md}": [
"prettier --write"
]
},
注意,在文件上寫的規則是 "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}"
,這樣會是「src 底下的子資料夾底下的檔案」
但是,在我的專案中,App.js 這些檔案就直接是在「src 底下」,所以規則要改成 "src/*.{js,jsx,ts,tsx,json,css,scss,md}"
這樣,在每次 commit 時,就會幫我用 prettier
做 code formatting 了
第二種方式是用 vs code 的 plugin 來安裝 prettier
在 Extensions 搜尋 Prettier - Code formatter
安裝好之後,我可以設定在存檔時跑 prettier
設定方式:
在 vs code 的 Settings 搜尋「Format On Save」,把「Format On Save」選項打勾
接著,在 Settings 搜尋「Default Formatter」> 選擇 esbenp.prettier-vscode
這樣,在每次存檔時,就會幫我用 prettier
做 code formatting 了
可參考 Format on Save (prettier) stopped working with latest update
也就是說,我在欄位輸入 <h1>hello</h1>
按下新增,JSX 會用純文字顯示出來,而不會是 h1
的標籤
dangerouslySetInnerHTML
這個功能文件可參考 dangerouslySetInnerHTML
把 <TodoContent>
改成:
在 dangerouslySetInnerHTML={}
裡面傳入一個物件,物件裡面在 __html:
後面放我要 render 出來的內容 todo.content
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent
$isFinished={todo.isFinished}
size={size}
dangerouslySetInnerHTML={{
__html: todo.content,
}}
></TodoContent>
這樣就可以輸出 HTML 標籤了(我在欄位輸入的內容,會真的變成 innerHTML
)
<a></a>
的 click-based XSS無論是否在寫 React,都要注意 click-based 的 XSS 的問題
例如:
我 render 出一個 a
連結 <a href={todo.content}>click me!</a>
todo.content
就是我在欄位輸入的內容
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent $isFinished={todo.isFinished} size={size}>
{todo.content}
</TodoContent>
<a href={todo.content}>click me!</a>
然後,如果我在欄位輸入 javascript: alert('haha') 按下新增,當我點擊「click me!」這個 a
連結時,就會執行 javascript: alert(‘haha’)
這行 JavaScript(跳出 haha 的 alert)
「click-based 的 XSS」運用的特性就是:
在 a
連結的 href=""
裡面,如果用「javascript: 」就可以在點擊時執行 JavaScript 的程式碼
之所以 React 無法防範這個 click-based 的 XSS 是因為:
React 內建的防範只會幫我做 escape,只會跳脫 <
, >
, ''
, ""
這些特殊符號,並不會把冒號 :
做跳脫
window.encodeURIComponent()
來修正 click-based 的 XSS用 window.encodeURIComponent()
把 todo.content
做編碼
<a href={window.encodeURIComponent(todo.content)}>click me!</a>
這樣,如果我輸入 javascript: alert(‘yo’) 按下新增,當我點擊「click me!」這個 a
連結時,javascript: alert(‘yo’) 會被編碼為 javascript%3A%20alert(‘yo’)
(會把冒號 :
做編碼)
因此,網頁會導向到 http://localhost:3000/javascript%3A%20alert(%E2%80%98yo%E2%80%99) ,但是並不會跳出 alert
]]>首先,輸入以下指令來安裝 husky
, lint-staged
和 prettier
npm install --save husky lint-staged prettier
安裝好後,在 package.json 的任意位置加上這段:
husky 也是一個套件,lint-staged 也是一個套件
我在 commit 之前(pre-commit),我要執行 lint-staged
在執行 lint-staged
時,針對符合 src/**/*.{js,jsx,ts,tsx,json,css,scss,md}
這些規則的檔案,我要跑 prettier --write
(針對要 commit 的這些檔案執行 prettier --write
)
因為我不希望每次 commit 都對所有檔案做 prettier
,如果是已經 commit 過的檔案,就不需要再對它執行 prettier
了(已經 prettier
好了)
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/*.{js,jsx,ts,tsx,json,css,scss,md}": [
"prettier --write"
]
},
注意,在文件上寫的規則是 "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}"
,這樣會是「src 底下的子資料夾底下的檔案」
但是,在我的專案中,App.js 這些檔案就直接是在「src 底下」,所以規則要改成 "src/*.{js,jsx,ts,tsx,json,css,scss,md}"
這樣,在每次 commit 時,就會幫我用 prettier
做 code formatting 了
第二種方式是用 vs code 的 plugin 來安裝 prettier
在 Extensions 搜尋 Prettier - Code formatter
安裝好之後,我可以設定在存檔時跑 prettier
設定方式:
在 vs code 的 Settings 搜尋「Format On Save」,把「Format On Save」選項打勾
接著,在 Settings 搜尋「Default Formatter」> 選擇 esbenp.prettier-vscode
這樣,在每次存檔時,就會幫我用 prettier
做 code formatting 了
可參考 Format on Save (prettier) stopped working with latest update
也就是說,我在欄位輸入 <h1>hello</h1>
按下新增,JSX 會用純文字顯示出來,而不會是 h1
的標籤
dangerouslySetInnerHTML
這個功能文件可參考 dangerouslySetInnerHTML
把 <TodoContent>
改成:
在 dangerouslySetInnerHTML={}
裡面傳入一個物件,物件裡面在 __html:
後面放我要 render 出來的內容 todo.content
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent
$isFinished={todo.isFinished}
size={size}
dangerouslySetInnerHTML={{
__html: todo.content,
}}
></TodoContent>
這樣就可以輸出 HTML 標籤了(我在欄位輸入的內容,會真的變成 innerHTML
)
<a></a>
的 click-based XSS無論是否在寫 React,都要注意 click-based 的 XSS 的問題
例如:
我 render 出一個 a
連結 <a href={todo.content}>click me!</a>
todo.content
就是我在欄位輸入的內容
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent $isFinished={todo.isFinished} size={size}>
{todo.content}
</TodoContent>
<a href={todo.content}>click me!</a>
然後,如果我在欄位輸入 javascript: alert('haha') 按下新增,當我點擊「click me!」這個 a
連結時,就會執行 javascript: alert(‘haha’)
這行 JavaScript(跳出 haha 的 alert)
「click-based 的 XSS」運用的特性就是:
在 a
連結的 href=""
裡面,如果用「javascript: 」就可以在點擊時執行 JavaScript 的程式碼
之所以 React 無法防範這個 click-based 的 XSS 是因為:
React 內建的防範只會幫我做 escape,只會跳脫 <
, >
, ''
, ""
這些特殊符號,並不會把冒號 :
做跳脫
window.encodeURIComponent()
來修正 click-based 的 XSS用 window.encodeURIComponent()
把 todo.content
做編碼
<a href={window.encodeURIComponent(todo.content)}>click me!</a>
這樣,如果我輸入 javascript: alert(‘yo’) 按下新增,當我點擊「click me!」這個 a
連結時,javascript: alert(‘yo’) 會被編碼為 javascript%3A%20alert(‘yo’)
(會把冒號 :
做編碼)
因此,網頁會導向到 http://localhost:3000/javascript%3A%20alert(%E2%80%98yo%E2%80%99) ,但是並不會跳出 alert
]]>.map()
這種 functional 的方式去做
因為沒有迴圈,所以 .forEach()
也不會有東西,所以會用 .map()
.map()
的好處是:可以把陣列裡面的每一個東西,都 map 成一個 component,如下程式碼這時的 todos
陣列就會是 [123, 456]
.map()
會產生出一個陣列
function App() {
const [todos, setTodos] = React.useState([
123, 456
]);
const handleButtonClick = () => {
}
return (
<div className="App">
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem content={todo} />)
}
</div>
);
}
上面用 .map()
的寫法,就等同於是下面這樣寫:
用大括號包住一個陣列,陣列裡面傳入我要 render 的很多 components
function App() {
const [todos, setTodos] = React.useState([
123, 456
]);
const handleButtonClick = () => {
}
return (
<div className="App">
<button onClick={handleButtonClick}>Add todo</button>
{
[<TodoItem content={123} />, <TodoItem content={456} />]
}
</div>
);
}
在 console 會看到一個 warning 寫說:「Warning: Each child in a list should have a unique "key" prop.」
key
的 prop,讓 React 可以辨別陣列裡面的每一個 itemkey
是不能重複的
這裡,會先用 index
當作 key(但其實是不建議用 index
當作 key)
建議是用 todo.id
當作 key (請看 範例 - 新增 todo)
setTodos
的錯誤寫法function App() {
const [todos, setTodos] = React.useState([
123, 456
]);
const handleButtonClick = () => {
todos.push(777); // 這時的 todos 會是 [123, 456, 777]
setTodos(todos); // setTodos 裡面的 todos 也是 [123, 456, 777]
}
return (
<div className="App">
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map((todo, index) => <TodoItem key={index} content={todo} />)
}
</div>
);
}
上面這樣寫,會發現
點擊「Add todo 按鈕」不會有任何反應,原因為:
todos.push(777)
之後,這時的 todos
會是 [123, 456, 777]
(這是舊的 state)
setTodos(todos)
裡面的 todos 也是 [123, 456, 777]
(這是新的 state)
當 React 判定「舊的 state」跟「新的 state」是一樣時,就不會做任何事情
setTodos
產生一個新的 state」原本的 todos
是「不會變的」
setTodos
的正確寫法在 React 的 state 是「immutable 不可改變的」
因此,如果是要「新增」的話,要這樣寫:
setTodos([...todos, 777]);
在 setTodos()
裡面,建立一個新的陣列,用解構的語法把 todos
原本的值複製過來,後面再加上我要新增的東西
import styled from 'styled-components';
import TodoItem from './TodoItem';
import { useState } from 'react'; // 用解構的語法
const YellowTodoItem = styled(TodoItem)`
background-color: yellow;
`
function App() {
const [todos, setTodos] = useState([
123, 456
]);
const handleButtonClick = () => {
setTodos([...todos, 777]);
}
return (
<div className="App">
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map((todo, index) => <TodoItem key={index} content={todo} />)
}
</div>
);
}
export default App;
React 的 component 有分兩種:
範例:
把 input
的 value
(也就是 input
會顯示在畫面上的值) 放到一個 state 裡面
每當 state 改變,就會 re-render
function App() {
const [value, setValue] = useState('');
const [todos, setTodos] = useState([
123, 456
]);
const handleButtonClick = () => {
setTodos([...todos, 777]);
}
const handleInputChange = (e) => {
setValue(e.target.value);
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map((todo, index) => <TodoItem key={index} content={todo} />)
}
</div>
);
}
要拿出 input
的 value 有幾種方法
在 input
裡面不加上 value={value} onChange={handleInputChange}
這兩個
當我要拿出 input
的 value 時我再用 document.querySelector('.input-todo').value
拿出來
function App() {
const [todos, setTodos] = useState([
123, 456
]);
const handleButtonClick = () => {
const value = document.querySelector('.input-todo').value;
setTodos([...todos, 777]);
}
return (
<div className="App">
<input className="input-todo" type="text" placeholder="add todo" />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map((todo, index) => <TodoItem key={index} content={todo} />)
}
</div>
);
}
先在上方引入 useRef
這個 function
在 input
傳入一個參數 ref
來存取到這個 DOM 元素
import styled from 'styled-components';
import TodoItem from './TodoItem';
import { useState, useRef } from 'react'; // 用解構的語法
const YellowTodoItem = styled(TodoItem)`
background-color: yellow;
`
function App() {
const [todos, setTodos] = useState([
123, 456
]);
const inputRef = useRef();
const handleButtonClick = () => {
console.log(inputRef);
setTodos([...todos, 777]);
}
return (
<div className="App">
<input ref={inputRef} className="input-todo" type="text" placeholder="add todo" />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map((todo, index) => <TodoItem key={index} content={todo} />)
}
</div>
);
}
export default App;
console.log(inputRef)
會印出一個物件,物件裡面有 current 這個值(這是 React 提供的)
因此,inputRef.current
就會是 input
這個 DOM 元素
我就可以用 inputRef.current.value
來拿到 input
的 value
接下來的範例,會使用「controlled component」這個方法
setTodos()
用 ...todos
的方式來新增 todo
在 setTodos()
小括號裡面,要產生一個新的陣列
import TodoItem from './TodoItem';
import { useState } from 'react'; // 用解構的語法
let id = 2; // 會一直遞增的 id
function App() {
const [value, setValue] = useState('');
const [todos, setTodos] = useState([
{id: 1, content: 'abc'}
]);
const handleInputChange = (e) => {
setValue(e.target.value);
}
const handleButtonClick = () => {
setTodos([
{
id: id,
content: value
}, ...todos]);
setValue('');
id ++
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} content={todo.content} />)
}
</div>
);
}
export default App;
id
存成 state不好的原因是:
id 的改變並不會造成「畫面的改變」,並不需要 re-render 畫面
但是如果把 id
存成 state,每當 id
改變時(state 改變),畫面就會 re-render,造成不必要的效能浪費
import TodoItem from './TodoItem';
import { useState } from 'react'; // 用解構的語法
function App() {
const [value, setValue] = useState('');
const [todos, setTodos] = useState([
{id: 1, content: 'abc'}
]);
const [id, setId] = useState(2);
const handleInputChange = (e) => {
setValue(e.target.value);
}
const handleButtonClick = () => {
setTodos([
{
id: id,
content: value
}, ...todos]);
setValue('');
setId(id + 1);
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} content={todo.content} />)
}
</div>
);
}
export default App;
因此,不該把 id
存成 state
useRef
useRef
為了要讓「值可以保存住」,useRef
可以當成 state 來用,也可以直接操作。但是在 component re-render 時,useRef
的值不會變
useRef(2)
來設定 id
的初始值是 2useRef(2)
會回傳下面的物件,所以要用 id.current
才能拿到 useRef(2)
小括號裡面的值,也就是 2 {
current: 2
}
id
的值,就用 id.current++
todos.map(todo => <TodoItem key={todo.id} todo={todo} />)
為了確定是否有成功把 id
放到 todo 上面,可以在 <TodoItem>
直接把整個 todo
傳入 App.js:
import TodoItem from './TodoItem';
import { useState, useRef } from 'react'; // 用解構的語法
function App() {
const [value, setValue] = useState('');
const [todos, setTodos] = useState([
{id: 1, content: 'abc'}
]);
const id = useRef(2);
const handleInputChange = (e) => {
setValue(e.target.value);
}
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value
}, ...todos]);
setValue('');
id.current++
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} />)
}
</div>
);
}
export default App;
TodoItem.js:
export default function TodoItem({ className, size, todo }){
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button>已完成</Button>
<RedButton>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
這樣就可以看到:
data-todo-id
有成功放到 todo 上面了
在 App.js 裡面,會 render 出 TodoItem,所以
當我按下了在 TodoItem.js 的 <RedButton>刪除</RedButton>
,我要怎麼去改變 App.js 裡面的 state 呢?
作法如下:
這樣就可以達成「在 child 改變 “parent 的 state”」
在 App.js 宣告一個 function 叫做 handleDeleteTodo
,會接收一個 id
handleDeleteTodo
這個 function 當作 prop 傳給 <TodoItem>
App.js:
function App() {
const [value, setValue] = useState('');
const [todos, setTodos] = useState([
{id: 1, content: 'abc'}
]);
const id = useRef(2);
const handleInputChange = (e) => {
setValue(e.target.value);
}
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value
}, ...todos]);
setValue('');
id.current++
}
const handleDeleteTodo = id => {
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} />)
}
</div>
);
}
這時,在 TodoItem.js 裡面的 TodoItem
就可以接收到 handleDeleteTodo
這個 function
接著,就可以在 TodoItem
component 的 <RedButton>
加上 onClick
事件:當點擊刪除按鈕時,執行 handleDeleteTodo
function,並傳入 todo.id
TodoItem.js:
export default function TodoItem({ className, size, todo, handleDeleteTodo }){
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button>已完成</Button>
<RedButton onClick={() => {
handleDeleteTodo(todo.id)
}
}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
setTodos()
用 .filter()
來刪除指定的 todo
執行 handleDeleteTodo
function 就會去呼叫在 App.js 的 handleDeleteTodo
function
我就可以在 App.js 裡面,用 handleDeleteTodo
function 接收到的 id
來把該 todo 刪掉
因此,在 handleDeleteTodo
function 裡面,用 setTodo()
來更新 todos
陣列:
不能用 .splice()
來刪除 todo,因為 .splice()
會改到原本的 todos
陣列
.filter()
來刪除 todo,因為 .filter()
會產生一個新的陣列,不會去改到原本的 todos
陣列App.js:
...
const handleDeleteTodo = id => {
setTodos(todos.filter(todo => todo.id !== id))
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} />)
}
</div>
);
}
因為在 JSX 語法中,沒有 if...else
,所以要寫判斷式的話,有兩個方式:
TodoItem.js:
用 {todo.isFinished ? '未完成' : '已完成'}
三元運算子來決定 todo
是要顯示已完成 or 未完成
export default function TodoItem({ className, size, todo, handleDeleteTodo }){
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button>
{todo.isFinished ? '未完成' : '已完成'}
</Button>
<RedButton onClick={() => {
handleDeleteTodo(todo.id)
}
}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
然後,同樣在 TodoItem.js 裡面,就可以在 TodoContent
的 styled component 用 props
加上 text-decoration: line-through
這段
用 &&
意思就是:如果 props.isFinished
是 true 的話,就會回傳後面那行 text-decoration: line-through;
const TodoContent = styled.div`
color: ${props => props.theme.colors.primary_500};
font-size: 16px;
${props => props.size === 'XL' && `
font-size: 20px;
`}
${props => props.isFinished && `
text-decoration: line-through;
`}
`
{todo.isFinished && '未完成'}
意思是:如果 todo.isFinished
是 true,就會回傳 '未完成'{!todo.isFinished && '已完成'}
意思是:如果 todo.isFinished
是 false,就會回傳 '已完成'export default function TodoItem({ className, size, todo, handleDeleteTodo, handleToggleIsFinished }){
const handleToggleClick = () => {
handleToggleIsFinished(todo.id)
}
const handleDeleteClick = () => {
handleDeleteTodo(todo.id)
}
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent $isFinished={todo.isFinished} size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button onClick={handleToggleClick}>
{todo.isFinished && '未完成'}
{!todo.isFinished && '已完成'}
</Button>
<RedButton onClick={handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
可參考文件 Transient props
props
,除了會傳給 styled component 之外,也會直接傳到 DOM 上面例如我在 <TodoContent>
加上一個 props
叫做 id="abc"
<TodoContent id="abc" isFinished={todo.isFinished} size={size}>{todo.content}</TodoContent>
render 出來的 DOM 就會有 id="abc"
這個屬性:
props
傳給 DOM,我只想要傳給 styled component 做 style 的處理就好,那就在 props
前面加一個 $
即可加上 $
就會是一個 transient prop
在 id="abc"
前面加一個 $
<TodoContent $id="abc" isFinished={todo.isFinished} size={size}>{todo.content}</TodoContent>
render 出來的 DOM 就不會有 id="abc"
這個屬性了:
在 styled component 用時,就要寫成 props.$id
,而不是 props.id
:
const TodoContent = styled.div`
color: ${props => props.theme.colors.primary_500};
font-size: 16px;
${props => props.size === 'XL' && `
font-size: 20px;
`}
${props => props.$isFinished && `
text-decoration: line-through;
`}
${props => props.$id && `
color: red
`}
`
宣告一個 handleToggleIsFinished
function 來處理「已完成/未完成」
setTodos()
用 .map()
修改指定的 todo
的屬性.map()
會產生出一個新的陣列,我就可以在這個新的陣列裡面修改那個 id
的 todo
(修改 isFinished
的狀態)
handleToggleIsFinished
當作 prop 傳到 <TodoItem>
App.js:
...
// 切換已完成/未完成
const handleToggleIsFinished = id => {
setTodos(todos.map(todo => {
if (todo.id !== id) return todo;
return {
...todo,
isFinished: !todo.isFinished
}
}))
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} handleToggleIsFinished={handleToggleIsFinished} />)
}
</div>
);
}
這樣,在 TodoItem.js 的 <TodoItem>
就可以接受到 handleToggleIsFinished
在 <Button>
就可以加上 onClick
的事件了
這裡把 onClick
裡面的 function 抽出來寫(handleToggleClick
),這樣的可讀性會比直接寫在 onClick
裡面好(直接寫在裡面叫做 inline function)
export default function TodoItem({ className, size, todo, handleDeleteTodo, handleToggleIsFinished }){
const handleToggleClick = () => {
handleToggleIsFinished(todo.id)
}
const handleDeleteClick = () => {
handleDeleteTodo(todo.id)
}
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent $isFinished={todo.isFinished} size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button onClick={handleToggleClick}>
{todo.isFinished ? '未完成' : '已完成'}
</Button>
<RedButton onClick={handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
在 setTodos()
小括號裡面,要產生一個新的陣列
setTodos()
用 ...todos
的解構語法來新增 todo
setTodos()
用 .filter()
來刪除指定的 todo
setTodos()
用 .map()
修改指定的 todo
的屬性從 todo list 學到的重點觀念有:
.map()
這種 functional 的方式去做
因為沒有迴圈,所以 .forEach()
也不會有東西,所以會用 .map()
.map()
的好處是:可以把陣列裡面的每一個東西,都 map 成一個 component,如下程式碼這時的 todos
陣列就會是 [123, 456]
.map()
會產生出一個陣列
function App() {
const [todos, setTodos] = React.useState([
123, 456
]);
const handleButtonClick = () => {
}
return (
<div className="App">
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem content={todo} />)
}
</div>
);
}
上面用 .map()
的寫法,就等同於是下面這樣寫:
用大括號包住一個陣列,陣列裡面傳入我要 render 的很多 components
function App() {
const [todos, setTodos] = React.useState([
123, 456
]);
const handleButtonClick = () => {
}
return (
<div className="App">
<button onClick={handleButtonClick}>Add todo</button>
{
[<TodoItem content={123} />, <TodoItem content={456} />]
}
</div>
);
}
在 console 會看到一個 warning 寫說:「Warning: Each child in a list should have a unique "key" prop.」
key
的 prop,讓 React 可以辨別陣列裡面的每一個 itemkey
是不能重複的
這裡,會先用 index
當作 key(但其實是不建議用 index
當作 key)
建議是用 todo.id
當作 key (請看 範例 - 新增 todo)
setTodos
的錯誤寫法function App() {
const [todos, setTodos] = React.useState([
123, 456
]);
const handleButtonClick = () => {
todos.push(777); // 這時的 todos 會是 [123, 456, 777]
setTodos(todos); // setTodos 裡面的 todos 也是 [123, 456, 777]
}
return (
<div className="App">
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map((todo, index) => <TodoItem key={index} content={todo} />)
}
</div>
);
}
上面這樣寫,會發現
點擊「Add todo 按鈕」不會有任何反應,原因為:
todos.push(777)
之後,這時的 todos
會是 [123, 456, 777]
(這是舊的 state)
setTodos(todos)
裡面的 todos 也是 [123, 456, 777]
(這是新的 state)
當 React 判定「舊的 state」跟「新的 state」是一樣時,就不會做任何事情
setTodos
產生一個新的 state」原本的 todos
是「不會變的」
setTodos
的正確寫法在 React 的 state 是「immutable 不可改變的」
因此,如果是要「新增」的話,要這樣寫:
setTodos([...todos, 777]);
在 setTodos()
裡面,建立一個新的陣列,用解構的語法把 todos
原本的值複製過來,後面再加上我要新增的東西
import styled from 'styled-components';
import TodoItem from './TodoItem';
import { useState } from 'react'; // 用解構的語法
const YellowTodoItem = styled(TodoItem)`
background-color: yellow;
`
function App() {
const [todos, setTodos] = useState([
123, 456
]);
const handleButtonClick = () => {
setTodos([...todos, 777]);
}
return (
<div className="App">
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map((todo, index) => <TodoItem key={index} content={todo} />)
}
</div>
);
}
export default App;
React 的 component 有分兩種:
範例:
把 input
的 value
(也就是 input
會顯示在畫面上的值) 放到一個 state 裡面
每當 state 改變,就會 re-render
function App() {
const [value, setValue] = useState('');
const [todos, setTodos] = useState([
123, 456
]);
const handleButtonClick = () => {
setTodos([...todos, 777]);
}
const handleInputChange = (e) => {
setValue(e.target.value);
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map((todo, index) => <TodoItem key={index} content={todo} />)
}
</div>
);
}
要拿出 input
的 value 有幾種方法
在 input
裡面不加上 value={value} onChange={handleInputChange}
這兩個
當我要拿出 input
的 value 時我再用 document.querySelector('.input-todo').value
拿出來
function App() {
const [todos, setTodos] = useState([
123, 456
]);
const handleButtonClick = () => {
const value = document.querySelector('.input-todo').value;
setTodos([...todos, 777]);
}
return (
<div className="App">
<input className="input-todo" type="text" placeholder="add todo" />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map((todo, index) => <TodoItem key={index} content={todo} />)
}
</div>
);
}
先在上方引入 useRef
這個 function
在 input
傳入一個參數 ref
來存取到這個 DOM 元素
import styled from 'styled-components';
import TodoItem from './TodoItem';
import { useState, useRef } from 'react'; // 用解構的語法
const YellowTodoItem = styled(TodoItem)`
background-color: yellow;
`
function App() {
const [todos, setTodos] = useState([
123, 456
]);
const inputRef = useRef();
const handleButtonClick = () => {
console.log(inputRef);
setTodos([...todos, 777]);
}
return (
<div className="App">
<input ref={inputRef} className="input-todo" type="text" placeholder="add todo" />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map((todo, index) => <TodoItem key={index} content={todo} />)
}
</div>
);
}
export default App;
console.log(inputRef)
會印出一個物件,物件裡面有 current 這個值(這是 React 提供的)
因此,inputRef.current
就會是 input
這個 DOM 元素
我就可以用 inputRef.current.value
來拿到 input
的 value
接下來的範例,會使用「controlled component」這個方法
setTodos()
用 ...todos
的方式來新增 todo
在 setTodos()
小括號裡面,要產生一個新的陣列
import TodoItem from './TodoItem';
import { useState } from 'react'; // 用解構的語法
let id = 2; // 會一直遞增的 id
function App() {
const [value, setValue] = useState('');
const [todos, setTodos] = useState([
{id: 1, content: 'abc'}
]);
const handleInputChange = (e) => {
setValue(e.target.value);
}
const handleButtonClick = () => {
setTodos([
{
id: id,
content: value
}, ...todos]);
setValue('');
id ++
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} content={todo.content} />)
}
</div>
);
}
export default App;
id
存成 state不好的原因是:
id 的改變並不會造成「畫面的改變」,並不需要 re-render 畫面
但是如果把 id
存成 state,每當 id
改變時(state 改變),畫面就會 re-render,造成不必要的效能浪費
import TodoItem from './TodoItem';
import { useState } from 'react'; // 用解構的語法
function App() {
const [value, setValue] = useState('');
const [todos, setTodos] = useState([
{id: 1, content: 'abc'}
]);
const [id, setId] = useState(2);
const handleInputChange = (e) => {
setValue(e.target.value);
}
const handleButtonClick = () => {
setTodos([
{
id: id,
content: value
}, ...todos]);
setValue('');
setId(id + 1);
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} content={todo.content} />)
}
</div>
);
}
export default App;
因此,不該把 id
存成 state
useRef
useRef
為了要讓「值可以保存住」,useRef
可以當成 state 來用,也可以直接操作。但是在 component re-render 時,useRef
的值不會變
useRef(2)
來設定 id
的初始值是 2useRef(2)
會回傳下面的物件,所以要用 id.current
才能拿到 useRef(2)
小括號裡面的值,也就是 2 {
current: 2
}
id
的值,就用 id.current++
todos.map(todo => <TodoItem key={todo.id} todo={todo} />)
為了確定是否有成功把 id
放到 todo 上面,可以在 <TodoItem>
直接把整個 todo
傳入 App.js:
import TodoItem from './TodoItem';
import { useState, useRef } from 'react'; // 用解構的語法
function App() {
const [value, setValue] = useState('');
const [todos, setTodos] = useState([
{id: 1, content: 'abc'}
]);
const id = useRef(2);
const handleInputChange = (e) => {
setValue(e.target.value);
}
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value
}, ...todos]);
setValue('');
id.current++
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} />)
}
</div>
);
}
export default App;
TodoItem.js:
export default function TodoItem({ className, size, todo }){
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button>已完成</Button>
<RedButton>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
這樣就可以看到:
data-todo-id
有成功放到 todo 上面了
在 App.js 裡面,會 render 出 TodoItem,所以
當我按下了在 TodoItem.js 的 <RedButton>刪除</RedButton>
,我要怎麼去改變 App.js 裡面的 state 呢?
作法如下:
這樣就可以達成「在 child 改變 “parent 的 state”」
在 App.js 宣告一個 function 叫做 handleDeleteTodo
,會接收一個 id
handleDeleteTodo
這個 function 當作 prop 傳給 <TodoItem>
App.js:
function App() {
const [value, setValue] = useState('');
const [todos, setTodos] = useState([
{id: 1, content: 'abc'}
]);
const id = useRef(2);
const handleInputChange = (e) => {
setValue(e.target.value);
}
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value
}, ...todos]);
setValue('');
id.current++
}
const handleDeleteTodo = id => {
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} />)
}
</div>
);
}
這時,在 TodoItem.js 裡面的 TodoItem
就可以接收到 handleDeleteTodo
這個 function
接著,就可以在 TodoItem
component 的 <RedButton>
加上 onClick
事件:當點擊刪除按鈕時,執行 handleDeleteTodo
function,並傳入 todo.id
TodoItem.js:
export default function TodoItem({ className, size, todo, handleDeleteTodo }){
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button>已完成</Button>
<RedButton onClick={() => {
handleDeleteTodo(todo.id)
}
}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
setTodos()
用 .filter()
來刪除指定的 todo
執行 handleDeleteTodo
function 就會去呼叫在 App.js 的 handleDeleteTodo
function
我就可以在 App.js 裡面,用 handleDeleteTodo
function 接收到的 id
來把該 todo 刪掉
因此,在 handleDeleteTodo
function 裡面,用 setTodo()
來更新 todos
陣列:
不能用 .splice()
來刪除 todo,因為 .splice()
會改到原本的 todos
陣列
.filter()
來刪除 todo,因為 .filter()
會產生一個新的陣列,不會去改到原本的 todos
陣列App.js:
...
const handleDeleteTodo = id => {
setTodos(todos.filter(todo => todo.id !== id))
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} />)
}
</div>
);
}
因為在 JSX 語法中,沒有 if...else
,所以要寫判斷式的話,有兩個方式:
TodoItem.js:
用 {todo.isFinished ? '未完成' : '已完成'}
三元運算子來決定 todo
是要顯示已完成 or 未完成
export default function TodoItem({ className, size, todo, handleDeleteTodo }){
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button>
{todo.isFinished ? '未完成' : '已完成'}
</Button>
<RedButton onClick={() => {
handleDeleteTodo(todo.id)
}
}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
然後,同樣在 TodoItem.js 裡面,就可以在 TodoContent
的 styled component 用 props
加上 text-decoration: line-through
這段
用 &&
意思就是:如果 props.isFinished
是 true 的話,就會回傳後面那行 text-decoration: line-through;
const TodoContent = styled.div`
color: ${props => props.theme.colors.primary_500};
font-size: 16px;
${props => props.size === 'XL' && `
font-size: 20px;
`}
${props => props.isFinished && `
text-decoration: line-through;
`}
`
{todo.isFinished && '未完成'}
意思是:如果 todo.isFinished
是 true,就會回傳 '未完成'{!todo.isFinished && '已完成'}
意思是:如果 todo.isFinished
是 false,就會回傳 '已完成'export default function TodoItem({ className, size, todo, handleDeleteTodo, handleToggleIsFinished }){
const handleToggleClick = () => {
handleToggleIsFinished(todo.id)
}
const handleDeleteClick = () => {
handleDeleteTodo(todo.id)
}
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent $isFinished={todo.isFinished} size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button onClick={handleToggleClick}>
{todo.isFinished && '未完成'}
{!todo.isFinished && '已完成'}
</Button>
<RedButton onClick={handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
可參考文件 Transient props
props
,除了會傳給 styled component 之外,也會直接傳到 DOM 上面例如我在 <TodoContent>
加上一個 props
叫做 id="abc"
<TodoContent id="abc" isFinished={todo.isFinished} size={size}>{todo.content}</TodoContent>
render 出來的 DOM 就會有 id="abc"
這個屬性:
props
傳給 DOM,我只想要傳給 styled component 做 style 的處理就好,那就在 props
前面加一個 $
即可加上 $
就會是一個 transient prop
在 id="abc"
前面加一個 $
<TodoContent $id="abc" isFinished={todo.isFinished} size={size}>{todo.content}</TodoContent>
render 出來的 DOM 就不會有 id="abc"
這個屬性了:
在 styled component 用時,就要寫成 props.$id
,而不是 props.id
:
const TodoContent = styled.div`
color: ${props => props.theme.colors.primary_500};
font-size: 16px;
${props => props.size === 'XL' && `
font-size: 20px;
`}
${props => props.$isFinished && `
text-decoration: line-through;
`}
${props => props.$id && `
color: red
`}
`
宣告一個 handleToggleIsFinished
function 來處理「已完成/未完成」
setTodos()
用 .map()
修改指定的 todo
的屬性.map()
會產生出一個新的陣列,我就可以在這個新的陣列裡面修改那個 id
的 todo
(修改 isFinished
的狀態)
handleToggleIsFinished
當作 prop 傳到 <TodoItem>
App.js:
...
// 切換已完成/未完成
const handleToggleIsFinished = id => {
setTodos(todos.map(todo => {
if (todo.id !== id) return todo;
return {
...todo,
isFinished: !todo.isFinished
}
}))
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} handleToggleIsFinished={handleToggleIsFinished} />)
}
</div>
);
}
這樣,在 TodoItem.js 的 <TodoItem>
就可以接受到 handleToggleIsFinished
在 <Button>
就可以加上 onClick
的事件了
這裡把 onClick
裡面的 function 抽出來寫(handleToggleClick
),這樣的可讀性會比直接寫在 onClick
裡面好(直接寫在裡面叫做 inline function)
export default function TodoItem({ className, size, todo, handleDeleteTodo, handleToggleIsFinished }){
const handleToggleClick = () => {
handleToggleIsFinished(todo.id)
}
const handleDeleteClick = () => {
handleDeleteTodo(todo.id)
}
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent $isFinished={todo.isFinished} size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button onClick={handleToggleClick}>
{todo.isFinished ? '未完成' : '已完成'}
</Button>
<RedButton onClick={handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
在 setTodos()
小括號裡面,要產生一個新的陣列
setTodos()
用 ...todos
的解構語法來新增 todo
setTodos()
用 .filter()
來刪除指定的 todo
setTodos()
用 .map()
修改指定的 todo
的屬性從 todo list 學到的重點觀念有:
環境建置分兩種:
接下來,會用現成的 create-react-app (這是 react 官方提供的)來做環境建置
安裝完後,可以用 npx create-react-app -V
來查詢版本
<React.StrictMode>
會幫我檢查是否有用到不該用的東西,但是因為它需要偵測,會造成 console.log 出來的結果跟預期中的不一樣,所以建議是把 <React.StrictMode>
先移除
style={}
大括號裡面要傳入一個物件
const titleStyle = {
color: 'red',
textAlign: 'center'
}
function Title({ size }) {
if (size === 'XL') {
return <h1>Hello</h1>
}
return (
<h2 style={titleStyle}>Hello</h2>
)
}
比較常見的寫法是把物件直接寫在 style={}
裡面:
function Title({ size }) {
if (size === 'XL') {
return <h1>Hello</h1>
}
return (
<h2 style={{
color: 'red',
textAlign: 'center'
}}>Hello</h2>
)
}
className
和 css(這是 create-react-app 內建的方式)在 JSX 語法裡面,要寫 className
,不能寫 class
,因為 class
是 JS 的保留字
因此,用 JSX 寫:
<div className="App"></div>
render 到畫面上時,就會是:
<div class="App"></div>
接下來,因為有用 webpack 的關係,我可以直接在 App.js 裡面 import App.css
import './App.css';
在 App.css 裡面,我就可以寫 css 的內容了
styled components 是用 tagged template literal 建立出一套寫 css 的方法
參考文章 JavaScript ES6 中的模版字符串(template literals)和標籤模版(tagged template)
styled components 直接支援 scss, sass 的寫法
styled components 也會自動幫我加上各種 broswer 的 prefix,所以不用擔心兼容性的問題
首先,要先把套件安裝起來
安裝好後,就在 App.js 上方引入 styled components:
import styled from 'styled-components';
接著,就可以開始幫 component 加上 style 了
<Description>
這個 component 加上 styleDescription
就是一個「擁有這些 style 的一個 <p>
component」
const Description = styled.p`
color: salmon;
font-weight: bold;
border: 1px solid green;
`
function App() {
return (
<div className="App">
<Title size="M" />
<Description>
I love you!
</Description>
</div>
);
}
render 之後就會看到:
styled components 幫我做的事情就是:自動產生一個像亂碼的 className 放到元素上,並且把我寫的 css 套用上去
const TitleWrapper = styled.h2`
color: teal;
&:hover {
color: orange;
}
span {
color: purple;
}
`
function Title({ size }) {
return (
<TitleWrapper>Hello<span>yoyoyo</span></TitleWrapper>
)
}
const TodoItemWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
border: 1px solid black;
`
const TodoContent = styled.div`
color: salmon;
font-size: ${props => props.size === 'XL' ? '20px' : '12px'}
`
const TodoButtonWrapper = styled.div``
const Button = styled.button`
padding: 5px;
color: teal;
&:hover {
color: red;
}
& + & {
margin-left: 10px;
}
`
function App() {
return (
<div className="App">
<TodoItemWrapper>
<TodoContent size="XL">I am a todo</TodoContent>
<TodoButtonWrapper>
<Button>已完成</Button>
<Button>刪除</Button>
</TodoButtonWrapper>
</TodoItemWrapper>
</div>
);
}
props
參數的方式props
就像是自定義的「HTML 元素的屬性」
在 <TodoContent>
傳入 size="XL"
這個 prop
在 styled component 就可以用 ${props => props.size ...}
來拿到 size="XL"
這個 prop
const TodoContent = styled.div`
color: salmon;
font-size: ${props => props.size === 'XL' ? '20px' : '12px'}
`
function App() {
return (
<div className="App">
<TodoItemWrapper>
<TodoContent size="XL">I am a todo</TodoContent>
<TodoButtonWrapper>
<Button>已完成</Button>
<Button>刪除</Button>
</TodoButtonWrapper>
</TodoItemWrapper>
</div>
);
}
<TodoContent>
的 font-size
預設是 12px
如果 <TodoContent>
的 size="XL"
的話,就會用 font-size: 20px
覆蓋掉原本的 font-size: 12px
const TodoContent = styled.div`
color: salmon;
font-size: 12px;
${props => props.size === 'XL' && `
font-size: 20px;
`}
`
function App() {
return (
<div className="App">
<TodoItemWrapper>
<TodoContent size="XL">I am a todo</TodoContent>
<TodoButtonWrapper>
<Button>已完成</Button>
<Button>刪除</Button>
</TodoButtonWrapper>
</TodoItemWrapper>
</div>
);
}
在 <Nav>
加上 $active
這個屬性
在 styled component 就可以做出:如果 $active
是 true,才會有 background-color
const Nav = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100px;
cursor: pointer;
// 如果 $active 是 true,才會有 background-color
${(props) =>
props.$active &&
`
background-color: rgba(202, 68, 111, 0.1);
`}
`;
export default function Header() {
return (
<HeaderContainer>
<LeftContainer>
<Brand>我的第一個部落格</Brand>
<NavbarList>
<Nav $active>首頁</Nav> {/* 加上 $active 這個屬性 */}
<Nav>發布文章</Nav>
</NavbarList>
</LeftContainer>
<NavbarList>
<Nav>登入</Nav>
</NavbarList>
</HeaderContainer>
);
}
const TodoItemWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
border: 1px solid black;
`
const TodoContent = styled.div`
color: salmon;
font-size: 12px;
${props => props.size === 'XL' && `
font-size: 20px;
`}
`
const TodoButtonWrapper = styled.div``
const Button = styled.button`
padding: 5px;
color: teal;
&:hover {
color: red;
}
& + & {
margin-left: 10px;
}
`
function TodoItem({ size, content }){
return (
<TodoItemWrapper>
<TodoContent size={size}>{content}</TodoContent>
<TodoButtonWrapper>
<Button>已完成</Button>
<Button>刪除</Button>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
function App() {
return (
<div className="App">
<TodoItem content={123} />
<TodoItem content={456} size='XL' />
</div>
);
}
把 Button
做 restyle 變成 BlueButton
const Button = styled.button`
padding: 5px;
color: teal;
&:hover {
color: red;
}
& + & {
margin-left: 10px;
}
`
const BlueButton = styled(Button)`
color: blue;
`
function TodoItem({ size, content }){
return (
<TodoItemWrapper>
<TodoContent size={size}>{content}</TodoContent>
<TodoButtonWrapper>
<BlueButton>已完成</BlueButton>
<Button>刪除</Button>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
className
參數因為 style components 是透過「傳 className」,所以要給 react component 一個 className
的參數,並把參數放在我想要 restyle 的元素上
function TodoItem({ className, size, content }){
return (
<TodoItemWrapper className={className}>
<TodoContent size={size}>{content}</TodoContent>
<TodoButtonWrapper>
<BlueButton>已完成</BlueButton>
<Button>刪除</Button>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
const YellowTodoItem = styled(TodoItem)`
background-color: yellow;
`
function App() {
return (
<div className="App">
<TodoItem content={123} />
<YellowTodoItem content={456} size='XL' />
</div>
);
}
style components 會自動幫 background-color: yellow
產生一個新的 className,並且把這個 className 加在 <TodoItemWrapper>
上面
就跟一般在寫 css 時的 media query 用法是一樣的:
const Button = styled.button`
padding: 5px;
color: teal;
font-size: 20px;
@media (min-width: 768px) {
font-size: 16px;
}
&:hover {
color: red;
}
& + & {
margin-left: 10px;
}
`
在實務上,都會建立一個 constants 資料夾,在裡面新建一個檔案叫做 style.js
然後,就可以把 media query 的斷點都寫在 style.js 裡面做 export:
export const MEDIA_QUERY_MD = `@media (min-width: 768px)`;
export const MEDIA_QUERY_LG = `@media (min-width: 1000px)`;
在 App.js 要用時引入即可,這樣程式碼會比較乾淨
import { MEDIA_QUERY_MD, MEDIA_QUERY_LG} from './constants/style';
const Button = styled.button`
padding: 5px;
color: teal;
font-size: 20px;
${MEDIA_QUERY_MD} {
font-size: 16px;
}
${MEDIA_QUERY_LG} {
font-size: 12px;
}
&:hover {
color: red;
}
& + & {
margin-left: 10px;
}
`
使用變數的做法是:傳一個 global 的參數,這樣在其他檔案就可以直接使用
ThemeProvider
<ThemeProvider></ThemeProvider>
是一個 component
然後,用 <ThemeProvider></ThemeProvider>
把我要 render 的 <App />
包住
在 <ThemeProvider></ThemeProvider>
要接收一個 theme
參數,在 theme
裡面就可以自己定義我想要的變數,例如 colors
import { ThemeProvider } from 'styled-components';
const theme = {
colors: {
primary_300: '#ef9898',
primary_400: '#ec6b6b',
primary_500: '#ea1212',
}
}
ReactDOM.render(
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
,
document.getElementById('root')
);
接著,在 App.js 我就可以用 props.theme.colors.primary_500
來拿到 primary_500: '#ea1212'
這個顏色
const TodoContent = styled.div`
color: ${props => props.theme.colors.primary_500};
font-size: 12px;
${props => props.size === 'XL' && `
font-size: 20px;
`}
`
當 component 越來越多時,就可以把不同的 component 獨立成不同的檔案
例如
TodoItem
這個 component,然後把它 exportexport default
來 export componentimport styled from 'styled-components';
import { MEDIA_QUERY_MD, MEDIA_QUERY_LG} from './constants/style';
const TodoItemWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
border: 1px solid black;
margin-bottom: 10px;
`
const TodoContent = styled.div`
color: ${props => props.theme.colors.primary_500};
font-size: 12px;
${props => props.size === 'XL' && `
font-size: 20px;
`}
`
const TodoButtonWrapper = styled.div``
const Button = styled.button`
padding: 5px;
color: teal;
font-size: 20px;
${MEDIA_QUERY_MD} {
font-size: 16px;
}
${MEDIA_QUERY_LG} {
font-size: 12px;
}
&:hover {
color: red;
}
& + & {
margin-left: 10px;
}
`
const BlueButton = styled(Button)`
color: blue;
`
export default function TodoItem({ className, size, content }){
return (
<TodoItemWrapper className={className}>
<TodoContent size={size}>{content}</TodoContent>
<TodoButtonWrapper>
<BlueButton>已完成</BlueButton>
<Button>刪除</Button>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
TodoItem
拿來用import styled from 'styled-components';
import TodoItem from './TodoItem';
const YellowTodoItem = styled(TodoItem)`
background-color: yellow;
`
function App() {
return (
<div className="App">
<TodoItem content={123} />
<YellowTodoItem content={456} size='XL' />
</div>
);
}
export default App;
當我們在寫 JSX 的語法時,背後是透過 Babel 幫我們轉成 JavaScript 的程式碼
JSX:
<Button size="XL">hello</Button>
轉成 JavaScript 的程式碼:
React.createElement(Button, {
size: "XL"
}, "hello");
在 React 裡面,如果要用 state 的話,要在 App.js 寫這行:
import React from 'react';
從 React 16.8 之後,出現了 hooks 這個新的用法
import styled from 'styled-components';
import TodoItem from './TodoItem';
import React from 'react';
const YellowTodoItem = styled(TodoItem)`
background-color: yellow;
`
function App() {
const [counter, setCounter] = React.useState(0);
const handleButtonClick = () => {
setCounter(counter + 1)
}
return (
<div className="App">
counter: {counter}
<button onClick={handleButtonClick}>increment</button>
<TodoItem content={123} />
<YellowTodoItem content={456} size='XL' />
</div>
);
}
export default App;
React.useState()
是一個 hook,透過這個 hook 讓我們可以在 function component 裡面使用一個 state
React.useState()
會回傳一個陣列:
React.useState()
小括號裡面setCounter
是一個 function,呼叫 setCounter
這個 function 來更新 stateconst [counter, setCounter] = React.useState(0)
是「解構」的寫法
可以看成是這樣:
用「解構」拿出來的 a 就會是 123,b 就會是 456
function useState() {
return [123, 456]
}
const [a, b] = useState()
output:
點擊 button,counter 就會 +1
React 的這個流程是:
打開網頁後會做第一次的 render,render 就是指:React 會去執行 App()
這個 function,把 App()
return 的東西放到畫面上(這個動作叫做 mount,也就是把東西放到 DOM 上面,讓瀏覽器 render 出來)
每當我更新 state,React 就會 re-render,也就是「會再呼叫一次 App()
這個 function」
環境建置分兩種:
接下來,會用現成的 create-react-app (這是 react 官方提供的)來做環境建置
安裝完後,可以用 npx create-react-app -V
來查詢版本
<React.StrictMode>
會幫我檢查是否有用到不該用的東西,但是因為它需要偵測,會造成 console.log 出來的結果跟預期中的不一樣,所以建議是把 <React.StrictMode>
先移除
style={}
大括號裡面要傳入一個物件
const titleStyle = {
color: 'red',
textAlign: 'center'
}
function Title({ size }) {
if (size === 'XL') {
return <h1>Hello</h1>
}
return (
<h2 style={titleStyle}>Hello</h2>
)
}
比較常見的寫法是把物件直接寫在 style={}
裡面:
function Title({ size }) {
if (size === 'XL') {
return <h1>Hello</h1>
}
return (
<h2 style={{
color: 'red',
textAlign: 'center'
}}>Hello</h2>
)
}
className
和 css(這是 create-react-app 內建的方式)在 JSX 語法裡面,要寫 className
,不能寫 class
,因為 class
是 JS 的保留字
因此,用 JSX 寫:
<div className="App"></div>
render 到畫面上時,就會是:
<div class="App"></div>
接下來,因為有用 webpack 的關係,我可以直接在 App.js 裡面 import App.css
import './App.css';
在 App.css 裡面,我就可以寫 css 的內容了
styled components 是用 tagged template literal 建立出一套寫 css 的方法
參考文章 JavaScript ES6 中的模版字符串(template literals)和標籤模版(tagged template)
styled components 直接支援 scss, sass 的寫法
styled components 也會自動幫我加上各種 broswer 的 prefix,所以不用擔心兼容性的問題
首先,要先把套件安裝起來
安裝好後,就在 App.js 上方引入 styled components:
import styled from 'styled-components';
接著,就可以開始幫 component 加上 style 了
<Description>
這個 component 加上 styleDescription
就是一個「擁有這些 style 的一個 <p>
component」
const Description = styled.p`
color: salmon;
font-weight: bold;
border: 1px solid green;
`
function App() {
return (
<div className="App">
<Title size="M" />
<Description>
I love you!
</Description>
</div>
);
}
render 之後就會看到:
styled components 幫我做的事情就是:自動產生一個像亂碼的 className 放到元素上,並且把我寫的 css 套用上去
const TitleWrapper = styled.h2`
color: teal;
&:hover {
color: orange;
}
span {
color: purple;
}
`
function Title({ size }) {
return (
<TitleWrapper>Hello<span>yoyoyo</span></TitleWrapper>
)
}
const TodoItemWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
border: 1px solid black;
`
const TodoContent = styled.div`
color: salmon;
font-size: ${props => props.size === 'XL' ? '20px' : '12px'}
`
const TodoButtonWrapper = styled.div``
const Button = styled.button`
padding: 5px;
color: teal;
&:hover {
color: red;
}
& + & {
margin-left: 10px;
}
`
function App() {
return (
<div className="App">
<TodoItemWrapper>
<TodoContent size="XL">I am a todo</TodoContent>
<TodoButtonWrapper>
<Button>已完成</Button>
<Button>刪除</Button>
</TodoButtonWrapper>
</TodoItemWrapper>
</div>
);
}
props
參數的方式props
就像是自定義的「HTML 元素的屬性」
在 <TodoContent>
傳入 size="XL"
這個 prop
在 styled component 就可以用 ${props => props.size ...}
來拿到 size="XL"
這個 prop
const TodoContent = styled.div`
color: salmon;
font-size: ${props => props.size === 'XL' ? '20px' : '12px'}
`
function App() {
return (
<div className="App">
<TodoItemWrapper>
<TodoContent size="XL">I am a todo</TodoContent>
<TodoButtonWrapper>
<Button>已完成</Button>
<Button>刪除</Button>
</TodoButtonWrapper>
</TodoItemWrapper>
</div>
);
}
<TodoContent>
的 font-size
預設是 12px
如果 <TodoContent>
的 size="XL"
的話,就會用 font-size: 20px
覆蓋掉原本的 font-size: 12px
const TodoContent = styled.div`
color: salmon;
font-size: 12px;
${props => props.size === 'XL' && `
font-size: 20px;
`}
`
function App() {
return (
<div className="App">
<TodoItemWrapper>
<TodoContent size="XL">I am a todo</TodoContent>
<TodoButtonWrapper>
<Button>已完成</Button>
<Button>刪除</Button>
</TodoButtonWrapper>
</TodoItemWrapper>
</div>
);
}
在 <Nav>
加上 $active
這個屬性
在 styled component 就可以做出:如果 $active
是 true,才會有 background-color
const Nav = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100px;
cursor: pointer;
// 如果 $active 是 true,才會有 background-color
${(props) =>
props.$active &&
`
background-color: rgba(202, 68, 111, 0.1);
`}
`;
export default function Header() {
return (
<HeaderContainer>
<LeftContainer>
<Brand>我的第一個部落格</Brand>
<NavbarList>
<Nav $active>首頁</Nav> {/* 加上 $active 這個屬性 */}
<Nav>發布文章</Nav>
</NavbarList>
</LeftContainer>
<NavbarList>
<Nav>登入</Nav>
</NavbarList>
</HeaderContainer>
);
}
const TodoItemWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
border: 1px solid black;
`
const TodoContent = styled.div`
color: salmon;
font-size: 12px;
${props => props.size === 'XL' && `
font-size: 20px;
`}
`
const TodoButtonWrapper = styled.div``
const Button = styled.button`
padding: 5px;
color: teal;
&:hover {
color: red;
}
& + & {
margin-left: 10px;
}
`
function TodoItem({ size, content }){
return (
<TodoItemWrapper>
<TodoContent size={size}>{content}</TodoContent>
<TodoButtonWrapper>
<Button>已完成</Button>
<Button>刪除</Button>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
function App() {
return (
<div className="App">
<TodoItem content={123} />
<TodoItem content={456} size='XL' />
</div>
);
}
把 Button
做 restyle 變成 BlueButton
const Button = styled.button`
padding: 5px;
color: teal;
&:hover {
color: red;
}
& + & {
margin-left: 10px;
}
`
const BlueButton = styled(Button)`
color: blue;
`
function TodoItem({ size, content }){
return (
<TodoItemWrapper>
<TodoContent size={size}>{content}</TodoContent>
<TodoButtonWrapper>
<BlueButton>已完成</BlueButton>
<Button>刪除</Button>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
className
參數因為 style components 是透過「傳 className」,所以要給 react component 一個 className
的參數,並把參數放在我想要 restyle 的元素上
function TodoItem({ className, size, content }){
return (
<TodoItemWrapper className={className}>
<TodoContent size={size}>{content}</TodoContent>
<TodoButtonWrapper>
<BlueButton>已完成</BlueButton>
<Button>刪除</Button>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
const YellowTodoItem = styled(TodoItem)`
background-color: yellow;
`
function App() {
return (
<div className="App">
<TodoItem content={123} />
<YellowTodoItem content={456} size='XL' />
</div>
);
}
style components 會自動幫 background-color: yellow
產生一個新的 className,並且把這個 className 加在 <TodoItemWrapper>
上面
就跟一般在寫 css 時的 media query 用法是一樣的:
const Button = styled.button`
padding: 5px;
color: teal;
font-size: 20px;
@media (min-width: 768px) {
font-size: 16px;
}
&:hover {
color: red;
}
& + & {
margin-left: 10px;
}
`
在實務上,都會建立一個 constants 資料夾,在裡面新建一個檔案叫做 style.js
然後,就可以把 media query 的斷點都寫在 style.js 裡面做 export:
export const MEDIA_QUERY_MD = `@media (min-width: 768px)`;
export const MEDIA_QUERY_LG = `@media (min-width: 1000px)`;
在 App.js 要用時引入即可,這樣程式碼會比較乾淨
import { MEDIA_QUERY_MD, MEDIA_QUERY_LG} from './constants/style';
const Button = styled.button`
padding: 5px;
color: teal;
font-size: 20px;
${MEDIA_QUERY_MD} {
font-size: 16px;
}
${MEDIA_QUERY_LG} {
font-size: 12px;
}
&:hover {
color: red;
}
& + & {
margin-left: 10px;
}
`
使用變數的做法是:傳一個 global 的參數,這樣在其他檔案就可以直接使用
ThemeProvider
<ThemeProvider></ThemeProvider>
是一個 component
然後,用 <ThemeProvider></ThemeProvider>
把我要 render 的 <App />
包住
在 <ThemeProvider></ThemeProvider>
要接收一個 theme
參數,在 theme
裡面就可以自己定義我想要的變數,例如 colors
import { ThemeProvider } from 'styled-components';
const theme = {
colors: {
primary_300: '#ef9898',
primary_400: '#ec6b6b',
primary_500: '#ea1212',
}
}
ReactDOM.render(
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
,
document.getElementById('root')
);
接著,在 App.js 我就可以用 props.theme.colors.primary_500
來拿到 primary_500: '#ea1212'
這個顏色
const TodoContent = styled.div`
color: ${props => props.theme.colors.primary_500};
font-size: 12px;
${props => props.size === 'XL' && `
font-size: 20px;
`}
`
當 component 越來越多時,就可以把不同的 component 獨立成不同的檔案
例如
TodoItem
這個 component,然後把它 exportexport default
來 export componentimport styled from 'styled-components';
import { MEDIA_QUERY_MD, MEDIA_QUERY_LG} from './constants/style';
const TodoItemWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
border: 1px solid black;
margin-bottom: 10px;
`
const TodoContent = styled.div`
color: ${props => props.theme.colors.primary_500};
font-size: 12px;
${props => props.size === 'XL' && `
font-size: 20px;
`}
`
const TodoButtonWrapper = styled.div``
const Button = styled.button`
padding: 5px;
color: teal;
font-size: 20px;
${MEDIA_QUERY_MD} {
font-size: 16px;
}
${MEDIA_QUERY_LG} {
font-size: 12px;
}
&:hover {
color: red;
}
& + & {
margin-left: 10px;
}
`
const BlueButton = styled(Button)`
color: blue;
`
export default function TodoItem({ className, size, content }){
return (
<TodoItemWrapper className={className}>
<TodoContent size={size}>{content}</TodoContent>
<TodoButtonWrapper>
<BlueButton>已完成</BlueButton>
<Button>刪除</Button>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
TodoItem
拿來用import styled from 'styled-components';
import TodoItem from './TodoItem';
const YellowTodoItem = styled(TodoItem)`
background-color: yellow;
`
function App() {
return (
<div className="App">
<TodoItem content={123} />
<YellowTodoItem content={456} size='XL' />
</div>
);
}
export default App;
當我們在寫 JSX 的語法時,背後是透過 Babel 幫我們轉成 JavaScript 的程式碼
JSX:
<Button size="XL">hello</Button>
轉成 JavaScript 的程式碼:
React.createElement(Button, {
size: "XL"
}, "hello");
在 React 裡面,如果要用 state 的話,要在 App.js 寫這行:
import React from 'react';
從 React 16.8 之後,出現了 hooks 這個新的用法
import styled from 'styled-components';
import TodoItem from './TodoItem';
import React from 'react';
const YellowTodoItem = styled(TodoItem)`
background-color: yellow;
`
function App() {
const [counter, setCounter] = React.useState(0);
const handleButtonClick = () => {
setCounter(counter + 1)
}
return (
<div className="App">
counter: {counter}
<button onClick={handleButtonClick}>increment</button>
<TodoItem content={123} />
<YellowTodoItem content={456} size='XL' />
</div>
);
}
export default App;
React.useState()
是一個 hook,透過這個 hook 讓我們可以在 function component 裡面使用一個 state
React.useState()
會回傳一個陣列:
React.useState()
小括號裡面setCounter
是一個 function,呼叫 setCounter
這個 function 來更新 stateconst [counter, setCounter] = React.useState(0)
是「解構」的寫法
可以看成是這樣:
用「解構」拿出來的 a 就會是 123,b 就會是 456
function useState() {
return [123, 456]
}
const [a, b] = useState()
output:
點擊 button,counter 就會 +1
React 的這個流程是:
打開網頁後會做第一次的 render,render 就是指:React 會去執行 App()
這個 function,把 App()
return 的東西放到畫面上(這個動作叫做 mount,也就是把東西放到 DOM 上面,讓瀏覽器 render 出來)
每當我更新 state,React 就會 re-render,也就是「會再呼叫一次 App()
這個 function」
實作「新增、刪除、標示已完成/未完成」的功能
Component 就是「元件、組件」
component 的 function 名稱用大寫開頭,較好辨別它是一個 component
如果在操作 todo 時,「同時更新」資料和畫面,當其中一個忘記更新時,就會發生「資料、畫面不一致」的問題
要解決此問題,有兩種方式:
操作 todo 時,只更新畫面
當需要資料的時候,再把資料從畫面上拿出來
這樣,資料和畫面永遠都會是「同步」的
操作 todo 時,只更新資料
「畫面」都是由資料產生
這樣,資料和畫面永遠都會是「同步」的
state 就是「資料」 (程式的狀態 = 程式的資料)
用比較數學的寫法就是:
UI = f(state)
state
id
不算是 state
,因為別人只需要知道 todo 的 data-id
是多少就好了,不需要知道我自己這邊的 id
是多少
let state = {
todos: []
}
render
函式就是負責「把 state 轉成 UI」宣告一個 render
函式,在 render
函式裡面:
.todos
清空.join('')
把每個 todo 的 html 給結合起來 // 把 state 轉成 UI
function render() {
$('.todos').empty(); // 先把 .todos 清空
$('.todos').append(
state.todos.map(todo => {return Todo(todo)}).join('')
)
}
因為每當我改變 state 時,就要呼叫 render
函式,所以就可以這樣寫:
function updateState(newState) {
state = newState;
render();
}
{}
裡面可以寫 JavaScript 的程式碼,例如下面的 {props.name}
import React from "react";
import ReactDOM from "react-dom";
function Love(props) {
return <h1>I love you, {props.name}</h1>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Love name="Harry"/>, rootElement);
props
改為解構的寫法:import React from "react";
import ReactDOM from "react-dom";
function Love({ name }) {
return <h1>I love you, {name}</h1>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Love name="Harry"/>, rootElement);
n
可以寫成 n={5}
或是 n="5"
不可以寫成 n=5
import React from "react";
import ReactDOM from "react-dom";
function Love({ name, n }) {
return <h1> {n} I love you, {name}</h1>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Love name="Harry" n={5}/>, rootElement);
<Love></Love>
標籤裡面的內容,會被當作是 children
這個參數傳進去 function component 裡面children
這個參數是 react 幫我做好的
import React from "react";
import ReactDOM from "react-dom";
function Love({ children }) {
return <h1>I love you {children}</h1>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<Love>
for a thousand year
</Love>, rootElement);
React.useState()
是一個 function,會回傳給我一個陣列,陣列裡面:
value
setValue
React.useState(10)
小括號裡面填的是「初始值」
Counter()
是一個 component
value
,初始值是 10setValue
即可import React from "react";
import ReactDOM from "react-dom";
function Counter() {
const [value, setValue] = React.useState(10);
return <h1>{value}</h1>
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<Counter />, rootElement);
onClick={handleClick}
在大括號裡面要寫的是 JS 的程式碼
只要點擊 h1
,Counter
的 state 就會 +1
import React from "react";
import ReactDOM from "react-dom";
function Counter() {
const [value, setValue] = React.useState(10);
function handleClick() {
setValue(value + 1)
}
return <h1 onClick={handleClick}>{value}</h1>
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<Counter />, rootElement);
return
後面如果要換行,就要用小括號包起來return
後面如果直接接上東西,就不需要小括號,例如:
return <h1 onClick={handleClick}>{value}</h1>
但 return
後面如果要換行,
錯誤寫法:return
後面沒有接東西,就會是預設的 return undefined
function Title({ size }) {
if (size === 'XL') {
return <h1>Hello</h1>
}
return
<h2 style={{
color: 'red',
textAlign: 'center'
}}>Hello</h2>
}
正確寫法:要 return
的東西要用小括號包起來,才能換行
function Title({ size }) {
if (size === 'XL') {
return <h1>Hello</h1>
}
return (
<h2 style={{
color: 'red',
textAlign: 'center'
}}>Hello</h2>
)
}
把 Counter
這個 component 處理完,把 Counter
return 的東西,放到畫面上
當 state 更新時,react 會重新呼叫一次 Counter
這個 component,並重新 render 一次
實作「新增、刪除、標示已完成/未完成」的功能
Component 就是「元件、組件」
component 的 function 名稱用大寫開頭,較好辨別它是一個 component
如果在操作 todo 時,「同時更新」資料和畫面,當其中一個忘記更新時,就會發生「資料、畫面不一致」的問題
要解決此問題,有兩種方式:
操作 todo 時,只更新畫面
當需要資料的時候,再把資料從畫面上拿出來
這樣,資料和畫面永遠都會是「同步」的
操作 todo 時,只更新資料
「畫面」都是由資料產生
這樣,資料和畫面永遠都會是「同步」的
state 就是「資料」 (程式的狀態 = 程式的資料)
用比較數學的寫法就是:
UI = f(state)
state
id
不算是 state
,因為別人只需要知道 todo 的 data-id
是多少就好了,不需要知道我自己這邊的 id
是多少
let state = {
todos: []
}
render
函式就是負責「把 state 轉成 UI」宣告一個 render
函式,在 render
函式裡面:
.todos
清空.join('')
把每個 todo 的 html 給結合起來 // 把 state 轉成 UI
function render() {
$('.todos').empty(); // 先把 .todos 清空
$('.todos').append(
state.todos.map(todo => {return Todo(todo)}).join('')
)
}
因為每當我改變 state 時,就要呼叫 render
函式,所以就可以這樣寫:
function updateState(newState) {
state = newState;
render();
}
{}
裡面可以寫 JavaScript 的程式碼,例如下面的 {props.name}
import React from "react";
import ReactDOM from "react-dom";
function Love(props) {
return <h1>I love you, {props.name}</h1>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Love name="Harry"/>, rootElement);
props
改為解構的寫法:import React from "react";
import ReactDOM from "react-dom";
function Love({ name }) {
return <h1>I love you, {name}</h1>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Love name="Harry"/>, rootElement);
n
可以寫成 n={5}
或是 n="5"
不可以寫成 n=5
import React from "react";
import ReactDOM from "react-dom";
function Love({ name, n }) {
return <h1> {n} I love you, {name}</h1>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Love name="Harry" n={5}/>, rootElement);
<Love></Love>
標籤裡面的內容,會被當作是 children
這個參數傳進去 function component 裡面children
這個參數是 react 幫我做好的
import React from "react";
import ReactDOM from "react-dom";
function Love({ children }) {
return <h1>I love you {children}</h1>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<Love>
for a thousand year
</Love>, rootElement);
React.useState()
是一個 function,會回傳給我一個陣列,陣列裡面:
value
setValue
React.useState(10)
小括號裡面填的是「初始值」
Counter()
是一個 component
value
,初始值是 10setValue
即可import React from "react";
import ReactDOM from "react-dom";
function Counter() {
const [value, setValue] = React.useState(10);
return <h1>{value}</h1>
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<Counter />, rootElement);
onClick={handleClick}
在大括號裡面要寫的是 JS 的程式碼
只要點擊 h1
,Counter
的 state 就會 +1
import React from "react";
import ReactDOM from "react-dom";
function Counter() {
const [value, setValue] = React.useState(10);
function handleClick() {
setValue(value + 1)
}
return <h1 onClick={handleClick}>{value}</h1>
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<Counter />, rootElement);
return
後面如果要換行,就要用小括號包起來return
後面如果直接接上東西,就不需要小括號,例如:
return <h1 onClick={handleClick}>{value}</h1>
但 return
後面如果要換行,
錯誤寫法:return
後面沒有接東西,就會是預設的 return undefined
function Title({ size }) {
if (size === 'XL') {
return <h1>Hello</h1>
}
return
<h2 style={{
color: 'red',
textAlign: 'center'
}}>Hello</h2>
}
正確寫法:要 return
的東西要用小括號包起來,才能換行
function Title({ size }) {
if (size === 'XL') {
return <h1>Hello</h1>
}
return (
<h2 style={{
color: 'red',
textAlign: 'center'
}}>Hello</h2>
)
}
把 Counter
這個 component 處理完,把 Counter
return 的東西,放到畫面上
當 state 更新時,react 會重新呼叫一次 Counter
這個 component,並重新 render 一次
this
原本只是用來在物件導向裡面使用的
在物件導向裡面,this
是用來存取「目前對應到的這個 instance」,不然也沒有其他方法可以去存取目前對應到的 instance 了
this
對應到的就是「目前在呼叫 function 的這個 instance」
如果不是在物件導向的環境(也沒有在嚴格模式下),this
的預設值會是一個「global 的東西」,global 會取決於環境而有不同的值,但都會是一個全域的東西
例如:
this
的預設值是一個叫做 global
的變數function test() {
console.log(this === global); // 在 Node.js 裡面,this 的預設值是一個叫做 global 的變數
}
test(); // true
this
的預設值是 window
use strict
開啟嚴格模式但其實,會有上面這些預設值還滿奇怪的,因為 this
並沒有指向到任何東西,怎麼還會給它這些 global 的值呢?
這時,可以用 "use strict"
來開啟嚴格模式
this
預設值都會是 undefined,因為 this
本來就不需要任何預設值"use strict"; // 開啟嚴格模式
function test() {
console.log(this);
}
test(); // undefined
this
放在 function 裡面,this
預設值也是 undefined"use strict"; // 開啟嚴格模式
function test() {
var a = 1;
function inner() {
console.log(this);
}
inner();
}
test(); // undefined
如果是「用 DOM + 瀏覽器的事件」,this
的預設值就是「你去實際做操作的那個東西」
例如以下的 EventListener:
我 click 到哪一個按鈕,this
就會指向到那個按鈕
//用 DOM + 瀏覽器的事件
document.querySelector("btn").addEventListener("click", function () {
console.log(this); // 我 click 到哪一個按鈕,this 就會指向到那個按鈕
});
this
都是沒有任何意義的(undefined)call()
來呼叫 function在 call()
裡面傳入的第一個參數,就會是 function 裡面的 this
"use strict"; // 開啟嚴格模式
function test() {
console.log(this); // 123
}
test.call(123);
apply()
來呼叫 function在 apply()
裡面傳入的第一個參數,就會是 function 裡面的 this
"use strict"; // 開啟嚴格模式
function test() {
console.log(this); // [ 1 ]
}
test.apply([1]);
call()
可以傳多個參數,都是用逗號隔開apply()
的參數只能有兩個,第二個參數會是一個 array,用 array 把我要傳的參數包起來call()
跟 apply()
的第一個參數,都可以去改變 function 裡面 this
的值。傳入什麼值,this
就會是什麼值
對於 call 來說,可以傳多個參數,都是用逗號隔開
"hello"
就會是 this
的值"use strict"; // 開啟嚴格模式
function test(a, b, c) {
console.log(this); // [String: 'hello']
console.log(a, b, c); // 1 2 3
}
test.call("hello", 1, 2, 3);
對於 apply 來說,參數只能有兩個
this
的值"use strict"; // 開啟嚴格模式
function test(a, b, c) {
console.log(this); // hello
console.log(a, b, c); // 1 2 3
}
test.apply("hello", [1, 2, 3]); // 第二個參數會是一個 array,用 array 把我要傳的參數包起來
this
的值跟「寫在程式碼的哪裡」無關,只跟「function 是怎麼被呼叫的」有關this
的值會是「obj
本身」"use strict"; // 開啟嚴格模式
const obj = {
a: 123,
test: function () {
console.log(this); // { a: 123, test: [Function: test] }
},
};
obj.test();
this
的值都會是 inner
這個物件「直接呼叫 function」跟「用 call 呼叫」,this
的值都會是 inner
這個物件
"use strict"; // 開啟嚴格模式
const obj = {
a: 123,
inner: {
test: function () {
console.log(this); //
},
},
};
obj.inner.test(); // { test: [Function: test] }
obj.inner.test.call(obj.inner); // { test: [Function: test] }
this
的值會是 undefined執行 func()
會印出 this
的值是 undefined
"use strict"; // 開啟嚴格模式
const obj = {
a: 123,
inner: {
test: function () {
console.log(this); // undefined
},
},
};
const func = obj.inner.test;
func();
原因為:
func()
可以看成是 func.call(undefined)
因為在 func.call()
前面沒有其他東西了,所以第一個參數就只能是 undefined
"use strict"; // 開啟嚴格模式
const obj = {
a: 123,
inner: {
test: function () {
console.log(this); // undefined
},
},
};
const func = obj.inner.test;
func() 可以看成是 func.call(undefined);
call()
的形式來呼叫,就可以很清楚知道 this
的值會是什麼了用 call()
這個形式來呼叫 function,就可以很簡便的得知 this
的值
例如:
func()
因為在 func
前面沒有任何東西,所以 this
的預設值就是 undefinedobj.inner.test()
因為在 test()
前面是 obj.inner
,所以 this
的預設值就是 obj.inner
func() 可以看成是 func.call(undefined); // this 的預設值就是 undefined
obj.inner.test() 可以看成是 obj.inner.test.call(obj.inner) // this 的預設值就是 obj.inner
"use strict"; // 開啟嚴格模式
const obj = {
a: 123,
inner: {
test: function () {
console.log(this); // undefined
},
},
};
const func = obj.inner.test;
func() 可以看成是 func.call(undefined);
obj.inner.test() 可以看成是 obj.inner.test.call(obj.inner)
this
的部分就沒問題了!this
,會依據「是否為嚴格模式」以及「執行環境不同」而有不同的預設值(有可能是 window
或是 undefined
)this
,this
就會是「自己這個 instance」this
,可以把 function call 轉換成 call()
的形式,call()
的第一個參數是什麼,this
的預設值就會是什麼this
的練習題:下面各會印出什麼值呢?
"use strict";
function log() {
console.log(this);
}
var a = { a: 1, log: log };
var b = { a: 2, log: log };
log(); // undefined
a.log(); // { a: 1, log: [Function: log] }
b.log.apply(a); // { a: 1, log: [Function: log] }
log()
就是在一個沒有意義的地方呼叫 this
,在嚴格模式下,this
的值就會是 undefineda.log()
,this
的值就會是 a
b.log.apply(a)
,在 apply()
裡面傳入的第一個參數就會是 this
的值,所以就是 a
bind
強制綁定 this
的值下面範例中,用不同的方式去呼叫 function,this
就會有不同的值:
obj.test()
,this
的值是 obj
func()
,this
的值是 undefined"use strict";
const obj = {
a: 1,
test: function () {
console.log(this);
},
};
obj.test(); // this 的值是 obj
const func = obj.test;
func(); // this 的值是 undefined
但我想要做的是:不管用什麼方式呼叫 function,this
的值都是一樣的
要做到這件事情,就要用 bind
把 this
的值給強制綁定住,範例如下:
bind()
小括號裡面的東西當作 this
放到 obj.test
綁定完成之後,我就不用擔心因為呼叫的方式不同而有不同的 this
值了
就算是用 bindTest.call(123)
來呼叫 function,this
的值依然會是 hello
bind
跟「call
, apply
」不同的地方在於:bind
幫我把 this
綁定完之後,會回傳一個 function,所以 bindTest
會是一個 functioncall
, apply
」是幫我指定完 this
的值之後,會直接呼叫 function"use strict";
const obj = {
a: 1,
test: function () {
console.log(this);
},
};
const bindTest = obj.test.bind("hello");
bindTest(); // this 的值會是 hello
bindTest.call(123); // 就算用 call 來呼叫,this 的值依然會是 hello
先看這個沒有箭頭函式的範例:
class Test {
run() {
console.log("run this: ", this); // this 的值會是 t 這個 instance
setTimeout(function () {
console.log(this); // this 的值會是 window
}, 100);
}
}
const t = new Test();
t.run();
把上面的程式碼貼到瀏覽器的 console 去執行:
this
就會是 t
這個 instancethis
就會是 window為什麼第 5 行的 this
會是 window 呢?
因為過了 100 毫秒之後執行 setTimeout
裡面的 function,就是在一個沒有意義的地方呼叫 this
,所以在寬鬆模式下,瀏覽器的 this
預設值就會是 window(如果是在嚴格模式下,瀏覽器的 this
預設值就會是 undefined)
this
現在,我把 setTimeout
裡面的 function 改成箭頭函式,結果印出來的 this
跟上一層的 this
是一樣的(都是 t
這個 instance)
class Test {
run() {
console.log("run this: ", this); // this 的值會是 t 這個 instance
setTimeout(() => {
console.log(this); // this 的值會是 t 這個 instance
}, 100);
}
}
const t = new Test();
t.run();
原因為:
這是箭頭函式的特性(是一個特例)
箭頭函式裡面的 this
,跟我怎麼呼叫沒有關係,而是跟「定義在程式碼的哪裡」有關(概念跟 scope 比較像)
上一層定義好的 this
是什麼,在箭頭函式裡面的 this
就會是什麼
箭頭函式會去用「一開始被定義好的 this
的值」
因為是在 run
裡面定義了這個箭頭函式,所以在箭頭函式裡面的 this
就會是「在 run
裡面我可以存取的到的這個 this
」
this
原本只是用來在物件導向裡面使用的
在物件導向裡面,this
是用來存取「目前對應到的這個 instance」,不然也沒有其他方法可以去存取目前對應到的 instance 了
this
對應到的就是「目前在呼叫 function 的這個 instance」
如果不是在物件導向的環境(也沒有在嚴格模式下),this
的預設值會是一個「global 的東西」,global 會取決於環境而有不同的值,但都會是一個全域的東西
例如:
this
的預設值是一個叫做 global
的變數function test() {
console.log(this === global); // 在 Node.js 裡面,this 的預設值是一個叫做 global 的變數
}
test(); // true
this
的預設值是 window
use strict
開啟嚴格模式但其實,會有上面這些預設值還滿奇怪的,因為 this
並沒有指向到任何東西,怎麼還會給它這些 global 的值呢?
這時,可以用 "use strict"
來開啟嚴格模式
this
預設值都會是 undefined,因為 this
本來就不需要任何預設值"use strict"; // 開啟嚴格模式
function test() {
console.log(this);
}
test(); // undefined
this
放在 function 裡面,this
預設值也是 undefined"use strict"; // 開啟嚴格模式
function test() {
var a = 1;
function inner() {
console.log(this);
}
inner();
}
test(); // undefined
如果是「用 DOM + 瀏覽器的事件」,this
的預設值就是「你去實際做操作的那個東西」
例如以下的 EventListener:
我 click 到哪一個按鈕,this
就會指向到那個按鈕
//用 DOM + 瀏覽器的事件
document.querySelector("btn").addEventListener("click", function () {
console.log(this); // 我 click 到哪一個按鈕,this 就會指向到那個按鈕
});
this
都是沒有任何意義的(undefined)call()
來呼叫 function在 call()
裡面傳入的第一個參數,就會是 function 裡面的 this
"use strict"; // 開啟嚴格模式
function test() {
console.log(this); // 123
}
test.call(123);
apply()
來呼叫 function在 apply()
裡面傳入的第一個參數,就會是 function 裡面的 this
"use strict"; // 開啟嚴格模式
function test() {
console.log(this); // [ 1 ]
}
test.apply([1]);
call()
可以傳多個參數,都是用逗號隔開apply()
的參數只能有兩個,第二個參數會是一個 array,用 array 把我要傳的參數包起來call()
跟 apply()
的第一個參數,都可以去改變 function 裡面 this
的值。傳入什麼值,this
就會是什麼值
對於 call 來說,可以傳多個參數,都是用逗號隔開
"hello"
就會是 this
的值"use strict"; // 開啟嚴格模式
function test(a, b, c) {
console.log(this); // [String: 'hello']
console.log(a, b, c); // 1 2 3
}
test.call("hello", 1, 2, 3);
對於 apply 來說,參數只能有兩個
this
的值"use strict"; // 開啟嚴格模式
function test(a, b, c) {
console.log(this); // hello
console.log(a, b, c); // 1 2 3
}
test.apply("hello", [1, 2, 3]); // 第二個參數會是一個 array,用 array 把我要傳的參數包起來
this
的值跟「寫在程式碼的哪裡」無關,只跟「function 是怎麼被呼叫的」有關this
的值會是「obj
本身」"use strict"; // 開啟嚴格模式
const obj = {
a: 123,
test: function () {
console.log(this); // { a: 123, test: [Function: test] }
},
};
obj.test();
this
的值都會是 inner
這個物件「直接呼叫 function」跟「用 call 呼叫」,this
的值都會是 inner
這個物件
"use strict"; // 開啟嚴格模式
const obj = {
a: 123,
inner: {
test: function () {
console.log(this); //
},
},
};
obj.inner.test(); // { test: [Function: test] }
obj.inner.test.call(obj.inner); // { test: [Function: test] }
this
的值會是 undefined執行 func()
會印出 this
的值是 undefined
"use strict"; // 開啟嚴格模式
const obj = {
a: 123,
inner: {
test: function () {
console.log(this); // undefined
},
},
};
const func = obj.inner.test;
func();
原因為:
func()
可以看成是 func.call(undefined)
因為在 func.call()
前面沒有其他東西了,所以第一個參數就只能是 undefined
"use strict"; // 開啟嚴格模式
const obj = {
a: 123,
inner: {
test: function () {
console.log(this); // undefined
},
},
};
const func = obj.inner.test;
func() 可以看成是 func.call(undefined);
call()
的形式來呼叫,就可以很清楚知道 this
的值會是什麼了用 call()
這個形式來呼叫 function,就可以很簡便的得知 this
的值
例如:
func()
因為在 func
前面沒有任何東西,所以 this
的預設值就是 undefinedobj.inner.test()
因為在 test()
前面是 obj.inner
,所以 this
的預設值就是 obj.inner
func() 可以看成是 func.call(undefined); // this 的預設值就是 undefined
obj.inner.test() 可以看成是 obj.inner.test.call(obj.inner) // this 的預設值就是 obj.inner
"use strict"; // 開啟嚴格模式
const obj = {
a: 123,
inner: {
test: function () {
console.log(this); // undefined
},
},
};
const func = obj.inner.test;
func() 可以看成是 func.call(undefined);
obj.inner.test() 可以看成是 obj.inner.test.call(obj.inner)
this
的部分就沒問題了!this
,會依據「是否為嚴格模式」以及「執行環境不同」而有不同的預設值(有可能是 window
或是 undefined
)this
,this
就會是「自己這個 instance」this
,可以把 function call 轉換成 call()
的形式,call()
的第一個參數是什麼,this
的預設值就會是什麼this
的練習題:下面各會印出什麼值呢?
"use strict";
function log() {
console.log(this);
}
var a = { a: 1, log: log };
var b = { a: 2, log: log };
log(); // undefined
a.log(); // { a: 1, log: [Function: log] }
b.log.apply(a); // { a: 1, log: [Function: log] }
log()
就是在一個沒有意義的地方呼叫 this
,在嚴格模式下,this
的值就會是 undefineda.log()
,this
的值就會是 a
b.log.apply(a)
,在 apply()
裡面傳入的第一個參數就會是 this
的值,所以就是 a
bind
強制綁定 this
的值下面範例中,用不同的方式去呼叫 function,this
就會有不同的值:
obj.test()
,this
的值是 obj
func()
,this
的值是 undefined"use strict";
const obj = {
a: 1,
test: function () {
console.log(this);
},
};
obj.test(); // this 的值是 obj
const func = obj.test;
func(); // this 的值是 undefined
但我想要做的是:不管用什麼方式呼叫 function,this
的值都是一樣的
要做到這件事情,就要用 bind
把 this
的值給強制綁定住,範例如下:
bind()
小括號裡面的東西當作 this
放到 obj.test
綁定完成之後,我就不用擔心因為呼叫的方式不同而有不同的 this
值了
就算是用 bindTest.call(123)
來呼叫 function,this
的值依然會是 hello
bind
跟「call
, apply
」不同的地方在於:bind
幫我把 this
綁定完之後,會回傳一個 function,所以 bindTest
會是一個 functioncall
, apply
」是幫我指定完 this
的值之後,會直接呼叫 function"use strict";
const obj = {
a: 1,
test: function () {
console.log(this);
},
};
const bindTest = obj.test.bind("hello");
bindTest(); // this 的值會是 hello
bindTest.call(123); // 就算用 call 來呼叫,this 的值依然會是 hello
先看這個沒有箭頭函式的範例:
class Test {
run() {
console.log("run this: ", this); // this 的值會是 t 這個 instance
setTimeout(function () {
console.log(this); // this 的值會是 window
}, 100);
}
}
const t = new Test();
t.run();
把上面的程式碼貼到瀏覽器的 console 去執行:
this
就會是 t
這個 instancethis
就會是 window為什麼第 5 行的 this
會是 window 呢?
因為過了 100 毫秒之後執行 setTimeout
裡面的 function,就是在一個沒有意義的地方呼叫 this
,所以在寬鬆模式下,瀏覽器的 this
預設值就會是 window(如果是在嚴格模式下,瀏覽器的 this
預設值就會是 undefined)
this
現在,我把 setTimeout
裡面的 function 改成箭頭函式,結果印出來的 this
跟上一層的 this
是一樣的(都是 t
這個 instance)
class Test {
run() {
console.log("run this: ", this); // this 的值會是 t 這個 instance
setTimeout(() => {
console.log(this); // this 的值會是 t 這個 instance
}, 100);
}
}
const t = new Test();
t.run();
原因為:
這是箭頭函式的特性(是一個特例)
箭頭函式裡面的 this
,跟我怎麼呼叫沒有關係,而是跟「定義在程式碼的哪裡」有關(概念跟 scope 比較像)
上一層定義好的 this
是什麼,在箭頭函式裡面的 this
就會是什麼
箭頭函式會去用「一開始被定義好的 this
的值」
因為是在 run
裡面定義了這個箭頭函式,所以在箭頭函式裡面的 this
就會是「在 run
裡面我可以存取的到的這個 this
」
class
的名稱一定要大寫開頭class
畫出一個設計圖,列出 Dog
有哪些 method 可以用new Dog()
從「Dog
這個 class」實際建立出一個物件(instance)然後才可以使用 d.sayHello()
// 先用 class 畫出一個設計圖
class Dog {
sayHello() {
console.log("hello");
}
}
var d = new Dog(); // 實際建立出一個物件(instance)
d.sayHello(); // hello
this
會指向「目前所在的 instance」如果 this
是出現在物件導向「裡面」的話:
d.setName("harry")
代表「我要對 d
做 setName("harry")
」,所以這個 this
就會指向「呼叫 setName("harry")
的 instance」,也就是 d
第 4 行的 this.name = name
就是把 d
的 name
設為「我傳入的參數 name
」
setName(name)
函式稱為 setter,專門用來設定東西用的getName()
函式稱為 getter,專門用來取得值雖然用 d.name
也可以取得/更改值,但是不建議這樣寫。還是會建議用 class Dog
裡面提供的 method,也就是 d.getName()
和 d.setName()
來取得/更改 d
的 name
的值,這是比較好的開發習慣
// 先用 class 畫出一個設計圖
class Dog {
// setter
setName(name) {
this.name = name; // this 會指向 d
}
// getter
getName() {
return this.name;
}
sayHello() {
console.log("hello");
}
}
var d = new Dog(); // 實際建立出一個物件(instance)
d.setName("harry");
console.log(d.getName()); // harry
constructor
函式做初始化new Dog()
就像是一個 function call,可以傳參數進去,例如用 new Dog("danny")
把狗取名為 danny,用 new Dog("ben")
把狗取名為 ben
然後,在 class Dog
裡面就可以用 constructor
函式來接收參數
constructor
是一個特別的 function,稱為「建構子」,常用來做初始化
當我呼叫 new Dog("danny")
時,其實就是在呼叫 constructor
函式
所以,在 new Dog("danny")
裡面傳入的參數 danny
,就可以在 constructor(name)
這裡的 name
接收到,並且把 this.name
設定為 danny
// 先用 class 畫出一個設計圖
class Dog {
// 用 constructor 接收參數
constructor(name) {
this.name = name;
}
// setter
setName(name) {
this.name = name; // this 會指向 d
}
// getter
getName() {
return this.name;
}
sayHello() {
console.log(this.name);
}
}
var d = new Dog("danny"); // 實際建立出一個物件(instance)
d.sayHello(); // danny
var b = new Dog("ben");
b.sayHello(); // ben
console.log(d.sayHello === b.sayHello); // true
d.sayHello === b.sayHello
是 true 可以得知「d.sayHello
和 b.sayHello
是同一個 function」,只是會根據 this
指向不同的 instance,而印出不同的 this.name
因為在 ES5 裡面,沒有 class
這個語法可以用,所以要用其他方式來實作出物件導向:
// ES5 的物件導向
function Dog(name) {
let myName = name;
return {
getName: function () {
return myName;
},
sayHello: function () {
console.log(myName);
},
};
}
let d = Dog("danny");
d.sayHello(); // danny
let b = Dog("ben");
b.sayHello(); // ben
但是,這樣寫會有個問題是:
每呼叫一次 Dog
函式,都會產生一個新的物件,重新產生並回傳 getName
和 sayHello
這兩個 function
從 d.getName === b.getName
是 false 可以得知「d.getName
和 b.getName
是不同的兩個 function」
那如果我有 1000 隻狗,豈不是就會有 1000 個 getName
函式?這樣會很耗費記憶體
getName
函式才對,因為都是要做同樣一件事情(就是要 get name 而已),其實只需要一個 getName
函式就夠了!// ES5 的物件導向
function Dog(name) {
let myName = name;
return {
getName: function () {
return myName;
},
sayHello: function () {
console.log(myName);
},
};
}
let d = Dog("danny");
let b = Dog("ben");
console.log(d.getName === b.getName); // false
要怎麼解決上面的問題呢?解法如下:
constructor
來用,來實作出物件導向只要我在呼叫 Dog("danny")
前面加上 new
這個關鍵字,JavaScript 就會幫我在背後做好這整個機制:把 Dog
函式,變成是 ES6 的 constructor
函式的意思
這樣一來,d
就會是一個 instance 了
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
function Dog(name) {
this.name = name;
}
var d = new Dog("danny"); // 加上 new 這個關鍵字
console.log(d); // Dog { name: 'danny' }
prototype
在 Dog
裡面建立 methodprototype
是 JS 的一個機制
Dog.prototype.sayHello
就可以幫 Dog
的 prototype
加上 sayHello
這個 function 了然後,用 d.sayHello()
就可以呼叫到 Dog.prototype
裡面的 sayHello
這個 function
這時,d.sayHello
和 b.sayHello
會是同一個 function,因為這兩個都是在同一個 prototype
上面
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
function Dog(name) {
this.name = name;
}
Dog.prototype.getName = function () {
return this.name;
};
Dog.prototype.sayHello = function () {
console.log(this.name);
};
var d = new Dog("danny");
d.sayHello(); // danny
var b = new Dog("ben");
b.sayHello(); // ben
console.log(d.sayHello === b.sayHello); // true
(ES6 的物件導向,在底層就是用 prototype 去實作出來的)
d
是 Dog
的一個 instance,d
和 Dog.prototype
之間,會透過 __proto__
這個內部屬性給連接起來,這樣 JS 的引擎才會知道「呼叫 d.sayHello()
時,就要去 Dog.prototype
裡面找 sayHello
這個 function」在 JS 有一個內部的屬性叫做 __proto__
,這個屬性會暗示說「如果在 d
身上找不到 sayHello
的話,就要去 __proto__
找」
console.log(d.__proto__)
印出的結果會是:
{ getName: [Function (anonymous)], sayHello: [Function (anonymous)] }
d
是 Dog
的一個 instance,所以 d.__proto__
會等於 Dog.prototype
(這是 new
這個關鍵字幫我設定好的)console.log(d.__proto__ === Dog.prototype); // true
d.sayHello()
時,依序會是這樣的流程:d
身上找,有沒有 sayHello
-> 沒有d.__proto__
身上找,有沒有 sayHello
d.__proto__
= Dog.prototype
,所以就會去 Dog.prototype
找到 sayHello
並呼叫,裡面的 this
就會是 d
sayHello
的話,就會再往上一層的 d.__proto__.__proto__
找,有沒有 sayHello
d.__proto__.__proto__
= Object.prototype
,所以就會去 Object.prototype
找到 sayHello
並呼叫,裡面的 this
就會是 d
sayHello
的話,就會再往上一層的 d.__proto__.__proto__.__proto__
找,有沒有 sayHello
d.__proto__.__proto__.__proto__
已經回傳 null
了,就代表「已經找到頂了,都沒有找到這個 function」,那就會拋出錯誤「d.sayHello is not a function」d.sayHello()
1. d 身上有沒有 sayHello
2. d.__proto__ 有沒有 sayHello
3. d.__proto__.__proto__ 有沒有 sayHello
4. d.__proto__.__proto__.__proto__ 有沒有 sayHello
5. null 找到頂了
d.__proto__ = Dog.prototype
d.__proto__.__proto__ = Object.prototype
Dog.prototype.__proto__ = Object.prototype
__proto__
這個內部的屬性所構成的「prototype chain 原型鍊」,一層一層往上找,看是否能找到對應的 functionDog.prototype
和 Object.prototype
同時都有 sayHello
的話因為會先在 Dog.prototype
找到 sayHello
,就不會再繼續往上找了,所以印出的結果會是 dog danny
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
function Dog(name) {
this.name = name;
}
Dog.prototype.getName = function () {
return this.name;
};
Dog.prototype.sayHello = function () {
console.log("dog", this.name);
};
Object.prototype.sayHello = function () {
console.log("object", this.name);
};
var d = new Dog("danny");
d.sayHello(); // dog danny
Dog.__proto__
是一個 Function
,所以 Dog.__proto__
會等於 Function.prototype
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
function Dog(name) {
this.name = name;
}
Dog.prototype.getName = function () {
return this.name;
};
Dog.prototype.sayHello = function () {
console.log("dog", this.name);
};
Object.prototype.sayHello = function () {
console.log("object", this.name);
};
var d = new Dog("danny");
console.log(Dog.__proto__); // Function
console.log(Dog.__proto__ === Function.prototype); // true
String.prototype
var a = "friday";
a.toString();
console.log(a.__proto__ === String.prototype); // true
console.log(a.toString === String.prototype.toString); // true
字串 a
本身並沒有 toString
這個 method 可以用
因為 a
是一個 String,所以 a.__proto__
會等於 String.prototype
toString
這個 function 是放在 prototype
上面的
當我 call a.toString
時,其實是在 call String.prototype.toString
String.prototype
加上自訂的 function我只要在 String.prototype
加上 first
這樣的一個 function,
任何一個字串就都可以用 first
這個 function 來取得字串的第 0 個字元
這裡的 this
指向的就是「呼叫 first
的字串」
// 取得字串的第 0 個字元
String.prototype.first = function () {
return this[0];
};
var a = "friday";
console.log(a.first()); // f
先補充一個預備知識:
test.call()
是另一種呼叫 function 的方式,call()
小括號裡面可以傳參數進去
如果我在第一個參數傳 123 進去,印出來的 this
就會是 123
意思就是
call()
來呼叫 function 時,在 call()
裡面傳入的第一個參數,就會是 function 裡面的 this
function test() {
console.log(this);
}
test.call("123"); // [String: '123']
newDog()
要做的事情跟「new
這個關鍵字」一樣,這樣 b.sayHello()
才有辦法跑
Dog.call(obj, name)
這行 Dog.call(obj, name)
,就會去執行 Dog
這個 constructor 函式,因為 Dog
裡面的 this
會是 obj
,所以就是 obj.name = name
,所以在 obj
這個物件裡面就會有 name: 'ben'
這個資料了
obj
這個物件就是「在執行完 constructor 之後,可以把我在 newDog(name)
傳入的 name
放在裡面」
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
function Dog(name) {
this.name = name;
}
Dog.prototype.getName = function () {
return this.name;
};
Dog.prototype.sayHello = function () {
console.log(this.name);
};
var d = new Dog("danny");
var b = newDog("ben"); // newDog() 要做的事情跟「new 這個關鍵字」一樣
// b.sayHello();
function newDog(name) {
var obj = {};
Dog.call(obj, name); // 執行 constructor,在 Dog 裡面的 this 會是 obj
console.log(obj); // { name: 'ben' }
}
obj.__proto__ = Dog.prototype
的連結把 obj.__proto__
給指定到 Dog.prototype
,建立連結後,obj
也可以使用 Dog.prototype
上面的 function 了
obj
回傳回去因為 var b = newDog("ben")
,newDog("ben")
最後會 return obj
-> 所以,obj
就會等同於 b
,也就等同於是 Dog
的一個 instance 了
obj.__proto__ = Dog.prototype
這行就會等於是 b.__proto__ = Dog.prototype
所以,b
就可以去使用 Dog.prototype
上面的 sayHello
了,b.sayHello()
就可以跑了!(會印出我傳入的 ben
)
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
function Dog(name) {
this.name = name;
}
Dog.prototype.getName = function () {
return this.name;
};
Dog.prototype.sayHello = function () {
console.log(this.name);
};
var d = new Dog("danny"); // Dog 裡面的 this 會是 d
var b = newDog("ben"); // newDog() 要變成是 Dog 的一個 instance,要做的事情跟「new 這個關鍵字」一樣
b.sayHello(); // ben
function newDog(name) {
var obj = {}; // 產生一個新的 object
Dog.call(obj, name); // 執行 constructor(也就是 Dog 函式),把 obj 當作 this 丟進 constructor 裡面
// console.log(obj); // { name: 'ben' }
obj.__proto__ = Dog.prototype; // 建立連結,讓 obj 也可以使用 Dog.prototype 上面的 function
return obj; // 讓 obj 等同於 b
}
看完上面的範例後,可以知道 new 這個關鍵字幫我做的就是下面這幾件事情:
var obj = {};
Dog
函式)Dog.call(obj, name)
this
丟進 constructor 裡面function Dog(name) {
this.name = name;
}
__proto__
,讓 obj.__proto__
去連到 Dog.prototype
,這樣 b
才可以使用 sayHello
這個 functionobj.__proto__ = Dog.prototype;
b
(也就是 Dog
的一個 instance)return obj;
物件導向有一個很有名的概念,叫做「繼承」
需要用到一些共同的屬性時,就可以用繼承的方式(就不用所有東西都自己重新做)
範例:
有一種特殊品種的狗叫做 BlackDog
class BlackDog extends Dog
就是「讓 BlackDog
去繼承 Dog
」BlackDog
繼承 Dog
之後,就可以使用 Dog
的每一個 method (function) 了!
就像是 BlackDog
把 Dog
的東西都拿過來了,所以 BlackDog
可以使用 constructor
, sayHello
, 以及自己的 test
當執行到第 19 行 const d = new BlackDog("danny")
BlackDog
裡面沒有寫 constructor,所以就會往上層的 parent 找(因為 BlackDog
繼承了 Dog
,所以 BlackDog
的 parent 就是 Dog
),在 Dog
找到了 constructor 後,就執行 constructorBlackDog
就擁有 this.name
了當執行到第 21 行 d.test()
時,就會印出 test
裡面的 this.name
也可以用 d.sayHello()
來印出 this.name
// ES6 的物件導向
class Dog {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(this.name);
}
}
// BlackDog 繼承了 Dog
class BlackDog extends Dog {
test() {
console.log("test!", this.name);
}
}
const d = new BlackDog("danny");
d.test(); // test! danny
d.sayHello(); // danny
BlackDog
被建立時,就呼叫 sayHello
// ES6 的物件導向
class Dog {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(this.name);
}
}
// BlackDog 繼承了 Dog
class BlackDog extends Dog {
// 錯誤寫法
constructor() {
this.sayHello(); // 在 BlackDog 被建立時,就呼叫 sayHello
}
test() {
console.log("test!", this.name);
}
}
const d = new BlackDog("danny");
執行之後會噴出錯誤「ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor」
意思就是:
在存取 this
之前,一定要先 call super
而 super
就是「上一層的 constructor」,也就是「Dog
的 constructor」
為什麼一定要先 call super
呢?
原因為:
如果沒有先 call super
的話,
在執行 BlackDog
裡面的 this.sayHello()
時,在 sayHello
裡面會用到 this.name
,但是這時還沒有執行到 Dog
的 constructor,所以 this.name
還沒被初始化,就會造成 bug
因此,就強制一定要先 call super
super
是沒用的因為這時在 Dog
的 constructor
接收的 name
會是 undefined
那既然我已經在 BlackDog
複寫一個 constructor 了,那在 BlackDog
的 constructor 就要負責接收 name
,並且用 super(name)
把 name
也傳到 parent 的 constructor 去,讓 Dog
的 constructor 可以成功的初始化
這樣當我在 BlackDog
裡面 call this.sayHello
時,才會印出正確的 this.name
super
,才能存取到 this
// ES6 的物件導向
class Dog {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(this.name);
}
}
// BlackDog 繼承了 Dog
class BlackDog extends Dog {
// 正確寫法
constructor(name) {
super(name); // 就是 Dog 的 constructor,把 name 傳到 Dog 的 constructor 去
this.sayHello(); // 在 BlackDog 被建立時,就呼叫 sayHello(會印出 this.name)
}
test() {
console.log("test!", this.name);
}
}
const d = new BlackDog("danny");
]]>class
的名稱一定要大寫開頭class
畫出一個設計圖,列出 Dog
有哪些 method 可以用new Dog()
從「Dog
這個 class」實際建立出一個物件(instance)然後才可以使用 d.sayHello()
// 先用 class 畫出一個設計圖
class Dog {
sayHello() {
console.log("hello");
}
}
var d = new Dog(); // 實際建立出一個物件(instance)
d.sayHello(); // hello
this
會指向「目前所在的 instance」如果 this
是出現在物件導向「裡面」的話:
d.setName("harry")
代表「我要對 d
做 setName("harry")
」,所以這個 this
就會指向「呼叫 setName("harry")
的 instance」,也就是 d
第 4 行的 this.name = name
就是把 d
的 name
設為「我傳入的參數 name
」
setName(name)
函式稱為 setter,專門用來設定東西用的getName()
函式稱為 getter,專門用來取得值雖然用 d.name
也可以取得/更改值,但是不建議這樣寫。還是會建議用 class Dog
裡面提供的 method,也就是 d.getName()
和 d.setName()
來取得/更改 d
的 name
的值,這是比較好的開發習慣
// 先用 class 畫出一個設計圖
class Dog {
// setter
setName(name) {
this.name = name; // this 會指向 d
}
// getter
getName() {
return this.name;
}
sayHello() {
console.log("hello");
}
}
var d = new Dog(); // 實際建立出一個物件(instance)
d.setName("harry");
console.log(d.getName()); // harry
constructor
函式做初始化new Dog()
就像是一個 function call,可以傳參數進去,例如用 new Dog("danny")
把狗取名為 danny,用 new Dog("ben")
把狗取名為 ben
然後,在 class Dog
裡面就可以用 constructor
函式來接收參數
constructor
是一個特別的 function,稱為「建構子」,常用來做初始化
當我呼叫 new Dog("danny")
時,其實就是在呼叫 constructor
函式
所以,在 new Dog("danny")
裡面傳入的參數 danny
,就可以在 constructor(name)
這裡的 name
接收到,並且把 this.name
設定為 danny
// 先用 class 畫出一個設計圖
class Dog {
// 用 constructor 接收參數
constructor(name) {
this.name = name;
}
// setter
setName(name) {
this.name = name; // this 會指向 d
}
// getter
getName() {
return this.name;
}
sayHello() {
console.log(this.name);
}
}
var d = new Dog("danny"); // 實際建立出一個物件(instance)
d.sayHello(); // danny
var b = new Dog("ben");
b.sayHello(); // ben
console.log(d.sayHello === b.sayHello); // true
d.sayHello === b.sayHello
是 true 可以得知「d.sayHello
和 b.sayHello
是同一個 function」,只是會根據 this
指向不同的 instance,而印出不同的 this.name
因為在 ES5 裡面,沒有 class
這個語法可以用,所以要用其他方式來實作出物件導向:
// ES5 的物件導向
function Dog(name) {
let myName = name;
return {
getName: function () {
return myName;
},
sayHello: function () {
console.log(myName);
},
};
}
let d = Dog("danny");
d.sayHello(); // danny
let b = Dog("ben");
b.sayHello(); // ben
但是,這樣寫會有個問題是:
每呼叫一次 Dog
函式,都會產生一個新的物件,重新產生並回傳 getName
和 sayHello
這兩個 function
從 d.getName === b.getName
是 false 可以得知「d.getName
和 b.getName
是不同的兩個 function」
那如果我有 1000 隻狗,豈不是就會有 1000 個 getName
函式?這樣會很耗費記憶體
getName
函式才對,因為都是要做同樣一件事情(就是要 get name 而已),其實只需要一個 getName
函式就夠了!// ES5 的物件導向
function Dog(name) {
let myName = name;
return {
getName: function () {
return myName;
},
sayHello: function () {
console.log(myName);
},
};
}
let d = Dog("danny");
let b = Dog("ben");
console.log(d.getName === b.getName); // false
要怎麼解決上面的問題呢?解法如下:
constructor
來用,來實作出物件導向只要我在呼叫 Dog("danny")
前面加上 new
這個關鍵字,JavaScript 就會幫我在背後做好這整個機制:把 Dog
函式,變成是 ES6 的 constructor
函式的意思
這樣一來,d
就會是一個 instance 了
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
function Dog(name) {
this.name = name;
}
var d = new Dog("danny"); // 加上 new 這個關鍵字
console.log(d); // Dog { name: 'danny' }
prototype
在 Dog
裡面建立 methodprototype
是 JS 的一個機制
Dog.prototype.sayHello
就可以幫 Dog
的 prototype
加上 sayHello
這個 function 了然後,用 d.sayHello()
就可以呼叫到 Dog.prototype
裡面的 sayHello
這個 function
這時,d.sayHello
和 b.sayHello
會是同一個 function,因為這兩個都是在同一個 prototype
上面
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
function Dog(name) {
this.name = name;
}
Dog.prototype.getName = function () {
return this.name;
};
Dog.prototype.sayHello = function () {
console.log(this.name);
};
var d = new Dog("danny");
d.sayHello(); // danny
var b = new Dog("ben");
b.sayHello(); // ben
console.log(d.sayHello === b.sayHello); // true
(ES6 的物件導向,在底層就是用 prototype 去實作出來的)
d
是 Dog
的一個 instance,d
和 Dog.prototype
之間,會透過 __proto__
這個內部屬性給連接起來,這樣 JS 的引擎才會知道「呼叫 d.sayHello()
時,就要去 Dog.prototype
裡面找 sayHello
這個 function」在 JS 有一個內部的屬性叫做 __proto__
,這個屬性會暗示說「如果在 d
身上找不到 sayHello
的話,就要去 __proto__
找」
console.log(d.__proto__)
印出的結果會是:
{ getName: [Function (anonymous)], sayHello: [Function (anonymous)] }
d
是 Dog
的一個 instance,所以 d.__proto__
會等於 Dog.prototype
(這是 new
這個關鍵字幫我設定好的)console.log(d.__proto__ === Dog.prototype); // true
d.sayHello()
時,依序會是這樣的流程:d
身上找,有沒有 sayHello
-> 沒有d.__proto__
身上找,有沒有 sayHello
d.__proto__
= Dog.prototype
,所以就會去 Dog.prototype
找到 sayHello
並呼叫,裡面的 this
就會是 d
sayHello
的話,就會再往上一層的 d.__proto__.__proto__
找,有沒有 sayHello
d.__proto__.__proto__
= Object.prototype
,所以就會去 Object.prototype
找到 sayHello
並呼叫,裡面的 this
就會是 d
sayHello
的話,就會再往上一層的 d.__proto__.__proto__.__proto__
找,有沒有 sayHello
d.__proto__.__proto__.__proto__
已經回傳 null
了,就代表「已經找到頂了,都沒有找到這個 function」,那就會拋出錯誤「d.sayHello is not a function」d.sayHello()
1. d 身上有沒有 sayHello
2. d.__proto__ 有沒有 sayHello
3. d.__proto__.__proto__ 有沒有 sayHello
4. d.__proto__.__proto__.__proto__ 有沒有 sayHello
5. null 找到頂了
d.__proto__ = Dog.prototype
d.__proto__.__proto__ = Object.prototype
Dog.prototype.__proto__ = Object.prototype
__proto__
這個內部的屬性所構成的「prototype chain 原型鍊」,一層一層往上找,看是否能找到對應的 functionDog.prototype
和 Object.prototype
同時都有 sayHello
的話因為會先在 Dog.prototype
找到 sayHello
,就不會再繼續往上找了,所以印出的結果會是 dog danny
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
function Dog(name) {
this.name = name;
}
Dog.prototype.getName = function () {
return this.name;
};
Dog.prototype.sayHello = function () {
console.log("dog", this.name);
};
Object.prototype.sayHello = function () {
console.log("object", this.name);
};
var d = new Dog("danny");
d.sayHello(); // dog danny
Dog.__proto__
是一個 Function
,所以 Dog.__proto__
會等於 Function.prototype
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
function Dog(name) {
this.name = name;
}
Dog.prototype.getName = function () {
return this.name;
};
Dog.prototype.sayHello = function () {
console.log("dog", this.name);
};
Object.prototype.sayHello = function () {
console.log("object", this.name);
};
var d = new Dog("danny");
console.log(Dog.__proto__); // Function
console.log(Dog.__proto__ === Function.prototype); // true
String.prototype
var a = "friday";
a.toString();
console.log(a.__proto__ === String.prototype); // true
console.log(a.toString === String.prototype.toString); // true
字串 a
本身並沒有 toString
這個 method 可以用
因為 a
是一個 String,所以 a.__proto__
會等於 String.prototype
toString
這個 function 是放在 prototype
上面的
當我 call a.toString
時,其實是在 call String.prototype.toString
String.prototype
加上自訂的 function我只要在 String.prototype
加上 first
這樣的一個 function,
任何一個字串就都可以用 first
這個 function 來取得字串的第 0 個字元
這裡的 this
指向的就是「呼叫 first
的字串」
// 取得字串的第 0 個字元
String.prototype.first = function () {
return this[0];
};
var a = "friday";
console.log(a.first()); // f
先補充一個預備知識:
test.call()
是另一種呼叫 function 的方式,call()
小括號裡面可以傳參數進去
如果我在第一個參數傳 123 進去,印出來的 this
就會是 123
意思就是
call()
來呼叫 function 時,在 call()
裡面傳入的第一個參數,就會是 function 裡面的 this
function test() {
console.log(this);
}
test.call("123"); // [String: '123']
newDog()
要做的事情跟「new
這個關鍵字」一樣,這樣 b.sayHello()
才有辦法跑
Dog.call(obj, name)
這行 Dog.call(obj, name)
,就會去執行 Dog
這個 constructor 函式,因為 Dog
裡面的 this
會是 obj
,所以就是 obj.name = name
,所以在 obj
這個物件裡面就會有 name: 'ben'
這個資料了
obj
這個物件就是「在執行完 constructor 之後,可以把我在 newDog(name)
傳入的 name
放在裡面」
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
function Dog(name) {
this.name = name;
}
Dog.prototype.getName = function () {
return this.name;
};
Dog.prototype.sayHello = function () {
console.log(this.name);
};
var d = new Dog("danny");
var b = newDog("ben"); // newDog() 要做的事情跟「new 這個關鍵字」一樣
// b.sayHello();
function newDog(name) {
var obj = {};
Dog.call(obj, name); // 執行 constructor,在 Dog 裡面的 this 會是 obj
console.log(obj); // { name: 'ben' }
}
obj.__proto__ = Dog.prototype
的連結把 obj.__proto__
給指定到 Dog.prototype
,建立連結後,obj
也可以使用 Dog.prototype
上面的 function 了
obj
回傳回去因為 var b = newDog("ben")
,newDog("ben")
最後會 return obj
-> 所以,obj
就會等同於 b
,也就等同於是 Dog
的一個 instance 了
obj.__proto__ = Dog.prototype
這行就會等於是 b.__proto__ = Dog.prototype
所以,b
就可以去使用 Dog.prototype
上面的 sayHello
了,b.sayHello()
就可以跑了!(會印出我傳入的 ben
)
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
function Dog(name) {
this.name = name;
}
Dog.prototype.getName = function () {
return this.name;
};
Dog.prototype.sayHello = function () {
console.log(this.name);
};
var d = new Dog("danny"); // Dog 裡面的 this 會是 d
var b = newDog("ben"); // newDog() 要變成是 Dog 的一個 instance,要做的事情跟「new 這個關鍵字」一樣
b.sayHello(); // ben
function newDog(name) {
var obj = {}; // 產生一個新的 object
Dog.call(obj, name); // 執行 constructor(也就是 Dog 函式),把 obj 當作 this 丟進 constructor 裡面
// console.log(obj); // { name: 'ben' }
obj.__proto__ = Dog.prototype; // 建立連結,讓 obj 也可以使用 Dog.prototype 上面的 function
return obj; // 讓 obj 等同於 b
}
看完上面的範例後,可以知道 new 這個關鍵字幫我做的就是下面這幾件事情:
var obj = {};
Dog
函式)Dog.call(obj, name)
this
丟進 constructor 裡面function Dog(name) {
this.name = name;
}
__proto__
,讓 obj.__proto__
去連到 Dog.prototype
,這樣 b
才可以使用 sayHello
這個 functionobj.__proto__ = Dog.prototype;
b
(也就是 Dog
的一個 instance)return obj;
物件導向有一個很有名的概念,叫做「繼承」
需要用到一些共同的屬性時,就可以用繼承的方式(就不用所有東西都自己重新做)
範例:
有一種特殊品種的狗叫做 BlackDog
class BlackDog extends Dog
就是「讓 BlackDog
去繼承 Dog
」BlackDog
繼承 Dog
之後,就可以使用 Dog
的每一個 method (function) 了!
就像是 BlackDog
把 Dog
的東西都拿過來了,所以 BlackDog
可以使用 constructor
, sayHello
, 以及自己的 test
當執行到第 19 行 const d = new BlackDog("danny")
BlackDog
裡面沒有寫 constructor,所以就會往上層的 parent 找(因為 BlackDog
繼承了 Dog
,所以 BlackDog
的 parent 就是 Dog
),在 Dog
找到了 constructor 後,就執行 constructorBlackDog
就擁有 this.name
了當執行到第 21 行 d.test()
時,就會印出 test
裡面的 this.name
也可以用 d.sayHello()
來印出 this.name
// ES6 的物件導向
class Dog {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(this.name);
}
}
// BlackDog 繼承了 Dog
class BlackDog extends Dog {
test() {
console.log("test!", this.name);
}
}
const d = new BlackDog("danny");
d.test(); // test! danny
d.sayHello(); // danny
BlackDog
被建立時,就呼叫 sayHello
// ES6 的物件導向
class Dog {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(this.name);
}
}
// BlackDog 繼承了 Dog
class BlackDog extends Dog {
// 錯誤寫法
constructor() {
this.sayHello(); // 在 BlackDog 被建立時,就呼叫 sayHello
}
test() {
console.log("test!", this.name);
}
}
const d = new BlackDog("danny");
執行之後會噴出錯誤「ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor」
意思就是:
在存取 this
之前,一定要先 call super
而 super
就是「上一層的 constructor」,也就是「Dog
的 constructor」
為什麼一定要先 call super
呢?
原因為:
如果沒有先 call super
的話,
在執行 BlackDog
裡面的 this.sayHello()
時,在 sayHello
裡面會用到 this.name
,但是這時還沒有執行到 Dog
的 constructor,所以 this.name
還沒被初始化,就會造成 bug
因此,就強制一定要先 call super
super
是沒用的因為這時在 Dog
的 constructor
接收的 name
會是 undefined
那既然我已經在 BlackDog
複寫一個 constructor 了,那在 BlackDog
的 constructor 就要負責接收 name
,並且用 super(name)
把 name
也傳到 parent 的 constructor 去,讓 Dog
的 constructor 可以成功的初始化
這樣當我在 BlackDog
裡面 call this.sayHello
時,才會印出正確的 this.name
super
,才能存取到 this
// ES6 的物件導向
class Dog {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(this.name);
}
}
// BlackDog 繼承了 Dog
class BlackDog extends Dog {
// 正確寫法
constructor(name) {
super(name); // 就是 Dog 的 constructor,把 name 傳到 Dog 的 constructor 去
this.sayHello(); // 在 BlackDog 被建立時,就呼叫 sayHello(會印出 this.name)
}
test() {
console.log("test!", this.name);
}
}
const d = new BlackDog("danny");
]]>
console.log(a)
會是 11
function test() {
var a = 10;
function inner() {
a++;
console.log(a); // 11
}
inner(); // 呼叫 inner function
}
test();
範例:
在 function test
裡面,return function inner
用一個變數 func
去接收「test()
所回傳的 inner function」
console.log(a)
一樣會是 11
function test() {
var a = 10;
function inner() {
a++;
console.log(a); // 11
}
return inner; // 回傳 inner function
}
var func = test(); // 接收 inner 這個 function
func(); // 等同於是「呼叫 inner function」
如果再次呼叫 func()
,console.log(a)
就依序會是 11, 12, 13
function test() {
var a = 10;
function inner() {
a++;
console.log(a);
}
return inner; // 回傳 inner function
}
var func = test(); // 接收 inner 這個 function
func(); // 等同於是「呼叫 inner function」
func();
func();
output:
11
12
13
以上面範例為例:
當進入到 function test
時,會產生 test EC 並建立 test VO
test EC
test VO = {
a: 10,
inner: func
}
之前有說過,當 function 執行結束後,所有資源都會被釋放掉
照理來說,當執行到第七行 return inner
之後,function test
就執行完畢了,在 test VO 裡面的資源都會被清空
function inner
不知道為什麼,可以把「在 test VO 裡面的 a
給記起來」(a
就像是一直被鎖在 function inner
裡面)只有在 function inner
裡面可以存取到 a
的值,其他地方都存取不到。也可以透過 function inner
去改變 a
的值
例如,有一個 function complex
每次都要做很複雜的計算
如果是按照以前的作法,我每 call 一次 function complex
,都要重新做一次複雜的計算才能得到結果
但是,我可以利用閉包的特性,來避免一直重複計算:
我把 function complex
傳進 function cache
後,cache(complex)
會 return 一個新的 function -> 所以,變數 cachedComplex
就是這個 return 的 function,所以就可以接收一個 num
的參數
利用 var ans = {}
這個物件,把值記起來
在 function cache
裡面 return 的 function,會把 ans
的值給記幾來,我就可以一直重複運用它
cachedComplex(20)
時:因為是第一次執行,所以會進行計算cachedComplex(20)
時:因為前面已經計算過了,所以不用重新計算,可以直接輸出結果 -> cachedComplex
利用「閉包」的特性,把值給記起來function complex(num) {
// 複雜的計算
console.log("calculate"); // 有出現 "calculate" 代表「真的有執行 function complex」
return num * num * num;
}
// 會回傳一個新的 function
function cache(func) {
var ans = {}; // 用這個物件,把值記起來
return function (num) {
if (ans[num]) {
// 如果 ans[num] 有東西的話,就直接回傳 ans[num] 的 value
return ans[num];
}
// 如果 ans[num] 沒東西的話
ans[num] = func(num); // 第一次執行時,會執行到這裡 ans[20] = complex(20)
return ans[num];
};
}
console.log(complex(20)); // 進行一次複雜的計算
console.log(complex(20)); // 進行一次複雜的計算
console.log(complex(20)); // 進行一次複雜的計算
const cachedComplex = cache(complex);
console.log(cachedComplex(20)); // 因為是第一次,所以會需要執行 complex(20)
console.log(cachedComplex(20)); // 利用閉包的特性,不需計算就可以直接回傳 ans[20] 的值
console.log(cachedComplex(20)); // 利用閉包的特性,不需計算就可以直接回傳結果 ans[20] 的值
每一次執行 cachedComplex(20)
,其實就是在執行下面這段:
會記住變數 ans
的值
return function (20) {
if (ans[20]) {
// 如果 ans[20] 有東西的話,就直接回傳 ans[20] 的 value
return ans[20];
}
// 如果 ans[20] 沒東西的話
ans[20] = func(20); // 第一次執行時,會執行到這裡 ans[20] = complex(20)
return ans[20];
};
每一個 EC 都有一個 scope chain。當進入一個 EC 時,scope chain 就會被建立
Activation Object 和 Variable Object 做的事情是一樣的,都是拿來存放相關的資訊,只是在 global EC 裡面稱作 VO,在 function EC 裡面稱作 AO
當進入到 global EC 後,VO 會被建立,裡面存放所有相關的資訊
當進入到一個 function EC 後,AO 會被新增,裡面也是存放所有相關的資訊(有一個預設的屬性是 arguments
)
var v1 = 10;
function test() {
var vTest = 20;
function inner() {
console.log(v1, vTest); // 10 20
}
return inner;
}
var inner = test();
inner();
用上面這段程式碼為例,我們假裝自己是 JS 的引擎,看看背後是怎麼實際執行的:
在 globalEC 裡面:
v1
被初始化成 undefinedinner
被初始化成 undefinedtest
是一個 functionscopeChain
會是 [globalEC.VO]
設定 function test
的 [[Scope]]
屬性,會把「上一層的 scopeChain
給複製進來」,也就是 [globalEC.VO]
globalEC: {
VO: {
v1: undefined,
inner: undefined,
test: func
},
scopeChain: [globalEC.VO]
}
test.[[Scope]] = [globalEC.VO]
第 1 行 var v1 = 10;
,會把 globalEC.VO
裡面的 v1
變成 10
第 10 行 var inner = test();
,會執行 function test
,就進入了 testEC
在 testEC
裡面:
vTest
被初始化成 undefinedinner
是一個 functiontestEC
的 scopeChain
會是「自己的 AO」+「自己的 [[Scope]]
屬性」,也就是 [testEC.AO, globalEC.VO]
設定 inner
的 [[Scope]]
屬性,會把「上一層的 scopeChain
給複製進來」,也就是 [testEC.AO, globalEC.VO]
testEC: {
AO: {
vTest: undefined,
inner: func
},
scopeChain: [testEC.AO, globalEC.VO]
}
inner.[[Scope]] = [testEC.AO, globalEC.VO]
globalEC: {
VO: {
v1: 10,
inner: undefined,
test: func
},
scopeChain: [globalEC.VO]
}
test.[[Scope]] = [globalEC.VO]
執行第 3 行 var vTest = 20;
把 vTest
的值更新成 20
當執行到第 7 行 return inner;
時,照理來說,testEC
執行完畢了,testEC
和 testAO
就要被清空
可是,因為 function inner
被 return 回去,在 inner.[[Scope]]
會需要引用到 testEC.AO
和 globalEC.VO
,所以 JS 底層的垃圾回收機制就不能把 testEC.AO
和 globalEC.VO
回收掉,testEC.AO
和 globalEC.VO
都會被存在 function inner
裡面
scopeChain
、[[Scope]]
和 Closure 之間的關係,就是 Closure 的原理:因為保留了 scopeChain
,所以在離開了前一個 function 後,還是可以存取的到前一個 function 的 AO 的值因為 function 的 [[Scope]]
屬性會把「要存取的 scopeChain
」給記起來,當我進入 function 的 EC 時,再去初始化 scopeChain
。scopeChain
會需要用到前面幾層的 AO
和 VO
,所以那些 AO
和 VO
必須被保留起來
在 testEC
執行完畢後,testEC
就被 pop 掉了(testEC
可以回收),但 testEC.AO
要保留起來
inner.[[Scope]] = [testEC.AO, globalEC.VO]
globalEC: {
VO: {
v1: 10,
inner: func,
test: func
},
scopeChain: [globalEC.VO]
}
test.[[Scope]] = [globalEC.VO]
testEC.AO: {
vTest: 20,
inner: func
}
在 innerEC
裡面:
innerEC
的 scopeChain
會是「自己的 AO」+「自己的 [[Scope]]
屬性」,也就是 [innerEC.AO, testEC.AO, globalEC.VO]
inner.[[Scope]] = [testEC.AO, globalEC.VO]
innerEC: {
AO: {
},
scopeChain: [innerEC.AO, testEC.AO, globalEC.VO]
}
globalEC: {
VO: {
v1: 10,
inner: func,
test: func
},
scopeChain: [globalEC.VO]
}
test.[[Scope]] = [globalEC.VO]
testEC.AO: {
vTest: 20,
inner: func
}
執行這行 console.log(v1, vTest);
時,會在自己的 scopeChain
裡面一層一層往上找:
先找 v1
innerEC.AO
裡面,找不到 v1
testEC.AO
裡面,找不到 v1
globalEC.VO
裡面,找到 v1: 10
,所以就會輸出 10再找 vTest
innerEC.AO
裡面,找不到 vTest
testEC.AO
裡面,找到 vTest: 20
,所以就會輸出 20假設,我在 function test
裡面宣告了一個很巨大的物件 var obj = { huge object };
var v1 = 10;
function test() {
var vTest = 20;
var obj = { huge object }; // 一個很巨大的物件
function inner() {
console.log(v1, vTest); // 10 20
}
return inner;
}
var inner = test();
inner();
當 testEC
執行完畢後,這個很巨大的物件還是會隨著 testEC.AO
被帶到 innerEC
的 scopeChain
裡面
雖然我在 function inner
裡面完全不會用到這個巨大的物件,但它還是沒辦法被回收掉,因為 obj: { huge object }
就是屬於 testEC.AO
的一部分
inner.[[Scope]] = [testEC.AO, globalEC.VO]
innerEC: {
AO: {
},
scopeChain: [innerEC.AO, testEC.AO, globalEC.VO]
}
globalEC: {
VO: {
v1: 10,
inner: func,
test: func
},
scopeChain: [globalEC.VO]
}
test.[[Scope]] = [globalEC.VO]
testEC.AO: {
vTest: 20,
inner: func,
obj: { huge object } // 這個很巨大的物件還是會存在
}
var arr = [];
for (var i = 0; i < 5; i++) {
arr[i] = function () {
console.log(i);
};
}
arr[0](); // 呼叫 arr[0] 這個 function,會印出 5
當我呼叫 arr[0]()
時,我以為會輸出的是 0,但結果是輸出 5
原因為:
因為是在 global 宣告 var i = 0
,所以變數 i
就是一個全域變數(等同於是下面這樣寫)
var arr = [];
var i; // i 是全域變數
for (i = 0; i < 5; i++) {
arr[i] = function () {
console.log(i);
};
}
arr[0](); // 呼叫 arr[0] 這個 function,會印出 5
迴圈的每一圈都會建立一個 function,只是還沒執行:
// 當迴圈跑第一圈時
arr[0] = function () {
console.log(i);
};
// 當迴圈跑第二圈時
arr[1] = function () {
console.log(i);
};
... 以此類推
當執行到第 9 行 arr[0]()
時,就是執行 arr[0]
這個 function,那在 function 裡面的 console.log(i)
,要怎麼找到這個 i
呢?
因為在自己的 function scope 沒有 i
,所以會往上層的 scope(也就是 global scope)去找到這個 i
在執行第 9 行 arr[0]()
時,for 迴圈已經跑完了,這時候的全域變數 i
的值已經是 5 了,這就是為什麼最後 console.log(i)
的結果會是 5
利用閉包的原理,寫一個這樣的 function:
在 logN()
裡面傳入什麼值,就會印出什麼值
// 利用閉包的原理
function logN(n) {
return function () {
console.log(n);
};
}
const log2 = logN(2);
log2(); // 2
就可以把 for 迴圈裡面的程式碼改成 arr[i] = logN(i);
var arr = [];
var i; // i 是全域變數
for (i = 0; i < 5; i++) {
arr[i] = logN(i);
}
// 利用閉包的原理
function logN(n) {
return function () {
console.log(n);
};
}
arr[0](); // 呼叫 arr[0] 這個 function,會印出 0
因為 logN(i)
會回傳一個新的 function,所以就會有一個新的作用域可以去記住當時的 i
的值
因為 i
是 logN(i)
所傳進去的參數,所以這個 i
會被記在 function logN
的 Activation Object 裡面
改成這樣之後,就會是我想要的結果了:
執行 arr[0]()
就輸出 0,執行 arr[1]()
就輸出 1
一般我要呼叫一個 function,就是用 test()
這種方式
function test() {
console.log("hello");
}
test(); // 呼叫 function test
那要怎麼呼叫一個匿名的 function 呢?
在 JS 裡面,如果要呼叫一個「匿名的 function」,可以用 IIFE (immediately-invoked function expression)這個方法,中文翻作「立即呼叫函式表達式」,範例如下:
把 function 用一個小括號包起來,後面再加上一個小括號(裡面可以傳參數進去)
(function (num) {
console.log(num);
})(123); // 立即呼叫函式,會輸出 123
用 IIFE 也是可以呼叫一個有名字的函式:
(function test() {
console.log("hello");
})(); // 立即呼叫函式,會輸出 hello
所以上面的 for 迴圈範例,就可以改成這樣:
改成用 IIFE 的好處是「不用再額外宣告一個 logN
函式」
var arr = [];
var i; // i 是全域變數
for (i = 0; i < 5; i++) {
arr[i] = (function (number) {
return function () {
console.log(number);
};
})(i);
}
arr[1](); // 呼叫 arr[0] 這個 function,會印出 1
在 for 迴圈裡面,改為用 let
宣告 i
,就可以了
var arr = [];
// 改為用 let 宣告 i
for (let i = 0; i < 5; i++) {
arr[i] = function () {
console.log(i);
};
}
arr[1](); // 呼叫 arr[0] 這個 function,會印出 1
arr[3](); // 呼叫 arr[0] 這個 function,會印出 3
let
的表現不太一樣用 let
宣告的變數,作用域會是一個 block
所以,每一圈的 i
,就會跟 arr[i]
的 i
有相同的值
// 迴圈第 1 圈
{
let i = 0;
arr[0] = function() {
console.log(i)
}
}
// 迴圈第 2 圈
{
let i = 1;
arr[1] = function() {
console.log(i)
}
}
// 迴圈第 3 圈
{
let i = 2;
arr[2] = function() {
console.log(i)
}
}
... 以此類推
舉一個例子:
雖然我寫了兩個 function 去操控 money
但是,我會碰到的問題是:如果有其他同事跟我協作,他完全可以不透過這兩個 function,就任意把 money
的值改掉,這是我完全沒辦法預防的
var money = 99;
function add(num) {
money += num;
}
function deduct(num) {
if (num >= 10) {
money -= 10;
} else {
money -= num;
}
}
add(1); // money = 100
deduct(100); // money = 90
money = -1; // 被同事亂改成 -1
console.log(money); // -1
這時,就可以用閉包來解決這個問題:
function createWallet(initMoney) {
var money = initMoney;
return {
add: function (num) {
money += num;
},
deduct: function (num) {
if (num >= 10) {
money -= 10;
} else {
money -= num;
}
},
getMoney: function () {
return money;
},
};
}
let myWallet = createWallet(99);
myWallet.add(1);
myWallet.deduct(100);
console.log(myWallet.getMoney()); // 90
改成用這樣寫之後,其他人就沒辦法用什麼 myWallet.money = -1;
來把我的 money
亂改值了
money
的值,只能使用 createWallet
回傳回來的這些 function 來操控我傳進去的 money
的值console.log(a)
會是 11
function test() {
var a = 10;
function inner() {
a++;
console.log(a); // 11
}
inner(); // 呼叫 inner function
}
test();
範例:
在 function test
裡面,return function inner
用一個變數 func
去接收「test()
所回傳的 inner function」
console.log(a)
一樣會是 11
function test() {
var a = 10;
function inner() {
a++;
console.log(a); // 11
}
return inner; // 回傳 inner function
}
var func = test(); // 接收 inner 這個 function
func(); // 等同於是「呼叫 inner function」
如果再次呼叫 func()
,console.log(a)
就依序會是 11, 12, 13
function test() {
var a = 10;
function inner() {
a++;
console.log(a);
}
return inner; // 回傳 inner function
}
var func = test(); // 接收 inner 這個 function
func(); // 等同於是「呼叫 inner function」
func();
func();
output:
11
12
13
以上面範例為例:
當進入到 function test
時,會產生 test EC 並建立 test VO
test EC
test VO = {
a: 10,
inner: func
}
之前有說過,當 function 執行結束後,所有資源都會被釋放掉
照理來說,當執行到第七行 return inner
之後,function test
就執行完畢了,在 test VO 裡面的資源都會被清空
function inner
不知道為什麼,可以把「在 test VO 裡面的 a
給記起來」(a
就像是一直被鎖在 function inner
裡面)只有在 function inner
裡面可以存取到 a
的值,其他地方都存取不到。也可以透過 function inner
去改變 a
的值
例如,有一個 function complex
每次都要做很複雜的計算
如果是按照以前的作法,我每 call 一次 function complex
,都要重新做一次複雜的計算才能得到結果
但是,我可以利用閉包的特性,來避免一直重複計算:
我把 function complex
傳進 function cache
後,cache(complex)
會 return 一個新的 function -> 所以,變數 cachedComplex
就是這個 return 的 function,所以就可以接收一個 num
的參數
利用 var ans = {}
這個物件,把值記起來
在 function cache
裡面 return 的 function,會把 ans
的值給記幾來,我就可以一直重複運用它
cachedComplex(20)
時:因為是第一次執行,所以會進行計算cachedComplex(20)
時:因為前面已經計算過了,所以不用重新計算,可以直接輸出結果 -> cachedComplex
利用「閉包」的特性,把值給記起來function complex(num) {
// 複雜的計算
console.log("calculate"); // 有出現 "calculate" 代表「真的有執行 function complex」
return num * num * num;
}
// 會回傳一個新的 function
function cache(func) {
var ans = {}; // 用這個物件,把值記起來
return function (num) {
if (ans[num]) {
// 如果 ans[num] 有東西的話,就直接回傳 ans[num] 的 value
return ans[num];
}
// 如果 ans[num] 沒東西的話
ans[num] = func(num); // 第一次執行時,會執行到這裡 ans[20] = complex(20)
return ans[num];
};
}
console.log(complex(20)); // 進行一次複雜的計算
console.log(complex(20)); // 進行一次複雜的計算
console.log(complex(20)); // 進行一次複雜的計算
const cachedComplex = cache(complex);
console.log(cachedComplex(20)); // 因為是第一次,所以會需要執行 complex(20)
console.log(cachedComplex(20)); // 利用閉包的特性,不需計算就可以直接回傳 ans[20] 的值
console.log(cachedComplex(20)); // 利用閉包的特性,不需計算就可以直接回傳結果 ans[20] 的值
每一次執行 cachedComplex(20)
,其實就是在執行下面這段:
會記住變數 ans
的值
return function (20) {
if (ans[20]) {
// 如果 ans[20] 有東西的話,就直接回傳 ans[20] 的 value
return ans[20];
}
// 如果 ans[20] 沒東西的話
ans[20] = func(20); // 第一次執行時,會執行到這裡 ans[20] = complex(20)
return ans[20];
};
每一個 EC 都有一個 scope chain。當進入一個 EC 時,scope chain 就會被建立
Activation Object 和 Variable Object 做的事情是一樣的,都是拿來存放相關的資訊,只是在 global EC 裡面稱作 VO,在 function EC 裡面稱作 AO
當進入到 global EC 後,VO 會被建立,裡面存放所有相關的資訊
當進入到一個 function EC 後,AO 會被新增,裡面也是存放所有相關的資訊(有一個預設的屬性是 arguments
)
var v1 = 10;
function test() {
var vTest = 20;
function inner() {
console.log(v1, vTest); // 10 20
}
return inner;
}
var inner = test();
inner();
用上面這段程式碼為例,我們假裝自己是 JS 的引擎,看看背後是怎麼實際執行的:
在 globalEC 裡面:
v1
被初始化成 undefinedinner
被初始化成 undefinedtest
是一個 functionscopeChain
會是 [globalEC.VO]
設定 function test
的 [[Scope]]
屬性,會把「上一層的 scopeChain
給複製進來」,也就是 [globalEC.VO]
globalEC: {
VO: {
v1: undefined,
inner: undefined,
test: func
},
scopeChain: [globalEC.VO]
}
test.[[Scope]] = [globalEC.VO]
第 1 行 var v1 = 10;
,會把 globalEC.VO
裡面的 v1
變成 10
第 10 行 var inner = test();
,會執行 function test
,就進入了 testEC
在 testEC
裡面:
vTest
被初始化成 undefinedinner
是一個 functiontestEC
的 scopeChain
會是「自己的 AO」+「自己的 [[Scope]]
屬性」,也就是 [testEC.AO, globalEC.VO]
設定 inner
的 [[Scope]]
屬性,會把「上一層的 scopeChain
給複製進來」,也就是 [testEC.AO, globalEC.VO]
testEC: {
AO: {
vTest: undefined,
inner: func
},
scopeChain: [testEC.AO, globalEC.VO]
}
inner.[[Scope]] = [testEC.AO, globalEC.VO]
globalEC: {
VO: {
v1: 10,
inner: undefined,
test: func
},
scopeChain: [globalEC.VO]
}
test.[[Scope]] = [globalEC.VO]
執行第 3 行 var vTest = 20;
把 vTest
的值更新成 20
當執行到第 7 行 return inner;
時,照理來說,testEC
執行完畢了,testEC
和 testAO
就要被清空
可是,因為 function inner
被 return 回去,在 inner.[[Scope]]
會需要引用到 testEC.AO
和 globalEC.VO
,所以 JS 底層的垃圾回收機制就不能把 testEC.AO
和 globalEC.VO
回收掉,testEC.AO
和 globalEC.VO
都會被存在 function inner
裡面
scopeChain
、[[Scope]]
和 Closure 之間的關係,就是 Closure 的原理:因為保留了 scopeChain
,所以在離開了前一個 function 後,還是可以存取的到前一個 function 的 AO 的值因為 function 的 [[Scope]]
屬性會把「要存取的 scopeChain
」給記起來,當我進入 function 的 EC 時,再去初始化 scopeChain
。scopeChain
會需要用到前面幾層的 AO
和 VO
,所以那些 AO
和 VO
必須被保留起來
在 testEC
執行完畢後,testEC
就被 pop 掉了(testEC
可以回收),但 testEC.AO
要保留起來
inner.[[Scope]] = [testEC.AO, globalEC.VO]
globalEC: {
VO: {
v1: 10,
inner: func,
test: func
},
scopeChain: [globalEC.VO]
}
test.[[Scope]] = [globalEC.VO]
testEC.AO: {
vTest: 20,
inner: func
}
在 innerEC
裡面:
innerEC
的 scopeChain
會是「自己的 AO」+「自己的 [[Scope]]
屬性」,也就是 [innerEC.AO, testEC.AO, globalEC.VO]
inner.[[Scope]] = [testEC.AO, globalEC.VO]
innerEC: {
AO: {
},
scopeChain: [innerEC.AO, testEC.AO, globalEC.VO]
}
globalEC: {
VO: {
v1: 10,
inner: func,
test: func
},
scopeChain: [globalEC.VO]
}
test.[[Scope]] = [globalEC.VO]
testEC.AO: {
vTest: 20,
inner: func
}
執行這行 console.log(v1, vTest);
時,會在自己的 scopeChain
裡面一層一層往上找:
先找 v1
innerEC.AO
裡面,找不到 v1
testEC.AO
裡面,找不到 v1
globalEC.VO
裡面,找到 v1: 10
,所以就會輸出 10再找 vTest
innerEC.AO
裡面,找不到 vTest
testEC.AO
裡面,找到 vTest: 20
,所以就會輸出 20假設,我在 function test
裡面宣告了一個很巨大的物件 var obj = { huge object };
var v1 = 10;
function test() {
var vTest = 20;
var obj = { huge object }; // 一個很巨大的物件
function inner() {
console.log(v1, vTest); // 10 20
}
return inner;
}
var inner = test();
inner();
當 testEC
執行完畢後,這個很巨大的物件還是會隨著 testEC.AO
被帶到 innerEC
的 scopeChain
裡面
雖然我在 function inner
裡面完全不會用到這個巨大的物件,但它還是沒辦法被回收掉,因為 obj: { huge object }
就是屬於 testEC.AO
的一部分
inner.[[Scope]] = [testEC.AO, globalEC.VO]
innerEC: {
AO: {
},
scopeChain: [innerEC.AO, testEC.AO, globalEC.VO]
}
globalEC: {
VO: {
v1: 10,
inner: func,
test: func
},
scopeChain: [globalEC.VO]
}
test.[[Scope]] = [globalEC.VO]
testEC.AO: {
vTest: 20,
inner: func,
obj: { huge object } // 這個很巨大的物件還是會存在
}
var arr = [];
for (var i = 0; i < 5; i++) {
arr[i] = function () {
console.log(i);
};
}
arr[0](); // 呼叫 arr[0] 這個 function,會印出 5
當我呼叫 arr[0]()
時,我以為會輸出的是 0,但結果是輸出 5
原因為:
因為是在 global 宣告 var i = 0
,所以變數 i
就是一個全域變數(等同於是下面這樣寫)
var arr = [];
var i; // i 是全域變數
for (i = 0; i < 5; i++) {
arr[i] = function () {
console.log(i);
};
}
arr[0](); // 呼叫 arr[0] 這個 function,會印出 5
迴圈的每一圈都會建立一個 function,只是還沒執行:
// 當迴圈跑第一圈時
arr[0] = function () {
console.log(i);
};
// 當迴圈跑第二圈時
arr[1] = function () {
console.log(i);
};
... 以此類推
當執行到第 9 行 arr[0]()
時,就是執行 arr[0]
這個 function,那在 function 裡面的 console.log(i)
,要怎麼找到這個 i
呢?
因為在自己的 function scope 沒有 i
,所以會往上層的 scope(也就是 global scope)去找到這個 i
在執行第 9 行 arr[0]()
時,for 迴圈已經跑完了,這時候的全域變數 i
的值已經是 5 了,這就是為什麼最後 console.log(i)
的結果會是 5
利用閉包的原理,寫一個這樣的 function:
在 logN()
裡面傳入什麼值,就會印出什麼值
// 利用閉包的原理
function logN(n) {
return function () {
console.log(n);
};
}
const log2 = logN(2);
log2(); // 2
就可以把 for 迴圈裡面的程式碼改成 arr[i] = logN(i);
var arr = [];
var i; // i 是全域變數
for (i = 0; i < 5; i++) {
arr[i] = logN(i);
}
// 利用閉包的原理
function logN(n) {
return function () {
console.log(n);
};
}
arr[0](); // 呼叫 arr[0] 這個 function,會印出 0
因為 logN(i)
會回傳一個新的 function,所以就會有一個新的作用域可以去記住當時的 i
的值
因為 i
是 logN(i)
所傳進去的參數,所以這個 i
會被記在 function logN
的 Activation Object 裡面
改成這樣之後,就會是我想要的結果了:
執行 arr[0]()
就輸出 0,執行 arr[1]()
就輸出 1
一般我要呼叫一個 function,就是用 test()
這種方式
function test() {
console.log("hello");
}
test(); // 呼叫 function test
那要怎麼呼叫一個匿名的 function 呢?
在 JS 裡面,如果要呼叫一個「匿名的 function」,可以用 IIFE (immediately-invoked function expression)這個方法,中文翻作「立即呼叫函式表達式」,範例如下:
把 function 用一個小括號包起來,後面再加上一個小括號(裡面可以傳參數進去)
(function (num) {
console.log(num);
})(123); // 立即呼叫函式,會輸出 123
用 IIFE 也是可以呼叫一個有名字的函式:
(function test() {
console.log("hello");
})(); // 立即呼叫函式,會輸出 hello
所以上面的 for 迴圈範例,就可以改成這樣:
改成用 IIFE 的好處是「不用再額外宣告一個 logN
函式」
var arr = [];
var i; // i 是全域變數
for (i = 0; i < 5; i++) {
arr[i] = (function (number) {
return function () {
console.log(number);
};
})(i);
}
arr[1](); // 呼叫 arr[0] 這個 function,會印出 1
在 for 迴圈裡面,改為用 let
宣告 i
,就可以了
var arr = [];
// 改為用 let 宣告 i
for (let i = 0; i < 5; i++) {
arr[i] = function () {
console.log(i);
};
}
arr[1](); // 呼叫 arr[0] 這個 function,會印出 1
arr[3](); // 呼叫 arr[0] 這個 function,會印出 3
let
的表現不太一樣用 let
宣告的變數,作用域會是一個 block
所以,每一圈的 i
,就會跟 arr[i]
的 i
有相同的值
// 迴圈第 1 圈
{
let i = 0;
arr[0] = function() {
console.log(i)
}
}
// 迴圈第 2 圈
{
let i = 1;
arr[1] = function() {
console.log(i)
}
}
// 迴圈第 3 圈
{
let i = 2;
arr[2] = function() {
console.log(i)
}
}
... 以此類推
舉一個例子:
雖然我寫了兩個 function 去操控 money
但是,我會碰到的問題是:如果有其他同事跟我協作,他完全可以不透過這兩個 function,就任意把 money
的值改掉,這是我完全沒辦法預防的
var money = 99;
function add(num) {
money += num;
}
function deduct(num) {
if (num >= 10) {
money -= 10;
} else {
money -= num;
}
}
add(1); // money = 100
deduct(100); // money = 90
money = -1; // 被同事亂改成 -1
console.log(money); // -1
這時,就可以用閉包來解決這個問題:
function createWallet(initMoney) {
var money = initMoney;
return {
add: function (num) {
money += num;
},
deduct: function (num) {
if (num >= 10) {
money -= 10;
} else {
money -= num;
}
},
getMoney: function () {
return money;
},
};
}
let myWallet = createWallet(99);
myWallet.add(1);
myWallet.deduct(100);
console.log(myWallet.getMoney()); // 90
改成用這樣寫之後,其他人就沒辦法用什麼 myWallet.money = -1;
來把我的 money
亂改值了
money
的值,只能使用 createWallet
回傳回來的這些 function 來操控我傳進去的 money
的值下面這樣寫,程式會出錯
console.log(b); // b is not defined
但是,改成下面這樣後,程式就可以正常執行了
console.log(b); // undefined
var b = 10;
上面的程式碼,可以想成是這樣:把「變數的宣告」提升到第一行
注意,只有「宣告」會 hoisting,「賦值」不會 hoisting
var b;
console.log(b);
b = 10;
test();
function test() {
console.log(123); // 123
}
上面的程式碼,可以想成是這樣:把「function 的宣告」提升到第一行
function test() {
console.log(123); // 123
}
test();
注意,下面這樣寫,程式碼會出現錯誤:test is not a function
test();
var test = function () {
console.log(123);
};
原因為:
只有「變數的宣告」會 hoisting,「變數的賦值」不會 hoisting
所以,上面的程式碼可以看成是這樣:
在呼叫 test()
時,test
是 undefined,並不是一個 function,所以會出錯(test is not a function)
var test; // 變數的宣告
test(); // 這時的 test 是 undefined
test = function () { // 變數的賦值
console.log(123);
};
因為 hoisting 是跟變數有關的,所以 hoisting 只會發生在「變數自己的 scope 裡面」
舉例:
var a = "global";
function test() {
console.log(a); // undefined
var a = "local";
}
test();
console.log(a)
會印出 undefined 是因為:
在 test scope 裡面,var a = "local"
這行會有 hoisting,可以看成是這樣:
var a = "global";
function test() {
var a; // 「變數的宣告」會 hoisting
console.log(a); // undefined
a = "local"; // 「變數的賦值」不會 hoisting
}
test();
同時宣告了一個 function a
和變數 a
,這時,function 會有 hoisting 的優先權
function test() {
console.log(a); // [Function: a]
function a() {}
var a = "local";
}
test();
上面這段程式碼可以看成是這樣:
function test() {
function a() {} // function 會有 hoisting 的優先權
console.log(a); // [Function: a]
a = "local";
}
test();
function test() {
console.log(a); // [Function: a]
a(); // 2
function a() {
console.log(1);
}
function a() {
console.log(2);
}
var a = "local";
}
test();
下面的 console.log(a)
會是 123,不會是 undefined
function test(a) {
console.log(a); // 123
var a = 456;
}
test(123);
就算是把上面的程式碼看成這樣:
就算 var a = 456
有 hoisting,但是如果 a
沒有重新賦值,a
的值就會是我傳進去的參數 123
下面的 var a
這句的意思只是「我要宣告一個變數 a
」,但是因為在 test scope 裡面已經存在變數 a
了,所以 var a
這句就會被忽略(並不會把 a
的值變成 undefined)
function test(a) {
var a;
console.log(a); // 123
a = 456;
console.log(a); // 456
}
test(123);
再舉另一個例子:
var a
這句會被忽略,因為在 test scope 本來就已經存在變數 a
了
function test(a) {
var a = "test";
console.log(a); // test
var a; // 我要宣告一個變數 a (會被忽略,因為本來就已經有變數 a 了)
}
test();
function test(a) {
console.log(a); // [Function: a]
function a() {}
}
test(123);
Execution Contexts(簡稱 EC)就是「執行環境」
請問 log 出來的值依序會是什麼?
var a = 1;
function test() {
console.log("1.", a); // undefined
var a = 7;
console.log("2.", a); // 7
a++; // a = 8
var a; // 忽略
inner();
console.log("4.", a); // 30
function inner() {
console.log("3.", a); // 8
a = 30; // 這個 a 是「存活在 test scope 的區域變數」
b = 200; // b 變成全域變數了
}
}
test();
console.log("5.", a); // 1 (這個 a 是全域變數的 a)
a = 70;
console.log("6.", a); // 70
console.log("7.", b); // 200
output:
1. undefined
2. 7
3. 8
4. 30
5. 1
6. 70
7. 200
var
宣告變數因為 hoisting,console.log(a)
會是 undefined
console.log(a); // undefined
var a = 10;
let
和 const
宣告變數console.log(a)
都會出錯,顯示「ReferenceError: Cannot access 'a' before initialization」
console.log(a);
let a = 10;
console.log(a);
const a = 10;
let
和 const
是有 hoisting 的因為如果沒有 hoisting 的話,那在 function test
裡面的 a
,應該會往上層找到 let a = 10
,但是 console.log(a)
的結果並不是 10,是出現了錯誤「ReferenceError: Cannot access ‘a’ before initialization」
所以 let
和 const
是有 hoisting 的,只是 hoisting 的方式跟 var
不同(底層的運作方式不同)
let a = 10;
function test() {
console.log(a); // Cannot access 'a' before initialization
let a = 30;
}
test();
let
和 const
的 hoisting 運作方式,請接著往下看
let
和 const
的 hoisting 運作方式:不能在「賦值」之前,去存取變數上面那段程式碼,的確是可以看成下面這樣(hoisting):
如果第三行是寫 var a;
,a
會被初始化成 undefined,所以 console.log(a)
就會是 undefined
但是,如果是 let a;
或是 const a
,變數都不會被初始化成 undefined
a = 30;
之前」,我都不能去存取 a
以下面範例來說,Temporal Dead Zone 就是從「第三行開始到第五行」之間
let a = 10;
function test() {
let a;
console.log(a);
a = 30;
}
test();
]]>下面這樣寫,程式會出錯
console.log(b); // b is not defined
但是,改成下面這樣後,程式就可以正常執行了
console.log(b); // undefined
var b = 10;
上面的程式碼,可以想成是這樣:把「變數的宣告」提升到第一行
注意,只有「宣告」會 hoisting,「賦值」不會 hoisting
var b;
console.log(b);
b = 10;
test();
function test() {
console.log(123); // 123
}
上面的程式碼,可以想成是這樣:把「function 的宣告」提升到第一行
function test() {
console.log(123); // 123
}
test();
注意,下面這樣寫,程式碼會出現錯誤:test is not a function
test();
var test = function () {
console.log(123);
};
原因為:
只有「變數的宣告」會 hoisting,「變數的賦值」不會 hoisting
所以,上面的程式碼可以看成是這樣:
在呼叫 test()
時,test
是 undefined,並不是一個 function,所以會出錯(test is not a function)
var test; // 變數的宣告
test(); // 這時的 test 是 undefined
test = function () { // 變數的賦值
console.log(123);
};
因為 hoisting 是跟變數有關的,所以 hoisting 只會發生在「變數自己的 scope 裡面」
舉例:
var a = "global";
function test() {
console.log(a); // undefined
var a = "local";
}
test();
console.log(a)
會印出 undefined 是因為:
在 test scope 裡面,var a = "local"
這行會有 hoisting,可以看成是這樣:
var a = "global";
function test() {
var a; // 「變數的宣告」會 hoisting
console.log(a); // undefined
a = "local"; // 「變數的賦值」不會 hoisting
}
test();
同時宣告了一個 function a
和變數 a
,這時,function 會有 hoisting 的優先權
function test() {
console.log(a); // [Function: a]
function a() {}
var a = "local";
}
test();
上面這段程式碼可以看成是這樣:
function test() {
function a() {} // function 會有 hoisting 的優先權
console.log(a); // [Function: a]
a = "local";
}
test();
function test() {
console.log(a); // [Function: a]
a(); // 2
function a() {
console.log(1);
}
function a() {
console.log(2);
}
var a = "local";
}
test();
下面的 console.log(a)
會是 123,不會是 undefined
function test(a) {
console.log(a); // 123
var a = 456;
}
test(123);
就算是把上面的程式碼看成這樣:
就算 var a = 456
有 hoisting,但是如果 a
沒有重新賦值,a
的值就會是我傳進去的參數 123
下面的 var a
這句的意思只是「我要宣告一個變數 a
」,但是因為在 test scope 裡面已經存在變數 a
了,所以 var a
這句就會被忽略(並不會把 a
的值變成 undefined)
function test(a) {
var a;
console.log(a); // 123
a = 456;
console.log(a); // 456
}
test(123);
再舉另一個例子:
var a
這句會被忽略,因為在 test scope 本來就已經存在變數 a
了
function test(a) {
var a = "test";
console.log(a); // test
var a; // 我要宣告一個變數 a (會被忽略,因為本來就已經有變數 a 了)
}
test();
function test(a) {
console.log(a); // [Function: a]
function a() {}
}
test(123);
Execution Contexts(簡稱 EC)就是「執行環境」
請問 log 出來的值依序會是什麼?
var a = 1;
function test() {
console.log("1.", a); // undefined
var a = 7;
console.log("2.", a); // 7
a++; // a = 8
var a; // 忽略
inner();
console.log("4.", a); // 30
function inner() {
console.log("3.", a); // 8
a = 30; // 這個 a 是「存活在 test scope 的區域變數」
b = 200; // b 變成全域變數了
}
}
test();
console.log("5.", a); // 1 (這個 a 是全域變數的 a)
a = 70;
console.log("6.", a); // 70
console.log("7.", b); // 200
output:
1. undefined
2. 7
3. 8
4. 30
5. 1
6. 70
7. 200
var
宣告變數因為 hoisting,console.log(a)
會是 undefined
console.log(a); // undefined
var a = 10;
let
和 const
宣告變數console.log(a)
都會出錯,顯示「ReferenceError: Cannot access 'a' before initialization」
console.log(a);
let a = 10;
console.log(a);
const a = 10;
let
和 const
是有 hoisting 的因為如果沒有 hoisting 的話,那在 function test
裡面的 a
,應該會往上層找到 let a = 10
,但是 console.log(a)
的結果並不是 10,是出現了錯誤「ReferenceError: Cannot access ‘a’ before initialization」
所以 let
和 const
是有 hoisting 的,只是 hoisting 的方式跟 var
不同(底層的運作方式不同)
let a = 10;
function test() {
console.log(a); // Cannot access 'a' before initialization
let a = 30;
}
test();
let
和 const
的 hoisting 運作方式,請接著往下看
let
和 const
的 hoisting 運作方式:不能在「賦值」之前,去存取變數上面那段程式碼,的確是可以看成下面這樣(hoisting):
如果第三行是寫 var a;
,a
會被初始化成 undefined,所以 console.log(a)
就會是 undefined
但是,如果是 let a;
或是 const a
,變數都不會被初始化成 undefined
a = 30;
之前」,我都不能去存取 a
以下面範例來說,Temporal Dead Zone 就是從「第三行開始到第五行」之間
let a = 10;
function test() {
let a;
console.log(a);
a = 30;
}
test();
]]>
在 JavaScript 裡面,共有七種資料型態
前六種稱為 primitive type
primitive type(原始型態)
除了 primitive type 之外,其他的都是 object(物件)
typeof
來得知變數的資料型態是什麼但是,用 typeof
並不能保證最後出來的結果會是這七種型態(這是一個滿混淆人的地方),typeof
有一個表格會列出每一種變數出來的結果是什麼
例如:
typeof
對 function,出來的結果會是 functionconsole.log(typeof function () {});
null
的資料型態是 null,但是如果用 typeof
對 null,出來的結果會是 objectconsole.log(typeof null);
typeof null
會是 object 的原因官方文件 typeof null
在 JS 底層的實作裡面,有一個東西叫做 type tag(用來標記一個變數是什麼 type)
給 object 的 type tag 是 0
null
會被表示成一個 NULL pointer,type tag 是 0x00
用 typeof
去檢視 null
時,type tag 0x00
就被當成是 0
,因此會回傳 object
typeof
來檢查:一個變數是否有被宣告、且被賦予值如果 a
都沒有被宣告和賦予值 ,typeof a
結果會是 undefined
console.log(typeof a); // undefined
所以,有時會用下面這樣來檢查:一個變數是否有被宣告、且被賦予值
if (typeof a !== "undefined") {
console.log(a);
}
typeof
來檢查,不能直接寫 if (a !== "undefined")
嗎?如果是直接寫 if (a !== "undefined")
a
有被賦予值時,是 ok 的,沒有問題var a = 10;
if (a !== "undefined") {
console.log(a);
}
// output: 10
a
沒有被宣告和賦予值時,程式就會整個出錯,會跳出「ReferenceError: a is not defined」,造成程式無法執行下去if (a !== "undefined") {
console.log(a);
}
Array.isArray()
判斷是否為 array因為 typeof []
結果會是 object
如果想要知道一個變數是不是 array 的話,可以用 Array.isArray()
注意:在比較舊的瀏覽器,沒有 Array.isArray()
這個方法可以用
Object.prototype.toString.call()
可以準確判斷出型態第一個參數傳:我要檢測的東西
console.log(Object.prototype.toString.call("hello")); // [object String]
primitive type 和 object 最大的差別就是:primitive type 是 immutable(不能改變的)
下面這樣不叫做「改變」,叫做「重新賦值」
var a = 10;
a = 20;
str.toUpperCase()
會回傳一個新的字串,而不是去改變 str
自己
所以 console.log(str)
的結果並不會變成大寫
var str = "hello";
str.toUpperCase();
console.log(str); // output: hello
所以,必須要用一個新的變數 newStr
去接收新的字串:
var str = "hello";
var newStr = str.toUpperCase();
console.log(str, newStr);
// output: hello HELLO
arr.push(2)
可以改變原本的 arr
var arr = [1];
arr.push(2);
console.log(arr);
// output: [ 1, 2 ]
primitive type 和 object,對於「賦值」的反應是不一樣的
var a = 10;
var b = a;
console.log(a, b); // 10 10
b = 200;
console.log(a, b); // 10 200
var obj = {
number: 10,
};
var obj2 = obj;
console.log(obj, obj2); // { number: 10 } { number: 10 }
obj2.number = 20;
console.log(obj, obj2); // { number: 20 } { number: 20 }
把 obj2
的 number 改成 20,但是發現 obj
和 obj2
都變成 { number: 20 }
了
宣告變數 a
和 b
:
把 a
的值(10)複製給 b
var a = 10;
var b = a;
在記憶體裡面,可以想成就是這樣存的:
a: 10
b: 10
當我把 b
的值改成 20:
var a = 10;
var b = a;
b = 20;
在記憶體裡面就會把 b
的值改成 20:
a: 10
b: 20
當我宣告變數 obj
是 { number: 10 }
這個物件時,{ number: 10 }
會被放到某個記憶體位置上(例如 0x01
這個位置),在變數 obj
裡面,存的會是 0x01
這個記憶體位置
當我寫 var obj2 = obj
時,就是把 obj
的值(0x01
)複製給 obj2
,所以 obj
和 obj2
裡面存的都是 0x01
,是指向同一個記憶體位置(是同一個物件)
// 在 0x01 這個記憶體位置裡面
0x01: {
number: 10
}
// 變數裡面存的東西
obj: 0x01
obj2: 0x01
var obj = {
number: 10,
};
var obj2 = obj; // 把 obj 的值複製給 obj2
console.log(obj, obj2); // { number: 10 } { number: 10 }
obj2.number = 20;
console.log(obj, obj2); // { number: 20 } { number: 20 }
obj2.number = 20
時,意思是:我要去改變「obj2
存的記憶體位置 0x01
裡面的 object 底下的 number
屬性,把 value 改成 20」所以, obj
和 obj2
都會是 { number: 20 }
// 在 0x01 這個記憶體位置裡面
0x01: {
number: 20
}
=
重新賦值,就會是另一個新的記憶體位置了舉例來說
當我宣告 arr = []
時,背後做的事情是:
[]
一個記憶體位置 0x10
0x10
存到變數 arr
裡面當我寫 var arr2 = arr
時,就是把 arr
的值複製給 arr2
,所以 arr
和 arr2
裡面存的都是 0x10
,是指向同一個記憶體位置(是同一個陣列)
// 在 0x10 這個記憶體位置裡面
0x10: []
// 變數裡面存的東西
arr: 0x10
arr2: 0x10
var arr = [];
var arr2 = arr;
console.log(arr, arr2); // [], []
但是,當我寫 arr2 = ["arr2"]
時,因為是用 =
賦值,所以背後做的一樣會是:
['arr2']
一個新的記憶體位置 0x20
arr2
裡面存的東西,改成 0x20
這個新的記憶體位置這時,arr
和 arr2
已經指向不同的記憶體位置了,因此不會互相影響
// 在 0x10 這個記憶體位置裡面
0x10: []
0x20: ['arr2']
// 變數裡面存的東西
arr: 0x10
arr2: 0x20
var arr = [];
var arr2 = arr;
console.log(arr, arr2); // [], []
arr2 = ["arr2"];
console.log(arr, arr2); // [], ['arr2']
在 JS,當我對 object 用 =
賦值時,做的事情是:
if
裡面寫了 =
var a = 10;
if ((a = 20)) {
console.log(123);
}
// output: 123
在 if
裡面應該要用 ==
或是 ===
才對
如果不小心在 if
裡面寫了 =
,是重新賦值的意思,if ((a = 20))
的執行流程就會變成是:
a = 20
a
是否為 truea = 20;
if (a) {
console.log(123);
}
因此,會印出 123
==
會按照一個規則去轉換型態,===
不會轉換型態==
會轉換型態,所以 console.log(2 == '2')
是 true
===
不會轉換型態,所以 console.log(2 === '2')
是 false
===
比較兩個 primitive type 時,比較的是「兩個變數的值」===
比較兩個 object 時,只有在「兩個 object 指向同一個記憶體位置」時,===
才會是 trueconsole.log([] === []); // 兩個陣列是在「不同的記憶體位置」,所以是 false
console.log({} === {}); // 兩個物件是在「不同的記憶體位置」,所以是 false
因為 object 裡面存的是記憶體位置,所以 obj === obj2
比較的是「變數裡面存的記憶體位置」,因為記憶體位置都是 0x01
,所以會是 true
// 在 0x01 這個記憶體位置裡面
0x01: {
number: 1,
};
// 在 object 裡面存的是記憶體位置
obj: 0x01
obj2: 0x01
var obj = {
number: 1,
};
var obj2 = obj;
obj2.number = 2;
console.log(obj === obj2); // true
因為 arr
和 arr2
裡面存的記憶體位置不同,所以儘管陣列的值一樣,arr === arr2
還是 false
// 在記憶體位置裡面
0x01: [1];
0x02: [1];
// 在 object 裡面存的是記憶體位置
arr: 0x01
arr2: 0x02
var arr = [1];
var arr2 = [1];
console.log(arr === arr2); // false
NaN
當我用 Number()
把一個不是數字的東西要轉成數字時,a
就會是 NaN
==
和 ===
時的唯一特殊案例:NaN
不會等於任何東西(甚至是連自己本身都不等於)因為一個「不是數字的東西」無法跟另一個不是數字的東西做比較
var a = Number("hello");
console.log(a); // NaN
console.log(a === a); // false
console.log(NaN === NaN); // false
isNaN()
這個 function 來檢視是不是 NaN
下面這樣寫會跳出錯誤「SyntaxError: Missing initializer in const declaration」
因為用 const
宣告變數時,一開始就要給初始值
const b;
b = 20;
Scope(作用域),就是「變數的生存範圍」
範例一:
在上面的例子中,function test()
產生了一個 test scope,只有在這個 test scope 裡面,變數 a
才會被看到
變數 a
是區域變數
function test() {
var a = 10;
console.log(a); // 10
}
test();
console.log(a); // a is not defined
在最外層宣告的,叫做 global variable(全域變數)
全域變數的作用域是以「檔案」為單位,意思是:一旦宣告後,整個檔案的任何位置都看得到這個變數
變數 a
是全域變數
作用域是「往上層找」
console.log(a)
,JS 的引擎會先在 function test()
的作用域裡面找「是否有宣告變數 a
」,沒有找到的話,就會繼續往上層找,就找到了 global 這層的變數 a
var a = 20;
function test() {
console.log(a); // 20
}
test();
console.log(a); // 20
範例一:
function test
自己就有宣告變數 a
了,所以在 test scope 裡面用的 a
是「只存活在 test scope 的區域變數」,跟外面的全域變數 a
沒有任何關係第五行把區域變數的 a
的值改成 30,並不會影響到全域變數的 a
var a = 1;
function test() {
var a = 8; // 這個 a 是在 test scope 裡面的區域變數
console.log(a); // 8
a = 30; // 改變區域變數的 a 的值,並不會影響到全域變數的 a
console.log(a); // 30
}
test();
console.log(a); // 1 (這是全域變數的 a)
範例二:
function test
裡面沒有宣告過 a
,所以第三行的 a = 8
會往上層找到全域變數 a
,把全域變數 a
的值改成 8在 test scope 裡面的 a
就是「全域變數的 a
」
var a = 1;
function test() {
a = 8; // 這個 a 會是全域變數的 a,把值改成 8
console.log(a); // 8
}
test();
console.log(a); // 8 (全域變數 a 的值已經變成 8 了)
範例三:
第四行的 console.log(a)
,會先在自己的作用域(test scope)裡面找,所以會找到 var a = 10
第八行的 console.log(a)
,會先在自己的作用域(global)裡面找,所以會找到 var a = 20
var a = 20;
function test() {
var a = 10;
console.log(a); // 10 (這個 a 是在 test scope 的區域變數)
}
test();
console.log(a); // 20 (這個 a 是在 global scope 的全域變數)
如果 a = 10
在前面都沒有被宣告過,就會自動在 global 幫我宣告一個變數 a
注意,下面這樣是不好的寫法,因為原本只想在 function 裡面宣告的變數,變成全域變數了(全域變數太多,可能會跟其他變數衝突到)
function test() {
a = 10;
console.log(a); // 10
}
test();
console.log(a); // 10
就等同於是這樣寫:
var a; // 在 global 幫我宣告一個變數 a
function test() {
a = 10;
console.log(a); // 10
}
test();
console.log(a); // 10
下面範例的 scope chain(作用域鏈)就是:inner scope -> test scope -> global scope
會從自己的作用域開始找,找不到的話就一直往上一層找
var a = "global"; // global scope
function test() {
// test scope
var a = "test scope a";
var b = "test scope b";
console.log(a, b); // test scope a test scope b
function inner() {
// inner scope
var b = "inner scope b";
console.log(a, b); // test scope a inner scope b
}
inner();
}
test();
console.log(a); // global
因為我在 function change
裡面呼叫 test()
,所以在 function test
裡面的 a
會去找 function change
裡面宣告的 a
下面這是錯誤的 scope chain
test scope -> change scope -> global scope
下面這是正確的 scope chain(永遠不會改變)
change scope -> global scope
test scope -> global scope
所以,下面 console.log(a)
會去找「在 global scope 的 a
」
var a = "global"; // global scope
function change() {
// change scope 的上一層就是 global scope
var a = 10;
test();
}
function test() {
// test scope 的上一層就是 global scope
console.log(a); // global
}
change();
注意,在 function change
裡面呼叫 test()
,並不等於下面這樣:
下面這樣叫做「在 function change
裡面宣告 function test
」,這時,test scope 的上一層就會是 change scope 了
function change() {
// change scope
var a = 10;
function test() {
console.log(a);
}
}
如果把 function test
放在 function change
裡面宣告,那不管我在哪裡呼叫 test()
,function test
的 scope chain 都會是:test -> change -> global
var a = "global"; // global scope
function change() {
// change scope
var a = "change";
function inner() {
var a = "inner";
test();
}
function test() {
// scope chain 會是:test -> change -> global
console.log(a);
}
inner();
}
change();
var
宣告的變數,作用域的範圍是「一個 function」例如:
在 function test
裡面,都是變數 b
的作用域
function test() {
var a = 60;
if (a === 60) {
var b = 10; // b 的作用域是「一整個 function test」
}
console.log(b); // 10
}
test();
let
或 const
宣告的變數,作用域的範圍是「一個 block」例如:
function test() {
var a = 60;
if (a === 60) {
let b = 10; // b 的作用域只有在「if 這個 block」
}
console.log(b); // b is not defined
}
test();
]]>在 JavaScript 裡面,共有七種資料型態
前六種稱為 primitive type
primitive type(原始型態)
除了 primitive type 之外,其他的都是 object(物件)
typeof
來得知變數的資料型態是什麼但是,用 typeof
並不能保證最後出來的結果會是這七種型態(這是一個滿混淆人的地方),typeof
有一個表格會列出每一種變數出來的結果是什麼
例如:
typeof
對 function,出來的結果會是 functionconsole.log(typeof function () {});
null
的資料型態是 null,但是如果用 typeof
對 null,出來的結果會是 objectconsole.log(typeof null);
typeof null
會是 object 的原因官方文件 typeof null
在 JS 底層的實作裡面,有一個東西叫做 type tag(用來標記一個變數是什麼 type)
給 object 的 type tag 是 0
null
會被表示成一個 NULL pointer,type tag 是 0x00
用 typeof
去檢視 null
時,type tag 0x00
就被當成是 0
,因此會回傳 object
typeof
來檢查:一個變數是否有被宣告、且被賦予值如果 a
都沒有被宣告和賦予值 ,typeof a
結果會是 undefined
console.log(typeof a); // undefined
所以,有時會用下面這樣來檢查:一個變數是否有被宣告、且被賦予值
if (typeof a !== "undefined") {
console.log(a);
}
typeof
來檢查,不能直接寫 if (a !== "undefined")
嗎?如果是直接寫 if (a !== "undefined")
a
有被賦予值時,是 ok 的,沒有問題var a = 10;
if (a !== "undefined") {
console.log(a);
}
// output: 10
a
沒有被宣告和賦予值時,程式就會整個出錯,會跳出「ReferenceError: a is not defined」,造成程式無法執行下去if (a !== "undefined") {
console.log(a);
}
Array.isArray()
判斷是否為 array因為 typeof []
結果會是 object
如果想要知道一個變數是不是 array 的話,可以用 Array.isArray()
注意:在比較舊的瀏覽器,沒有 Array.isArray()
這個方法可以用
Object.prototype.toString.call()
可以準確判斷出型態第一個參數傳:我要檢測的東西
console.log(Object.prototype.toString.call("hello")); // [object String]
primitive type 和 object 最大的差別就是:primitive type 是 immutable(不能改變的)
下面這樣不叫做「改變」,叫做「重新賦值」
var a = 10;
a = 20;
str.toUpperCase()
會回傳一個新的字串,而不是去改變 str
自己
所以 console.log(str)
的結果並不會變成大寫
var str = "hello";
str.toUpperCase();
console.log(str); // output: hello
所以,必須要用一個新的變數 newStr
去接收新的字串:
var str = "hello";
var newStr = str.toUpperCase();
console.log(str, newStr);
// output: hello HELLO
arr.push(2)
可以改變原本的 arr
var arr = [1];
arr.push(2);
console.log(arr);
// output: [ 1, 2 ]
primitive type 和 object,對於「賦值」的反應是不一樣的
var a = 10;
var b = a;
console.log(a, b); // 10 10
b = 200;
console.log(a, b); // 10 200
var obj = {
number: 10,
};
var obj2 = obj;
console.log(obj, obj2); // { number: 10 } { number: 10 }
obj2.number = 20;
console.log(obj, obj2); // { number: 20 } { number: 20 }
把 obj2
的 number 改成 20,但是發現 obj
和 obj2
都變成 { number: 20 }
了
宣告變數 a
和 b
:
把 a
的值(10)複製給 b
var a = 10;
var b = a;
在記憶體裡面,可以想成就是這樣存的:
a: 10
b: 10
當我把 b
的值改成 20:
var a = 10;
var b = a;
b = 20;
在記憶體裡面就會把 b
的值改成 20:
a: 10
b: 20
當我宣告變數 obj
是 { number: 10 }
這個物件時,{ number: 10 }
會被放到某個記憶體位置上(例如 0x01
這個位置),在變數 obj
裡面,存的會是 0x01
這個記憶體位置
當我寫 var obj2 = obj
時,就是把 obj
的值(0x01
)複製給 obj2
,所以 obj
和 obj2
裡面存的都是 0x01
,是指向同一個記憶體位置(是同一個物件)
// 在 0x01 這個記憶體位置裡面
0x01: {
number: 10
}
// 變數裡面存的東西
obj: 0x01
obj2: 0x01
var obj = {
number: 10,
};
var obj2 = obj; // 把 obj 的值複製給 obj2
console.log(obj, obj2); // { number: 10 } { number: 10 }
obj2.number = 20;
console.log(obj, obj2); // { number: 20 } { number: 20 }
obj2.number = 20
時,意思是:我要去改變「obj2
存的記憶體位置 0x01
裡面的 object 底下的 number
屬性,把 value 改成 20」所以, obj
和 obj2
都會是 { number: 20 }
// 在 0x01 這個記憶體位置裡面
0x01: {
number: 20
}
=
重新賦值,就會是另一個新的記憶體位置了舉例來說
當我宣告 arr = []
時,背後做的事情是:
[]
一個記憶體位置 0x10
0x10
存到變數 arr
裡面當我寫 var arr2 = arr
時,就是把 arr
的值複製給 arr2
,所以 arr
和 arr2
裡面存的都是 0x10
,是指向同一個記憶體位置(是同一個陣列)
// 在 0x10 這個記憶體位置裡面
0x10: []
// 變數裡面存的東西
arr: 0x10
arr2: 0x10
var arr = [];
var arr2 = arr;
console.log(arr, arr2); // [], []
但是,當我寫 arr2 = ["arr2"]
時,因為是用 =
賦值,所以背後做的一樣會是:
['arr2']
一個新的記憶體位置 0x20
arr2
裡面存的東西,改成 0x20
這個新的記憶體位置這時,arr
和 arr2
已經指向不同的記憶體位置了,因此不會互相影響
// 在 0x10 這個記憶體位置裡面
0x10: []
0x20: ['arr2']
// 變數裡面存的東西
arr: 0x10
arr2: 0x20
var arr = [];
var arr2 = arr;
console.log(arr, arr2); // [], []
arr2 = ["arr2"];
console.log(arr, arr2); // [], ['arr2']
在 JS,當我對 object 用 =
賦值時,做的事情是:
if
裡面寫了 =
var a = 10;
if ((a = 20)) {
console.log(123);
}
// output: 123
在 if
裡面應該要用 ==
或是 ===
才對
如果不小心在 if
裡面寫了 =
,是重新賦值的意思,if ((a = 20))
的執行流程就會變成是:
a = 20
a
是否為 truea = 20;
if (a) {
console.log(123);
}
因此,會印出 123
==
會按照一個規則去轉換型態,===
不會轉換型態==
會轉換型態,所以 console.log(2 == '2')
是 true
===
不會轉換型態,所以 console.log(2 === '2')
是 false
===
比較兩個 primitive type 時,比較的是「兩個變數的值」===
比較兩個 object 時,只有在「兩個 object 指向同一個記憶體位置」時,===
才會是 trueconsole.log([] === []); // 兩個陣列是在「不同的記憶體位置」,所以是 false
console.log({} === {}); // 兩個物件是在「不同的記憶體位置」,所以是 false
因為 object 裡面存的是記憶體位置,所以 obj === obj2
比較的是「變數裡面存的記憶體位置」,因為記憶體位置都是 0x01
,所以會是 true
// 在 0x01 這個記憶體位置裡面
0x01: {
number: 1,
};
// 在 object 裡面存的是記憶體位置
obj: 0x01
obj2: 0x01
var obj = {
number: 1,
};
var obj2 = obj;
obj2.number = 2;
console.log(obj === obj2); // true
因為 arr
和 arr2
裡面存的記憶體位置不同,所以儘管陣列的值一樣,arr === arr2
還是 false
// 在記憶體位置裡面
0x01: [1];
0x02: [1];
// 在 object 裡面存的是記憶體位置
arr: 0x01
arr2: 0x02
var arr = [1];
var arr2 = [1];
console.log(arr === arr2); // false
NaN
當我用 Number()
把一個不是數字的東西要轉成數字時,a
就會是 NaN
==
和 ===
時的唯一特殊案例:NaN
不會等於任何東西(甚至是連自己本身都不等於)因為一個「不是數字的東西」無法跟另一個不是數字的東西做比較
var a = Number("hello");
console.log(a); // NaN
console.log(a === a); // false
console.log(NaN === NaN); // false
isNaN()
這個 function 來檢視是不是 NaN
下面這樣寫會跳出錯誤「SyntaxError: Missing initializer in const declaration」
因為用 const
宣告變數時,一開始就要給初始值
const b;
b = 20;
Scope(作用域),就是「變數的生存範圍」
範例一:
在上面的例子中,function test()
產生了一個 test scope,只有在這個 test scope 裡面,變數 a
才會被看到
變數 a
是區域變數
function test() {
var a = 10;
console.log(a); // 10
}
test();
console.log(a); // a is not defined
在最外層宣告的,叫做 global variable(全域變數)
全域變數的作用域是以「檔案」為單位,意思是:一旦宣告後,整個檔案的任何位置都看得到這個變數
變數 a
是全域變數
作用域是「往上層找」
console.log(a)
,JS 的引擎會先在 function test()
的作用域裡面找「是否有宣告變數 a
」,沒有找到的話,就會繼續往上層找,就找到了 global 這層的變數 a
var a = 20;
function test() {
console.log(a); // 20
}
test();
console.log(a); // 20
範例一:
function test
自己就有宣告變數 a
了,所以在 test scope 裡面用的 a
是「只存活在 test scope 的區域變數」,跟外面的全域變數 a
沒有任何關係第五行把區域變數的 a
的值改成 30,並不會影響到全域變數的 a
var a = 1;
function test() {
var a = 8; // 這個 a 是在 test scope 裡面的區域變數
console.log(a); // 8
a = 30; // 改變區域變數的 a 的值,並不會影響到全域變數的 a
console.log(a); // 30
}
test();
console.log(a); // 1 (這是全域變數的 a)
範例二:
function test
裡面沒有宣告過 a
,所以第三行的 a = 8
會往上層找到全域變數 a
,把全域變數 a
的值改成 8在 test scope 裡面的 a
就是「全域變數的 a
」
var a = 1;
function test() {
a = 8; // 這個 a 會是全域變數的 a,把值改成 8
console.log(a); // 8
}
test();
console.log(a); // 8 (全域變數 a 的值已經變成 8 了)
範例三:
第四行的 console.log(a)
,會先在自己的作用域(test scope)裡面找,所以會找到 var a = 10
第八行的 console.log(a)
,會先在自己的作用域(global)裡面找,所以會找到 var a = 20
var a = 20;
function test() {
var a = 10;
console.log(a); // 10 (這個 a 是在 test scope 的區域變數)
}
test();
console.log(a); // 20 (這個 a 是在 global scope 的全域變數)
如果 a = 10
在前面都沒有被宣告過,就會自動在 global 幫我宣告一個變數 a
注意,下面這樣是不好的寫法,因為原本只想在 function 裡面宣告的變數,變成全域變數了(全域變數太多,可能會跟其他變數衝突到)
function test() {
a = 10;
console.log(a); // 10
}
test();
console.log(a); // 10
就等同於是這樣寫:
var a; // 在 global 幫我宣告一個變數 a
function test() {
a = 10;
console.log(a); // 10
}
test();
console.log(a); // 10
下面範例的 scope chain(作用域鏈)就是:inner scope -> test scope -> global scope
會從自己的作用域開始找,找不到的話就一直往上一層找
var a = "global"; // global scope
function test() {
// test scope
var a = "test scope a";
var b = "test scope b";
console.log(a, b); // test scope a test scope b
function inner() {
// inner scope
var b = "inner scope b";
console.log(a, b); // test scope a inner scope b
}
inner();
}
test();
console.log(a); // global
因為我在 function change
裡面呼叫 test()
,所以在 function test
裡面的 a
會去找 function change
裡面宣告的 a
下面這是錯誤的 scope chain
test scope -> change scope -> global scope
下面這是正確的 scope chain(永遠不會改變)
change scope -> global scope
test scope -> global scope
所以,下面 console.log(a)
會去找「在 global scope 的 a
」
var a = "global"; // global scope
function change() {
// change scope 的上一層就是 global scope
var a = 10;
test();
}
function test() {
// test scope 的上一層就是 global scope
console.log(a); // global
}
change();
注意,在 function change
裡面呼叫 test()
,並不等於下面這樣:
下面這樣叫做「在 function change
裡面宣告 function test
」,這時,test scope 的上一層就會是 change scope 了
function change() {
// change scope
var a = 10;
function test() {
console.log(a);
}
}
如果把 function test
放在 function change
裡面宣告,那不管我在哪裡呼叫 test()
,function test
的 scope chain 都會是:test -> change -> global
var a = "global"; // global scope
function change() {
// change scope
var a = "change";
function inner() {
var a = "inner";
test();
}
function test() {
// scope chain 會是:test -> change -> global
console.log(a);
}
inner();
}
change();
var
宣告的變數,作用域的範圍是「一個 function」例如:
在 function test
裡面,都是變數 b
的作用域
function test() {
var a = 60;
if (a === 60) {
var b = 10; // b 的作用域是「一整個 function test」
}
console.log(b); // 10
}
test();
let
或 const
宣告的變數,作用域的範圍是「一個 block」例如:
function test() {
var a = 60;
if (a === 60) {
let b = 10; // b 的作用域只有在「if 這個 block」
}
console.log(b); // b is not defined
}
test();
]]>
module.exports = Utils
輸出模組const Utils = {
first: function(str) {
return str[0]
},
last: function(str) {
return str[str.length - 1]
}
}
module.exports = Utils
require()
引入 moduleconst utils = require('./utils')
來引入這個 module 即可const utils = require('./utils.js')
console.log(utils.first('abc'))
引入之後,輸入指令 node index.js
就會輸出 console.log(utils.first('abc'))
的結果(就是 a)
只有在 Node.js 上可以使用 CommonJS 這個模組化規範,也就是 module.exports
, require()
這些語法
在 Node.js 不會有命名衝突的問題,是因為在用 require()
引入 module (library) 的時候,我可以隨意用一個自己喜歡的變數名稱來引入
例如 const abc = require('./utils')
或是 const banana = require('./utils')
這些變數名稱(abc
, banana
)並不會影響到我要引入的 module (library)
可是,在瀏覽器上不能使用 require()
這個語法,因為瀏覽器不支援(瀏覽器沒有 require()
這個語法)
所以,如果瀏覽器想要引入一個 library,以 jQuery 為例,在瀏覽器上(index.html),我需要在 <header>
裡面加上這行 <script src="https://code.jquery.com/jquery-3.5.1.js"></script>
來把 jQuery 引入進來,然後我就可以有 $
或是 jQuery
這個全域變數(物件)可以使用
假設我在 index.html 同時引入了兩個 library,分別是 jQuery 和 lidemy
這兩個 library 都使用了 $
這個全域變數,那這時就會有命名衝突的問題
所以,在 jQuery 有提供了一個 function 叫做 jQuery.noConflict() 可以幫忙解決命名衝突的問題
(只要有用 global 全域變數來命名的 library 都會有命名衝突的問題)
為了要解決使用模組時的命名衝突問題,有很多人就實作了不同的「模組化規範」,像是:CommonJS, AMD, UMD (這幾個都不是官方的規範)
這些不同的規範就像是各家牌子的充電線,充電線的目的都一樣(讓手機充電),但是 Apple 手機和 Android 手機的充電線接頭長得都不一樣
這些規範的用法不一樣,不同規範之間也可能無法相容,但是目的都一樣
這些規範說明了我們要用什麼語法去使用模組、以及要怎麼把模組給別人用
直到後來 ES6 出現後,瀏覽器也支援了原生的模組化規範叫做 ES Modules,就可以使用 import
, export
這些語法
範例如下
在 utils.js 輸出 module:
export function first(str) {
return str[0]
}
在 index.js 引入 module:
import { first } from './utils.js'
console.log(first('abc'))
在 index.html 載入 index.js:
若是想要在瀏覽器上直接使用 import
與 export
,都必須以 module 的形式來執行,所以要在 script 標籤加上 type="module"
,這樣瀏覽器才看得懂
<script src="./index.js" type="module"></script>
但是會發現,當我用瀏覽器打開 index.html 時,會出現一個 CORS 的錯誤,原因為:沒有辦法直接用檔案的方式開啟,必須要在 local 開一個 server
因此,就輸入指令 python -m SimpleHTTPServer 8081
在 local 開一個 server 後,就可以在網址列輸入 localhost:8081 連到同樣的檔案位置(index.html),打開 console 就可以看到 console.log(first('abc'))
的結果(就是 a)
但要注意的是,上面所說的這個「瀏覽器的 module 規範」是新的語法(在近幾年才出來),舊的瀏覽器並不支援
因此在使用 module 規範之後,就可以用一些工具來幫我們做轉換,產生出來的檔案就可以丟到瀏覽器上面執行
即使我在 JS 裡面是使用 require()
的語法,做轉換之後一樣可以在瀏覽器上面執行
webpack 的功用就是:module bundler,把我的 modules(各種資源) 都包在一起,然後拿去做一些轉換,我就可以在瀏覽器上面也使用這些 modules
webpack 把「module」的概念擴充到不只是 JS 的模組,還包括圖片、CSS 檔案、聲音等資源,都可以當作 module(資源)打包在一起
首先,要用 npm 來安裝 webpack
輸入指令 npm init
來產生出一個 package.json 檔案(這個專案就會是一個 npm 的專案了)
按照 文件的 Basic Setup,輸入指令 npm install webpack webpack-cli --save-dev
來安裝 webpack
在實務上,開發時會把 source code 寫在一個 src 資料夾裡面,經過一些工具(例如 webpack, babel)轉換之後再放到別的地方去
因此,先建立一個 src 資料夾,裡面建立兩個檔案:utils.js 和 index.js
require()
的語法來引入 module在 utils.js 輸出 module:
const Utils = {
first: function(str) {
return str[0]
}
}
module.exports = Utils
在 index.js 引入 module:
const utils = require('./utils.js')
console.log(utils.first('abc'))
接下來,就是 webpack 要施展魔力的時刻了!
只要輸入指令 npx webpack
,就可以啟用 webpack 來幫我打包
webpack 預設的設定就是會去找「src 資料夾裡面的 index.js 檔案」來當作程式的入口點(入口點的意思就是:程式一開始就會去執行這個檔案)
webpack 會把打包完的東西放在 dist 資料夾(distribution 的簡寫,要發布出去的意思),在 dist 資料夾就會有一個 main.js 檔案(就是 webpack 打包出來的檔案)
打開 main.js 會看到是已經打包好並壓縮過的程式碼(webpack 會自動幫我做 minify, uglify)
這時,就試試看是否能夠執行
輸入指令 node dist/main.js
,有成功輸出結果 a
在 index.html 加上這行,引入 main.js 檔案:
<script src="dist/main.js"></script>
接著,用瀏覽器開啟 index.html,打開 console 就可以看到執行的結果了!(也就是 a)
原因為:
webpack 幫我們「在瀏覽器上實作了 require()
的功能」,然後再把 module 包在一起,讓我們在瀏覽器上也可以用 require()
的語法來使用模組
export
, import
語法來使用 moduleutils.js 輸出 module:
export function first(str) {
return str[0]
}
index.js 引入 module:
import $ from 'jquery' // 引入 jQuery
import { first } from './utils.js'
// 開始使用 jQuery
$(document).ready(() => {
$('.btn').click(function() {
alert(first('hello!'))
})
})
前面有提到,webpack 預設的設定就是會去找「src/index.js 檔案」來當作程式的入口點,如果我想要更改這個預設設定,就可以新建一個 webpack 的設定檔
webpack 的設定檔,預設的檔名叫做 webpack.config.js
設定檔的寫法可以參考官方文件 Using a Configuration
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
};
entry: './src/index.js'
就是程式的入口點,做為入口點的檔案會把 module 引入進來做一些事情(通常都會是 index.js),但如果就只有一個檔案的話,該檔案本身就會是入口點output
就是打包完的檔案,路徑會是 path.resolve(__dirname, 'dist')
,__dirname
就會去抓「我目前正在執行的資料夾 (__dirname
代表跟 config 檔同一個目錄)」,然後再放到底下的 dist 資料夾const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
};
在 package.json 裡面新增一行 "build": "webpack"
:
{
"name": "webpack-mtr04",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0"
}
}
這時,當我在 CLI 輸入指令 npm run build
時,就會幫我執行 build
的指令,也就是 webpack
(這麼做的目的只是讓我更方便去執行 webpack 而已)
因此,當我輸入指令 npm run build
時 > 就會執行 webpack
指令 > webpack 會去找設定檔(webpack.config.js) > 依據設定檔去做打包
webpack 除了可以打包我的程式碼,也可以打包我從 npm 安裝的程式碼
例如:
jQuery 也有一個 npm 的版本,我用 npm 的方式引入 jQuery
用 npm 的方式引入跟「用 <script>
的方式引入」是完全不同的
首先,輸入指令 npm install jquery --save-dev
來安裝 jQuery
安裝好之後,在 index.js 就可以引入 jQuery:
const $ = require('jquery') // 引入 jQuery
const utils = require('./utils.js')
console.log(utils.first('abc'))
// 開始使用 jQuery
$(document).ready(() => {
$('.btn').click(function() {
alert('hello!')
})
})
在 index.html 有一個 .btn
的按鈕:
<button class="btn">click me</button>
記得要再執行一次 npm run build
,webpack 才會再做一次打包
打包完後,點擊 .btn
就會出現 alert 了!
其實,用 npm 引入 library 一點都不神奇,它背後其實就是:
以引入 jQuery 為例
在專案資料夾底下,有一個 node modules 的檔案,打開後會找到裡面有一個 jquery 的資料夾 > 打開 jquery 資料夾裡面的 package.json 檔案,會看到有一個欄位是 main
:
"main": "dist/jquery.js",
main
的意思就是:當我用 require()
或 import
引入 jquery 時,就是使用「dist/jquery.js」這個檔案裡面的程式碼(是入口點)
所以,npm 就是透過這樣的方式來幫我引入 jQuery
有了模組化的好處是:
在前端開發時會有很多東西,我可以把不同的功能、library 都分散各個檔案(用不同的檔案去管理,結構可以切的比較明確),再用 webpack 幫我打包即可
以串接 api 為例
我不需要把串接 api 的程式碼都寫在 index.js 裡面
我可以把串接 api 的程式碼獨立出來寫在另一個檔案
然後在 index.js 裡面,我就只需要用 const API = require('./api.js')
引入,就可以在下面直接使用這個 api 了
const $ = require('jquery') // 引入 jQuery
const utils = require('./utils.js')
const API = require('./api.js') // 引入 api 的程式碼
console.log(utils.first('abc'))
// 開始使用 jQuery
$(document).ready(() => {
$('.btn').click(function() {
alert('hello!')
API.getCountries(){ // 開始使用 api
}
})
})
webpack 還有另一個很厲害的功能是: loader
loader 可以決定我可以載入什麼檔案格式
例如:
我只要用 css 相關的 loader 就可以「用 JavaScript 的方式動態的載入 css 的檔案」(完全不需要用 <link rel="stylesheet" href="style.css">
引入 css)
npm install --save-dev style-loader
來安裝 style-loadernpm install --save-dev css-loader
來安裝 css-loader指令裡面的 --save-dev
的意思就是:安裝的這個套件會出現在 package.json 裡面的 devDependencies,只有在開發的時候會用到此套件,打包出去後就不會用到了
在 webpack.config.js 新增一個區塊叫做 module.rules
:
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
};
test
就是:要針對什麼樣的檔案去套用這個 loaderstyle.css 也要放在 src 資料夾裡面
style.css:
body{
background-color: rgba(255, 0, 0, 0.2);
}
.btn{
padding: 30px;
}
要用 ./
才會是相對路徑,代表:同資料夾底下的 style.css
import css from './style.css'; // 引入 style.css
import $ from 'jquery' // 引入 jQuery
import { first } from './utils.js'
// 開始使用 jQuery
$(document).ready(() => {
$('.btn').click(function() {
alert(first('hello!'))
})
})
npm run build
讓 webpack 打包webpack 打包完之後,就可以看到網頁已經套用我寫好的 css 樣式了
webpack 背後幫我做的事情是:
會先把 css 的內容當作一個字串(很大的字串)
`body{
background-color: rgba(255, 0, 0, 0.2);
}
.btn{
padding: 30px;
}`
然後透過 JavaScript 的方式先在 <head>
建立一個 <style>
的節點,然後再把這個字串動態的插入到<style>
裡面,因此 css 就可以套用在網頁上
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body{
background-color: rgba(255, 0, 0, 0.2);
}
.btn{
padding: 30px;
}
</style>
</head>
babel-loader 會先用 babel 幫我把 ES6 轉換成 ES5 之後,再把 JS 檔案打包
npm install -D babel-loader @babel/core @babel/preset-env
來安裝 babel-loader在 webpack.config.js 新增一個 rule:
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
],
},
};
exclude: /(node_modules|bower_components)/
是因為:通常 node modules 裡面的檔案都已經被轉換過了,就不需要再轉換一次。bower_components
是另一個工具會用到的東西,也不需要被 babel 轉換presets
可以寫在這裡,或是也可以寫在 .babelrc 裡面: options: {
presets: ['@babel/preset-env']
}
npm run build
npm install sass-loader sass --save-dev
來安裝 sass-loader在 webpack.config.js 新增一個 rule:
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
// Creates `style` nodes from JS strings
"style-loader",
// Translates CSS into CommonJS
"css-loader",
// Compiles Sass to CSS
"sass-loader",
],
},
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
],
},
};
$color: rgba(186, 189, 21, 0.2);
body{
background-color: $color;
.btn{
padding: 30px;
}
}
import css from './style.scss'; // 引入 style.scss
npm run build
詳細可參考 Using webpack-dev-server
DevServer 會自動偵測,當我的檔案有變動時(按下儲存),DevServer 就會自動幫我 compile(把有修改的地方重新載入)
可以讓我不用每次修改完程式碼都要自己手動再打包一次
npm install --save-dev webpack-dev-server
來安裝 DevServercontentBase
的意思就是:tell the dev server where to look for files (dev server 要打開哪一個資料夾裡面的檔案)
This tells webpack-dev-server
to serve the files from the dist
directory on localhost:8080
.
devServer: {
contentBase: './dist',
},
"start": "webpack serve --open 'safari'"
Let's add a script to easily run the dev server.
"webpack serve --open 'safari'"
意思就是:用 safari 把 webpack dev server 打開
"scripts": {
"start": "webpack serve --open 'safari'",
"build": "webpack",
"test": "echo \"Error: no test specified\" && exit 1"
},
npm run start
safari 就會自動把 dev server 打開了
要把 index.html 也放到 dist 資料夾裡面,dev server 才會打開 index.html
(index.html 載入 main.js 的路徑就要改成 <script src="main.js"></script>
)
這時,當我修改了程式碼,例如我改了 style.css 裡面的背景顏色,按下存檔後,在 dev server 的 index.html 就會重新 compile 成我剛剛修改的樣子了!
詳細可參考 Using source maps
讓「打包後的程式碼」對應到「我原本的程式碼」,這樣在 console 做 debug 時,我就可以立刻知道錯誤是發生在我寫的哪一行
devtool: 'inline-source-map',
詳細可參考 HtmlWebpackPlugin
用 HtmlWebpackPlugin 這個 plugin 就可以自動幫我產生出一個 html 檔案,我就不需要自己建一個 html
注意,會需要用到 HtmlWebpackPlugin 是因為,有很多會用到 webpack 的網頁或 APP,它們的 html 元素都是用 JS 動態產生的,所以在 html 裡面不會有任何的元素
而且當我在使用 webpack 時,因為 CSS 和 JS 都是寫在獨立的檔案,所以 index.html 幾乎就沒有什麼內容了
npm install --save-dev html-webpack-plugin
來安裝 HtmlWebpackPlugin把 HtmlWebpackPlugin 引入進來
var HtmlWebpackPlugin = require('html-webpack-plugin');
在 module.exports
裡面加上這行:
plugins: [new HtmlWebpackPlugin()]
gulp 跟 webpack 在本質上是「完全不一樣的」
gulp 裡面有很多任務,我可以自己決定「每一個 task 的內容要是什麼」,基本上,只要我能夠寫出這個 task,gulp 就什麼事情都可以做到。例如:
「gulp 本身」做不到的事情是:bundle
但是,gulp 可以透過 webpack-plugin 去打包
我有很多資源(例如 .js, .scss, img),webpack 可以幫我把這些資源都 bundle 在一起
在 bundle 之前,需要透過 loader 把 .js, .scss, img 檔案載入進去 webpack ,webpack 再把這些檔案包起來
例如:
webpack 做不到的事情,例如:校正時間、定時 call API
webpack 主要的功能就是 bundle
因為瀏覽器原生沒有支援 require()
語法,所以要引入 module(資源)很不方便。有 webpack 幫我 bundle 這些資源之後,瀏覽器就可以支援 require()
語法了
module.exports = Utils
輸出模組const Utils = {
first: function(str) {
return str[0]
},
last: function(str) {
return str[str.length - 1]
}
}
module.exports = Utils
require()
引入 moduleconst utils = require('./utils')
來引入這個 module 即可const utils = require('./utils.js')
console.log(utils.first('abc'))
引入之後,輸入指令 node index.js
就會輸出 console.log(utils.first('abc'))
的結果(就是 a)
只有在 Node.js 上可以使用 CommonJS 這個模組化規範,也就是 module.exports
, require()
這些語法
在 Node.js 不會有命名衝突的問題,是因為在用 require()
引入 module (library) 的時候,我可以隨意用一個自己喜歡的變數名稱來引入
例如 const abc = require('./utils')
或是 const banana = require('./utils')
這些變數名稱(abc
, banana
)並不會影響到我要引入的 module (library)
可是,在瀏覽器上不能使用 require()
這個語法,因為瀏覽器不支援(瀏覽器沒有 require()
這個語法)
所以,如果瀏覽器想要引入一個 library,以 jQuery 為例,在瀏覽器上(index.html),我需要在 <header>
裡面加上這行 <script src="https://code.jquery.com/jquery-3.5.1.js"></script>
來把 jQuery 引入進來,然後我就可以有 $
或是 jQuery
這個全域變數(物件)可以使用
假設我在 index.html 同時引入了兩個 library,分別是 jQuery 和 lidemy
這兩個 library 都使用了 $
這個全域變數,那這時就會有命名衝突的問題
所以,在 jQuery 有提供了一個 function 叫做 jQuery.noConflict() 可以幫忙解決命名衝突的問題
(只要有用 global 全域變數來命名的 library 都會有命名衝突的問題)
為了要解決使用模組時的命名衝突問題,有很多人就實作了不同的「模組化規範」,像是:CommonJS, AMD, UMD (這幾個都不是官方的規範)
這些不同的規範就像是各家牌子的充電線,充電線的目的都一樣(讓手機充電),但是 Apple 手機和 Android 手機的充電線接頭長得都不一樣
這些規範的用法不一樣,不同規範之間也可能無法相容,但是目的都一樣
這些規範說明了我們要用什麼語法去使用模組、以及要怎麼把模組給別人用
直到後來 ES6 出現後,瀏覽器也支援了原生的模組化規範叫做 ES Modules,就可以使用 import
, export
這些語法
範例如下
在 utils.js 輸出 module:
export function first(str) {
return str[0]
}
在 index.js 引入 module:
import { first } from './utils.js'
console.log(first('abc'))
在 index.html 載入 index.js:
若是想要在瀏覽器上直接使用 import
與 export
,都必須以 module 的形式來執行,所以要在 script 標籤加上 type="module"
,這樣瀏覽器才看得懂
<script src="./index.js" type="module"></script>
但是會發現,當我用瀏覽器打開 index.html 時,會出現一個 CORS 的錯誤,原因為:沒有辦法直接用檔案的方式開啟,必須要在 local 開一個 server
因此,就輸入指令 python -m SimpleHTTPServer 8081
在 local 開一個 server 後,就可以在網址列輸入 localhost:8081 連到同樣的檔案位置(index.html),打開 console 就可以看到 console.log(first('abc'))
的結果(就是 a)
但要注意的是,上面所說的這個「瀏覽器的 module 規範」是新的語法(在近幾年才出來),舊的瀏覽器並不支援
因此在使用 module 規範之後,就可以用一些工具來幫我們做轉換,產生出來的檔案就可以丟到瀏覽器上面執行
即使我在 JS 裡面是使用 require()
的語法,做轉換之後一樣可以在瀏覽器上面執行
webpack 的功用就是:module bundler,把我的 modules(各種資源) 都包在一起,然後拿去做一些轉換,我就可以在瀏覽器上面也使用這些 modules
webpack 把「module」的概念擴充到不只是 JS 的模組,還包括圖片、CSS 檔案、聲音等資源,都可以當作 module(資源)打包在一起
首先,要用 npm 來安裝 webpack
輸入指令 npm init
來產生出一個 package.json 檔案(這個專案就會是一個 npm 的專案了)
按照 文件的 Basic Setup,輸入指令 npm install webpack webpack-cli --save-dev
來安裝 webpack
在實務上,開發時會把 source code 寫在一個 src 資料夾裡面,經過一些工具(例如 webpack, babel)轉換之後再放到別的地方去
因此,先建立一個 src 資料夾,裡面建立兩個檔案:utils.js 和 index.js
require()
的語法來引入 module在 utils.js 輸出 module:
const Utils = {
first: function(str) {
return str[0]
}
}
module.exports = Utils
在 index.js 引入 module:
const utils = require('./utils.js')
console.log(utils.first('abc'))
接下來,就是 webpack 要施展魔力的時刻了!
只要輸入指令 npx webpack
,就可以啟用 webpack 來幫我打包
webpack 預設的設定就是會去找「src 資料夾裡面的 index.js 檔案」來當作程式的入口點(入口點的意思就是:程式一開始就會去執行這個檔案)
webpack 會把打包完的東西放在 dist 資料夾(distribution 的簡寫,要發布出去的意思),在 dist 資料夾就會有一個 main.js 檔案(就是 webpack 打包出來的檔案)
打開 main.js 會看到是已經打包好並壓縮過的程式碼(webpack 會自動幫我做 minify, uglify)
這時,就試試看是否能夠執行
輸入指令 node dist/main.js
,有成功輸出結果 a
在 index.html 加上這行,引入 main.js 檔案:
<script src="dist/main.js"></script>
接著,用瀏覽器開啟 index.html,打開 console 就可以看到執行的結果了!(也就是 a)
原因為:
webpack 幫我們「在瀏覽器上實作了 require()
的功能」,然後再把 module 包在一起,讓我們在瀏覽器上也可以用 require()
的語法來使用模組
export
, import
語法來使用 moduleutils.js 輸出 module:
export function first(str) {
return str[0]
}
index.js 引入 module:
import $ from 'jquery' // 引入 jQuery
import { first } from './utils.js'
// 開始使用 jQuery
$(document).ready(() => {
$('.btn').click(function() {
alert(first('hello!'))
})
})
前面有提到,webpack 預設的設定就是會去找「src/index.js 檔案」來當作程式的入口點,如果我想要更改這個預設設定,就可以新建一個 webpack 的設定檔
webpack 的設定檔,預設的檔名叫做 webpack.config.js
設定檔的寫法可以參考官方文件 Using a Configuration
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
};
entry: './src/index.js'
就是程式的入口點,做為入口點的檔案會把 module 引入進來做一些事情(通常都會是 index.js),但如果就只有一個檔案的話,該檔案本身就會是入口點output
就是打包完的檔案,路徑會是 path.resolve(__dirname, 'dist')
,__dirname
就會去抓「我目前正在執行的資料夾 (__dirname
代表跟 config 檔同一個目錄)」,然後再放到底下的 dist 資料夾const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
};
在 package.json 裡面新增一行 "build": "webpack"
:
{
"name": "webpack-mtr04",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0"
}
}
這時,當我在 CLI 輸入指令 npm run build
時,就會幫我執行 build
的指令,也就是 webpack
(這麼做的目的只是讓我更方便去執行 webpack 而已)
因此,當我輸入指令 npm run build
時 > 就會執行 webpack
指令 > webpack 會去找設定檔(webpack.config.js) > 依據設定檔去做打包
webpack 除了可以打包我的程式碼,也可以打包我從 npm 安裝的程式碼
例如:
jQuery 也有一個 npm 的版本,我用 npm 的方式引入 jQuery
用 npm 的方式引入跟「用 <script>
的方式引入」是完全不同的
首先,輸入指令 npm install jquery --save-dev
來安裝 jQuery
安裝好之後,在 index.js 就可以引入 jQuery:
const $ = require('jquery') // 引入 jQuery
const utils = require('./utils.js')
console.log(utils.first('abc'))
// 開始使用 jQuery
$(document).ready(() => {
$('.btn').click(function() {
alert('hello!')
})
})
在 index.html 有一個 .btn
的按鈕:
<button class="btn">click me</button>
記得要再執行一次 npm run build
,webpack 才會再做一次打包
打包完後,點擊 .btn
就會出現 alert 了!
其實,用 npm 引入 library 一點都不神奇,它背後其實就是:
以引入 jQuery 為例
在專案資料夾底下,有一個 node modules 的檔案,打開後會找到裡面有一個 jquery 的資料夾 > 打開 jquery 資料夾裡面的 package.json 檔案,會看到有一個欄位是 main
:
"main": "dist/jquery.js",
main
的意思就是:當我用 require()
或 import
引入 jquery 時,就是使用「dist/jquery.js」這個檔案裡面的程式碼(是入口點)
所以,npm 就是透過這樣的方式來幫我引入 jQuery
有了模組化的好處是:
在前端開發時會有很多東西,我可以把不同的功能、library 都分散各個檔案(用不同的檔案去管理,結構可以切的比較明確),再用 webpack 幫我打包即可
以串接 api 為例
我不需要把串接 api 的程式碼都寫在 index.js 裡面
我可以把串接 api 的程式碼獨立出來寫在另一個檔案
然後在 index.js 裡面,我就只需要用 const API = require('./api.js')
引入,就可以在下面直接使用這個 api 了
const $ = require('jquery') // 引入 jQuery
const utils = require('./utils.js')
const API = require('./api.js') // 引入 api 的程式碼
console.log(utils.first('abc'))
// 開始使用 jQuery
$(document).ready(() => {
$('.btn').click(function() {
alert('hello!')
API.getCountries(){ // 開始使用 api
}
})
})
webpack 還有另一個很厲害的功能是: loader
loader 可以決定我可以載入什麼檔案格式
例如:
我只要用 css 相關的 loader 就可以「用 JavaScript 的方式動態的載入 css 的檔案」(完全不需要用 <link rel="stylesheet" href="style.css">
引入 css)
npm install --save-dev style-loader
來安裝 style-loadernpm install --save-dev css-loader
來安裝 css-loader指令裡面的 --save-dev
的意思就是:安裝的這個套件會出現在 package.json 裡面的 devDependencies,只有在開發的時候會用到此套件,打包出去後就不會用到了
在 webpack.config.js 新增一個區塊叫做 module.rules
:
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
};
test
就是:要針對什麼樣的檔案去套用這個 loaderstyle.css 也要放在 src 資料夾裡面
style.css:
body{
background-color: rgba(255, 0, 0, 0.2);
}
.btn{
padding: 30px;
}
要用 ./
才會是相對路徑,代表:同資料夾底下的 style.css
import css from './style.css'; // 引入 style.css
import $ from 'jquery' // 引入 jQuery
import { first } from './utils.js'
// 開始使用 jQuery
$(document).ready(() => {
$('.btn').click(function() {
alert(first('hello!'))
})
})
npm run build
讓 webpack 打包webpack 打包完之後,就可以看到網頁已經套用我寫好的 css 樣式了
webpack 背後幫我做的事情是:
會先把 css 的內容當作一個字串(很大的字串)
`body{
background-color: rgba(255, 0, 0, 0.2);
}
.btn{
padding: 30px;
}`
然後透過 JavaScript 的方式先在 <head>
建立一個 <style>
的節點,然後再把這個字串動態的插入到<style>
裡面,因此 css 就可以套用在網頁上
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body{
background-color: rgba(255, 0, 0, 0.2);
}
.btn{
padding: 30px;
}
</style>
</head>
babel-loader 會先用 babel 幫我把 ES6 轉換成 ES5 之後,再把 JS 檔案打包
npm install -D babel-loader @babel/core @babel/preset-env
來安裝 babel-loader在 webpack.config.js 新增一個 rule:
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
],
},
};
exclude: /(node_modules|bower_components)/
是因為:通常 node modules 裡面的檔案都已經被轉換過了,就不需要再轉換一次。bower_components
是另一個工具會用到的東西,也不需要被 babel 轉換presets
可以寫在這裡,或是也可以寫在 .babelrc 裡面: options: {
presets: ['@babel/preset-env']
}
npm run build
npm install sass-loader sass --save-dev
來安裝 sass-loader在 webpack.config.js 新增一個 rule:
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
// Creates `style` nodes from JS strings
"style-loader",
// Translates CSS into CommonJS
"css-loader",
// Compiles Sass to CSS
"sass-loader",
],
},
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
],
},
};
$color: rgba(186, 189, 21, 0.2);
body{
background-color: $color;
.btn{
padding: 30px;
}
}
import css from './style.scss'; // 引入 style.scss
npm run build
詳細可參考 Using webpack-dev-server
DevServer 會自動偵測,當我的檔案有變動時(按下儲存),DevServer 就會自動幫我 compile(把有修改的地方重新載入)
可以讓我不用每次修改完程式碼都要自己手動再打包一次
npm install --save-dev webpack-dev-server
來安裝 DevServercontentBase
的意思就是:tell the dev server where to look for files (dev server 要打開哪一個資料夾裡面的檔案)
This tells webpack-dev-server
to serve the files from the dist
directory on localhost:8080
.
devServer: {
contentBase: './dist',
},
"start": "webpack serve --open 'safari'"
Let's add a script to easily run the dev server.
"webpack serve --open 'safari'"
意思就是:用 safari 把 webpack dev server 打開
"scripts": {
"start": "webpack serve --open 'safari'",
"build": "webpack",
"test": "echo \"Error: no test specified\" && exit 1"
},
npm run start
safari 就會自動把 dev server 打開了
要把 index.html 也放到 dist 資料夾裡面,dev server 才會打開 index.html
(index.html 載入 main.js 的路徑就要改成 <script src="main.js"></script>
)
這時,當我修改了程式碼,例如我改了 style.css 裡面的背景顏色,按下存檔後,在 dev server 的 index.html 就會重新 compile 成我剛剛修改的樣子了!
詳細可參考 Using source maps
讓「打包後的程式碼」對應到「我原本的程式碼」,這樣在 console 做 debug 時,我就可以立刻知道錯誤是發生在我寫的哪一行
devtool: 'inline-source-map',
詳細可參考 HtmlWebpackPlugin
用 HtmlWebpackPlugin 這個 plugin 就可以自動幫我產生出一個 html 檔案,我就不需要自己建一個 html
注意,會需要用到 HtmlWebpackPlugin 是因為,有很多會用到 webpack 的網頁或 APP,它們的 html 元素都是用 JS 動態產生的,所以在 html 裡面不會有任何的元素
而且當我在使用 webpack 時,因為 CSS 和 JS 都是寫在獨立的檔案,所以 index.html 幾乎就沒有什麼內容了
npm install --save-dev html-webpack-plugin
來安裝 HtmlWebpackPlugin把 HtmlWebpackPlugin 引入進來
var HtmlWebpackPlugin = require('html-webpack-plugin');
在 module.exports
裡面加上這行:
plugins: [new HtmlWebpackPlugin()]
gulp 跟 webpack 在本質上是「完全不一樣的」
gulp 裡面有很多任務,我可以自己決定「每一個 task 的內容要是什麼」,基本上,只要我能夠寫出這個 task,gulp 就什麼事情都可以做到。例如:
「gulp 本身」做不到的事情是:bundle
但是,gulp 可以透過 webpack-plugin 去打包
我有很多資源(例如 .js, .scss, img),webpack 可以幫我把這些資源都 bundle 在一起
在 bundle 之前,需要透過 loader 把 .js, .scss, img 檔案載入進去 webpack ,webpack 再把這些檔案包起來
例如:
webpack 做不到的事情,例如:校正時間、定時 call API
webpack 主要的功能就是 bundle
因為瀏覽器原生沒有支援 require()
語法,所以要引入 module(資源)很不方便。有 webpack 幫我 bundle 這些資源之後,瀏覽器就可以支援 require()
語法了
gulp 就是一個 task manager
我可以把各種 task 寫在一個 gulp file 裡面,透過 gulp 的很多 plugin 去幫我執行這些任務,讓我可以很方便地用「程式化的方式」去管理這些 tasks
有一個類似的服務叫做 IFTTT,就是 If This Then That 的簡寫
用 npm init
新開一個 project 的目的只是為了要有 package.json 這個檔案
gulp
會出現錯誤,但改為使用 npx gulp
就正常,這是因為 global 的東西有裝錯,所以用 npx
就會用專案裡面的 gulp 來跑(不會去找 global 的)輸入 npm install --save-dev gulp
安裝完之後,如果用 gulp --version
出現錯誤,那可以改用 npx gulp --version
來查看版本,如果有出現版本,就表示安裝成功了
把官網上的程式碼複製貼上:
function defaultTask(cb) {
// place code for your default task here
cb();
}
exports.default = defaultTask
上面這段的意思是:
gulp 可以分成很多個 task,每個 task 就是一個 function
exports.default = defaultTask
意思就是「gulp 會幫我執行 .default
這個 function」,default
是 task 的名稱
如果在 CLI 輸入指令 npx gulp
,預設就會執行 default
這個 task
所以如果把 task 名稱改為 aaa,像是這樣:
function defaultTask(cb) {
// place code for your default task here
cb();
}
exports.aaa = defaultTask
在 CLI 就要輸入指令 npx gulp aaa
在 gulpfile.js 裡面:
function defaultTask(cb) {
// place code for your default task here
cb();
}
exports.default = defaultTask
cb
是 gulp 提供的一個 callback function,在 defaultTask
函式裡面呼叫 cb()
就是在跟 gulp 說「所有的任務都完成了」
gulp 有提供一些內建的 function,例如 src
和 dest
(都是一個 function)
用 const { src, dest } = require('gulp')
把 src
和 dest
拿進來
如果是用 gulp 內建的東西,可以想成是一個「資料流」的概念,就像是在寫 JS 陣列時,可以這樣一直串接 function:
return [a, b, c].map().filter().reduce()
在用 gulp 時也是這種資料流的概念:
const { src, dest } = require('gulp');
function defaultTask() {
// place code for your default task here
return src('src/*.js')
.pipe(dest('dist'))
}
exports.default = defaultTask
可以在 src()
小括號裡面放入「我要做事情的程式碼」
src('src/*.js')
意思就是:會去找 src 資料夾裡面所有的 .js 檔案
連接要用一個 .pipe()
(這是 gulp 的規則),.pipe()
小括號裡面再放入要做的事情
dest('dist')
意思就是:我要把它寫入到 dist 這個資料夾
寫完 task 之後,就可以輸入指令 npx gulp
來執行這個 task,執行完後,就可以看到多了一個 dist 資料夾,裡面有 demo.js 檔案
gulp 做的事情就是:把 src 資料夾裡面的 demo.js 檔案,複製到 dist 資料夾
詳細可參考 gulp-babel
是採用 plugin 的形式:
需要安裝 gulp 的 plugin,再搭配 babel 自己的東西
因為 @babel/core
和 @babel/preset-env
都已經裝好了,所以就只需要安裝 gulp-babel 這個 plugin,輸入指令 npm install --save-dev gulp-babel
裝好 plugin 之後,就可以開始使用了:
文件的範例程式碼:
const gulp = require('gulp');
const babel = require('gulp-babel');
gulp.task('default', () =>
gulp.src('src/app.js')
.pipe(babel({
presets: ['@babel/env']
}))
.pipe(gulp.dest('dist'))
);
我在 gulpfile.js 寫的程式碼:
const babel = require('gulp-babel')
就是把 gulp-babel
這個 plugin 引入進來
在中間用 .pipe(babel())
就可以讓 babel 幫我編譯
const { src, dest } = require('gulp');
const babel = require('gulp-babel');
function defaultTask() {
// place code for your default task here
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
}
exports.default = defaultTask
這裡我在 babel()
小括號裡面不用再寫一次 babel 的 preset 設定 {presets: ['@babel/env']}
是因為我在專案資料夾裡面已經有一個 .babelrc 的檔案,檔案裡面就已經設定好 preset 了:
{
"presets": [
"@babel/preset-env"
]
}
寫完之後,就可以輸入指令 npx gulp
來執行這個 task
執行完後,在 dist 資料夾裡面的 demo.js 就會是經過 babel 編譯過後的程式碼了!
接著,可以把「babel 編譯」的這個 task 用一個 compileJS
的 function 包起來:
const { src, dest } = require('gulp');
const babel = require('gulp-babel');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
}
function defaultTask() {
// place code for your default task here
}
exports.default = defaultTask
接著,就可來寫 sass 的任務
詳細可參考 gulp-sass
首先,先安裝需要的 plugin,輸入指令 npm install node-sass gulp-sass --save-dev
裝完 plugin 之後就可以開始使用了
在 gulpfile.js 裡面這樣寫:
var sass = require('gulp-sass')
就是:把 gulp-sass
這個 plugin 引入進來
sass.compiler = require('node-sass')
就是:指定 sass 的 compiler
const { src, dest } = require('gulp');
const babel = require('gulp-babel');
const sass = require('gulp-sass');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
}
function defaultTask() {
// place code for your default task here
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
}
exports.default = defaultTask
寫完之後,就可以輸入指令 npx gulp
來執行 task
執行完後,就會多一個 css 資料夾,裡面就會是把 style.sass 編譯過後的 style.css 檔案了
接著,也把這個 sass 的任務包成一個 compileCSS
的 function:
const { src, dest } = require('gulp');
const babel = require('gulp-babel');
const sass = require('gulp-sass');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
}
function compileCSS() {
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
}
exports.default = defaultTask
series
串接任務(按照順序,依序執行)用 gulp 內建的 series
來執行 compileJS
和 compileCSS
這兩個 tasks
const { src, dest, series } = require('gulp')
要記得把 series
先引入進來
exports.default = series(compileCSS, compileJS)
會按照順序:先執行 compileCSS
,再執行 compileJS
因為是先後分開執行,所花費的時間會較多
const { src, dest, series } = require('gulp');
const babel = require('gulp-babel');
const sass = require('gulp-sass');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
}
function compileCSS() {
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
}
exports.default = series(compileCSS, compileJS)
parallel
串接任務(會同時執行)exports.default = parallel(compileCSS, compileJS)
用 parallel
會同時執行 compileCSS
和 compileJS
因為是同時執行,所花費的時間會較少
const { src, dest, series, parallel } = require('gulp');
const babel = require('gulp-babel');
const sass = require('gulp-sass');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
}
function compileCSS() {
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
}
exports.default = parallel(compileCSS, compileJS)
詳細可參考 Conditional plugins
首先,先輸入指令 npm install --save-dev gulp-uglify
來安裝 plugin
接著,引入 plugin 並加上 .pipe(uglify())
const { src, dest, series, parallel } = require('gulp');
const babel = require('gulp-babel');
const uglify = require('gulp-uglify');
const sass = require('gulp-sass');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(uglify())
.pipe(dest('dist'))
}
function compileCSS() {
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
}
exports.default = parallel(compileCSS, compileJS)
然後用 npx gulp
執行任務後,就可以看到 JS 檔案已經被壓縮了(壓縮成一行,去掉所有換行、空白),通常在 server 上看到的都會是這樣 uglify 之後的檔案(為了節省檔案空間)
詳細可參考 gulp-clean-css
首先,先輸入指令 npm install gulp-clean-css --save-dev
來安裝 plugin
用 .pipe(cleanCSS({ compatibility: 'ie8' }))
來壓縮 CSS 檔案
compatibility: 'ie8'
可以設定「要支援到哪些瀏覽器」,會依據設定來決定要在 CSS 加上哪些瀏覽器的 prefixconst { src, dest, series, parallel } = require('gulp');
const babel = require('gulp-babel');
const uglify = require('gulp-uglify');
const sass = require('gulp-sass');
const rename = require('gulp-rename');
const cleanCSS = require('gulp-clean-css');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
.pipe(uglify())
.pipe(rename({ extname: '.min.js' }))
.pipe(dest('dist'))
}
function compileCSS() {
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
.pipe(cleanCSS({ compatibility: 'ie8' }))
.pipe(rename({ extname: '.min.css' }))
.pipe(dest('css'))
}
exports.default = parallel(compileCSS, compileJS)
首先,先輸入指令 npm install --save-dev gulp-rename
來安裝 plugin
通常會把有做過 uglify 或 minify 的 JS 和 CSS 檔案,副檔名改叫做 .min.js 和 .min.css,這樣才能和原始檔案做區別
用 .pipe(rename({ extname: 'min.js' }))
更改檔案名稱
const { src, dest, series, parallel } = require('gulp');
const babel = require('gulp-babel');
const uglify = require('gulp-uglify');
const sass = require('gulp-sass');
const rename = require('gulp-rename');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(uglify())
.pipe(rename({ extname: '.min.js' }))
.pipe(dest('dist'))
}
function compileCSS() {
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
}
exports.default = parallel(compileCSS, compileJS)
也可以這樣寫,在 compileJS()
先輸出「沒有壓縮過的」,再輸出「有壓縮過的」:
const { src, dest, series, parallel } = require('gulp');
const babel = require('gulp-babel');
const uglify = require('gulp-uglify');
const sass = require('gulp-sass');
const rename = require('gulp-rename');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
.pipe(uglify())
.pipe(rename({ extname: '.min.js' }))
.pipe(dest('dist'))
}
function compileCSS() {
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
}
exports.default = parallel(compileCSS, compileJS)
如果只想要執行 compileCSS
這個任務,就把 compileCSS
exports 出去,然後在 CLI 輸入指令 npx gulp compileCSS
就可以只跑 compileCSS
這個任務
const { src, dest, series, parallel } = require('gulp');
const babel = require('gulp-babel');
const uglify = require('gulp-uglify');
const sass = require('gulp-sass');
const rename = require('gulp-rename');
const cleanCSS = require('gulp-clean-css');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
.pipe(uglify())
.pipe(rename({ extname: '.min.js' }))
.pipe(dest('dist'))
}
function compileCSS() {
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
.pipe(cleanCSS({ compatibility: 'ie8' }))
.pipe(rename({ extname: '.min.css' }))
.pipe(dest('css'))
}
exports.compileCSS = compileCSS
exports.default = parallel(compileCSS, compileJS)
exports.default = parallel(compileCSS, compileJS)
意思就是:當我在 CLI 只輸入 npx gulp
(沒有加上任務名稱),那就會執行 default 的任務,也就是 parallel(compileCSS, compileJS)
(平行、同時跑這兩個任務)
這裡有先定義好每個不同的 task(都分別做不同的事情) 並 exports 出去:
exports.clean = clean;
exports.default = build;
exports.js = js;
exports.css = css
exports.img = img
exports.beforeEnd = beforeEnd
exports.sprite = gulp.series(sprite, css);
gulp.watch()
監測檔案變動watch
這個 function 就是會去監測裡面有寫的每個檔案,當檔案有變動的時候就重新執行該任務
function watch(done) {
if (env !== 'production') {
gulp.watch(`${base.src}/scss/**/*`, css);
gulp.watch(`${base.src}/html/**/*`, html);
gulp.watch(`${base.src}/js/**/*`, js);
gulp.watch(`${base.src}/image/*`, img);
gulp.watch(`${base.src}/image/*`, gulp.series(sprite, css));
}
done();
}
img()
壓縮圖片可以把 img 讀進來 > 把 img 壓縮之後 > 再把 img 存到我指定的地方
這樣就不需要自己去做圖片的壓縮
function img() {
return src(paths.image.src)
.pipe(imageMin())
.pipe(dest(paths.image.dest))
.pipe(connect.reload())
}
]]>gulp 就是一個 task manager
我可以把各種 task 寫在一個 gulp file 裡面,透過 gulp 的很多 plugin 去幫我執行這些任務,讓我可以很方便地用「程式化的方式」去管理這些 tasks
有一個類似的服務叫做 IFTTT,就是 If This Then That 的簡寫
用 npm init
新開一個 project 的目的只是為了要有 package.json 這個檔案
gulp
會出現錯誤,但改為使用 npx gulp
就正常,這是因為 global 的東西有裝錯,所以用 npx
就會用專案裡面的 gulp 來跑(不會去找 global 的)輸入 npm install --save-dev gulp
安裝完之後,如果用 gulp --version
出現錯誤,那可以改用 npx gulp --version
來查看版本,如果有出現版本,就表示安裝成功了
把官網上的程式碼複製貼上:
function defaultTask(cb) {
// place code for your default task here
cb();
}
exports.default = defaultTask
上面這段的意思是:
gulp 可以分成很多個 task,每個 task 就是一個 function
exports.default = defaultTask
意思就是「gulp 會幫我執行 .default
這個 function」,default
是 task 的名稱
如果在 CLI 輸入指令 npx gulp
,預設就會執行 default
這個 task
所以如果把 task 名稱改為 aaa,像是這樣:
function defaultTask(cb) {
// place code for your default task here
cb();
}
exports.aaa = defaultTask
在 CLI 就要輸入指令 npx gulp aaa
在 gulpfile.js 裡面:
function defaultTask(cb) {
// place code for your default task here
cb();
}
exports.default = defaultTask
cb
是 gulp 提供的一個 callback function,在 defaultTask
函式裡面呼叫 cb()
就是在跟 gulp 說「所有的任務都完成了」
gulp 有提供一些內建的 function,例如 src
和 dest
(都是一個 function)
用 const { src, dest } = require('gulp')
把 src
和 dest
拿進來
如果是用 gulp 內建的東西,可以想成是一個「資料流」的概念,就像是在寫 JS 陣列時,可以這樣一直串接 function:
return [a, b, c].map().filter().reduce()
在用 gulp 時也是這種資料流的概念:
const { src, dest } = require('gulp');
function defaultTask() {
// place code for your default task here
return src('src/*.js')
.pipe(dest('dist'))
}
exports.default = defaultTask
可以在 src()
小括號裡面放入「我要做事情的程式碼」
src('src/*.js')
意思就是:會去找 src 資料夾裡面所有的 .js 檔案
連接要用一個 .pipe()
(這是 gulp 的規則),.pipe()
小括號裡面再放入要做的事情
dest('dist')
意思就是:我要把它寫入到 dist 這個資料夾
寫完 task 之後,就可以輸入指令 npx gulp
來執行這個 task,執行完後,就可以看到多了一個 dist 資料夾,裡面有 demo.js 檔案
gulp 做的事情就是:把 src 資料夾裡面的 demo.js 檔案,複製到 dist 資料夾
詳細可參考 gulp-babel
是採用 plugin 的形式:
需要安裝 gulp 的 plugin,再搭配 babel 自己的東西
因為 @babel/core
和 @babel/preset-env
都已經裝好了,所以就只需要安裝 gulp-babel 這個 plugin,輸入指令 npm install --save-dev gulp-babel
裝好 plugin 之後,就可以開始使用了:
文件的範例程式碼:
const gulp = require('gulp');
const babel = require('gulp-babel');
gulp.task('default', () =>
gulp.src('src/app.js')
.pipe(babel({
presets: ['@babel/env']
}))
.pipe(gulp.dest('dist'))
);
我在 gulpfile.js 寫的程式碼:
const babel = require('gulp-babel')
就是把 gulp-babel
這個 plugin 引入進來
在中間用 .pipe(babel())
就可以讓 babel 幫我編譯
const { src, dest } = require('gulp');
const babel = require('gulp-babel');
function defaultTask() {
// place code for your default task here
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
}
exports.default = defaultTask
這裡我在 babel()
小括號裡面不用再寫一次 babel 的 preset 設定 {presets: ['@babel/env']}
是因為我在專案資料夾裡面已經有一個 .babelrc 的檔案,檔案裡面就已經設定好 preset 了:
{
"presets": [
"@babel/preset-env"
]
}
寫完之後,就可以輸入指令 npx gulp
來執行這個 task
執行完後,在 dist 資料夾裡面的 demo.js 就會是經過 babel 編譯過後的程式碼了!
接著,可以把「babel 編譯」的這個 task 用一個 compileJS
的 function 包起來:
const { src, dest } = require('gulp');
const babel = require('gulp-babel');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
}
function defaultTask() {
// place code for your default task here
}
exports.default = defaultTask
接著,就可來寫 sass 的任務
詳細可參考 gulp-sass
首先,先安裝需要的 plugin,輸入指令 npm install node-sass gulp-sass --save-dev
裝完 plugin 之後就可以開始使用了
在 gulpfile.js 裡面這樣寫:
var sass = require('gulp-sass')
就是:把 gulp-sass
這個 plugin 引入進來
sass.compiler = require('node-sass')
就是:指定 sass 的 compiler
const { src, dest } = require('gulp');
const babel = require('gulp-babel');
const sass = require('gulp-sass');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
}
function defaultTask() {
// place code for your default task here
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
}
exports.default = defaultTask
寫完之後,就可以輸入指令 npx gulp
來執行 task
執行完後,就會多一個 css 資料夾,裡面就會是把 style.sass 編譯過後的 style.css 檔案了
接著,也把這個 sass 的任務包成一個 compileCSS
的 function:
const { src, dest } = require('gulp');
const babel = require('gulp-babel');
const sass = require('gulp-sass');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
}
function compileCSS() {
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
}
exports.default = defaultTask
series
串接任務(按照順序,依序執行)用 gulp 內建的 series
來執行 compileJS
和 compileCSS
這兩個 tasks
const { src, dest, series } = require('gulp')
要記得把 series
先引入進來
exports.default = series(compileCSS, compileJS)
會按照順序:先執行 compileCSS
,再執行 compileJS
因為是先後分開執行,所花費的時間會較多
const { src, dest, series } = require('gulp');
const babel = require('gulp-babel');
const sass = require('gulp-sass');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
}
function compileCSS() {
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
}
exports.default = series(compileCSS, compileJS)
parallel
串接任務(會同時執行)exports.default = parallel(compileCSS, compileJS)
用 parallel
會同時執行 compileCSS
和 compileJS
因為是同時執行,所花費的時間會較少
const { src, dest, series, parallel } = require('gulp');
const babel = require('gulp-babel');
const sass = require('gulp-sass');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
}
function compileCSS() {
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
}
exports.default = parallel(compileCSS, compileJS)
詳細可參考 Conditional plugins
首先,先輸入指令 npm install --save-dev gulp-uglify
來安裝 plugin
接著,引入 plugin 並加上 .pipe(uglify())
const { src, dest, series, parallel } = require('gulp');
const babel = require('gulp-babel');
const uglify = require('gulp-uglify');
const sass = require('gulp-sass');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(uglify())
.pipe(dest('dist'))
}
function compileCSS() {
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
}
exports.default = parallel(compileCSS, compileJS)
然後用 npx gulp
執行任務後,就可以看到 JS 檔案已經被壓縮了(壓縮成一行,去掉所有換行、空白),通常在 server 上看到的都會是這樣 uglify 之後的檔案(為了節省檔案空間)
詳細可參考 gulp-clean-css
首先,先輸入指令 npm install gulp-clean-css --save-dev
來安裝 plugin
用 .pipe(cleanCSS({ compatibility: 'ie8' }))
來壓縮 CSS 檔案
compatibility: 'ie8'
可以設定「要支援到哪些瀏覽器」,會依據設定來決定要在 CSS 加上哪些瀏覽器的 prefixconst { src, dest, series, parallel } = require('gulp');
const babel = require('gulp-babel');
const uglify = require('gulp-uglify');
const sass = require('gulp-sass');
const rename = require('gulp-rename');
const cleanCSS = require('gulp-clean-css');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
.pipe(uglify())
.pipe(rename({ extname: '.min.js' }))
.pipe(dest('dist'))
}
function compileCSS() {
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
.pipe(cleanCSS({ compatibility: 'ie8' }))
.pipe(rename({ extname: '.min.css' }))
.pipe(dest('css'))
}
exports.default = parallel(compileCSS, compileJS)
首先,先輸入指令 npm install --save-dev gulp-rename
來安裝 plugin
通常會把有做過 uglify 或 minify 的 JS 和 CSS 檔案,副檔名改叫做 .min.js 和 .min.css,這樣才能和原始檔案做區別
用 .pipe(rename({ extname: 'min.js' }))
更改檔案名稱
const { src, dest, series, parallel } = require('gulp');
const babel = require('gulp-babel');
const uglify = require('gulp-uglify');
const sass = require('gulp-sass');
const rename = require('gulp-rename');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(uglify())
.pipe(rename({ extname: '.min.js' }))
.pipe(dest('dist'))
}
function compileCSS() {
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
}
exports.default = parallel(compileCSS, compileJS)
也可以這樣寫,在 compileJS()
先輸出「沒有壓縮過的」,再輸出「有壓縮過的」:
const { src, dest, series, parallel } = require('gulp');
const babel = require('gulp-babel');
const uglify = require('gulp-uglify');
const sass = require('gulp-sass');
const rename = require('gulp-rename');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
.pipe(uglify())
.pipe(rename({ extname: '.min.js' }))
.pipe(dest('dist'))
}
function compileCSS() {
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
}
exports.default = parallel(compileCSS, compileJS)
如果只想要執行 compileCSS
這個任務,就把 compileCSS
exports 出去,然後在 CLI 輸入指令 npx gulp compileCSS
就可以只跑 compileCSS
這個任務
const { src, dest, series, parallel } = require('gulp');
const babel = require('gulp-babel');
const uglify = require('gulp-uglify');
const sass = require('gulp-sass');
const rename = require('gulp-rename');
const cleanCSS = require('gulp-clean-css');
sass.compiler = require('node-sass');
function compileJS() {
return src('src/*.js')
.pipe(babel())
.pipe(dest('dist'))
.pipe(uglify())
.pipe(rename({ extname: '.min.js' }))
.pipe(dest('dist'))
}
function compileCSS() {
return src('src/*.sass')
.pipe(sass().on('error', sass.logError))
.pipe(dest('css'))
.pipe(cleanCSS({ compatibility: 'ie8' }))
.pipe(rename({ extname: '.min.css' }))
.pipe(dest('css'))
}
exports.compileCSS = compileCSS
exports.default = parallel(compileCSS, compileJS)
exports.default = parallel(compileCSS, compileJS)
意思就是:當我在 CLI 只輸入 npx gulp
(沒有加上任務名稱),那就會執行 default 的任務,也就是 parallel(compileCSS, compileJS)
(平行、同時跑這兩個任務)
這裡有先定義好每個不同的 task(都分別做不同的事情) 並 exports 出去:
exports.clean = clean;
exports.default = build;
exports.js = js;
exports.css = css
exports.img = img
exports.beforeEnd = beforeEnd
exports.sprite = gulp.series(sprite, css);
gulp.watch()
監測檔案變動watch
這個 function 就是會去監測裡面有寫的每個檔案,當檔案有變動的時候就重新執行該任務
function watch(done) {
if (env !== 'production') {
gulp.watch(`${base.src}/scss/**/*`, css);
gulp.watch(`${base.src}/html/**/*`, html);
gulp.watch(`${base.src}/js/**/*`, js);
gulp.watch(`${base.src}/image/*`, img);
gulp.watch(`${base.src}/image/*`, gulp.series(sprite, css));
}
done();
}
img()
壓縮圖片可以把 img 讀進來 > 把 img 壓縮之後 > 再把 img 存到我指定的地方
這樣就不需要自己去做圖片的壓縮
function img() {
return src(paths.image.src)
.pipe(imageMin())
.pipe(dest(paths.image.dest))
.pipe(connect.reload())
}
]]>
進入 Babel 官網的 Setup > 選擇 CLI,透過 Command line 的方式去做轉換
輸入指令 npm init
(一直按 Enter)來產生出 package.json
npm install --save-dev @babel/core @babel/cli
裝好之後,在 package.json 裡面就會自動新增 babel/core 和 babel/cli 這兩個 Dependencies
"devDependencies": {
"@babel/cli": "^7.12.1",
"@babel/core": "^7.12.3"
}
build
在 package.json 的 "scripts"
加上這段 "build": "babel src -d lib"
"scripts": {
"build": "babel src -d lib",
"test": "echo \"Error: no test specified\" && exit 1"
},
加上之後,當我在 CLI 輸入指令 npm run build
,就會執行 build
裡面寫好的這行 babel src -d lib
,意思就是:
我要執行 babel
這個指令,幫我編譯 src 資料夾裡面的內容(source code 都會寫在 src 資料夾裡面),編譯之後的檔案都會放到目的地也就是 lib 資料夾內(-d
就是 destination 的意思)
.babelrc
在專案資料夾的根目錄新增一個檔案叫做 .babelrc
然後在 .babelrc
檔案裡面貼上這段:
{
"presets": ["@babel/preset-env"]
}
這段的意思就是:
presets 就是 babel 給的一些預設設定,我要用這些預設設定來轉換程式碼
接著,要把這個 presets 安裝起來,輸入指令:
npm install @babel/preset-env --save-dev
安裝完 presets 之後,可以去 @babel/preset-env 的文件 看要如何設定更多細節
例如:可以設定我想要支援哪一些瀏覽器
在 Browserslist Integration 可以設定要支援「還有 25% 以上的人在使用的瀏覽器」
現在,就可以在專案資料夾裡面新增一個 src 資料夾 > 在 src 資料夾新增一個 demo.js
在 demo.js 裡面,就可以用最新的 JS 語法來寫,然後在 CLI 輸入指令 npm run build
之後,打開 lib 資料夾裡面的 demo.js 就可以看到編譯過後的程式碼了(編譯成舊的 JS 語法)
進入 Babel 官網的 Setup > 選擇 CLI,透過 Command line 的方式去做轉換
輸入指令 npm init
(一直按 Enter)來產生出 package.json
npm install --save-dev @babel/core @babel/cli
裝好之後,在 package.json 裡面就會自動新增 babel/core 和 babel/cli 這兩個 Dependencies
"devDependencies": {
"@babel/cli": "^7.12.1",
"@babel/core": "^7.12.3"
}
build
在 package.json 的 "scripts"
加上這段 "build": "babel src -d lib"
"scripts": {
"build": "babel src -d lib",
"test": "echo \"Error: no test specified\" && exit 1"
},
加上之後,當我在 CLI 輸入指令 npm run build
,就會執行 build
裡面寫好的這行 babel src -d lib
,意思就是:
我要執行 babel
這個指令,幫我編譯 src 資料夾裡面的內容(source code 都會寫在 src 資料夾裡面),編譯之後的檔案都會放到目的地也就是 lib 資料夾內(-d
就是 destination 的意思)
.babelrc
在專案資料夾的根目錄新增一個檔案叫做 .babelrc
然後在 .babelrc
檔案裡面貼上這段:
{
"presets": ["@babel/preset-env"]
}
這段的意思就是:
presets 就是 babel 給的一些預設設定,我要用這些預設設定來轉換程式碼
接著,要把這個 presets 安裝起來,輸入指令:
npm install @babel/preset-env --save-dev
安裝完 presets 之後,可以去 @babel/preset-env 的文件 看要如何設定更多細節
例如:可以設定我想要支援哪一些瀏覽器
在 Browserslist Integration 可以設定要支援「還有 25% 以上的人在使用的瀏覽器」
現在,就可以在專案資料夾裡面新增一個 src 資料夾 > 在 src 資料夾新增一個 demo.js
在 demo.js 裡面,就可以用最新的 JS 語法來寫,然後在 CLI 輸入指令 npm run build
之後,打開 lib 資料夾裡面的 demo.js 就可以看到編譯過後的程式碼了(編譯成舊的 JS 語法)
原本的 CSS 是不具有「程式」的概念的,但我現在可以用程式的概念去寫 CSS(例如用 function、迴圈、變數來寫,可以讓開發更加順利),再用 CSS 預處理器幫我把這些新的語法轉換成 CSS 的標準語法
style.css.map 這個檔案就是所謂的 source map,幫我把 style.css 的內容對應到 style.scss(實際在開發的檔案)
也可以傳入參數
如果沒有使用像是 Sass 這樣的預處理器,CSS 在開發上會有這些問題:
在最後的 production 階段,如果想要壓縮 css 檔案,就在 terminal 輸入:
sass --style=compressed main.sass main.css
extend 就像是大一點的變數,例如 object
style.sass:
用 %btn
先寫好一個 template
%btn
padding: 1rem 2rem
color: green
font-size: 1rem
.btn
&-primary
@extend %btn
&-secondary
@extend %btn
&-warning
@extend %btn
style.css:
會用逗號的方式去幫三個 class 套用同一個 template
.btn-warning, .btn-secondary, .btn-primary {
padding: 1rem 2rem;
color: green;
font-size: 1rem;
}
style.sass:
@mixin btn
padding: 1rem 2rem
color: green
font-size: 1rem
.btn
&-primary
+btn
&-secondary
+btn
&-warning
+btn
style.css:
會分別幫三個 class 套用同一個 template(會複製貼上三次)
.btn-primary {
padding: 1rem 2rem;
color: green;
font-size: 1rem;
}
.btn-secondary {
padding: 1rem 2rem;
color: green;
font-size: 1rem;
}
.btn-warning {
padding: 1rem 2rem;
color: green;
font-size: 1rem;
}
把水平置中寫死,但是因為不一定需要垂直置中,所以 top
只有先給預設值是 50%,意思就是:如果沒有傳 $top
,那 $top
就會是 50%
@mixin absCenter($top: 50%)
position: absolute
top: $top
left: 50%
transform: translate(-50%, -50%)
當元素有共用樣式時
style.sass:
$primary: orange
$secondary: grey
$warning: red
%btn
padding: 1rem 2rem
color: green
font-size: 1rem
@mixin hover-btn($color)
&:hover
background-color: $color
.btn
&-primary
@extend %btn
+hover-btn($primary)
&-secondary
@extend %btn
+hover-btn($secondary)
&-warning
@extend %btn
+hover-btn($warning)
style.css:
.btn-warning, .btn-secondary, .btn-primary {
padding: 1rem 2rem;
color: green;
font-size: 1rem;
}
.btn-primary:hover {
background-color: orange;
}
.btn-secondary:hover {
background-color: grey;
}
.btn-warning:hover {
background-color: red;
}
@mixin flex-center
display: flex
justify-content: center
align-items: center
function 跟 mixin 的差別在於:function 可以回傳數值
以下範例是 function 搭配 mixin 使用
style.sass:
可以看到
function 適合用在「當我需要在一個屬性下做運算」時,function 可以回傳我想要的值,例如這裡的 letter-spacing
mixin 適合用在「當我需要打包一組功能」時
$primary: orange
$secondary: grey
$warning: red
%btn
padding: 1rem 2rem
color: green
font-size: 1rem
@function letter-spacing($font-index)
@return $font-index/10 * 0.2rem
@mixin hover-btn($color, $font-index)
&:hover
background-color: $color
letter-spacing: letter-spacing($font-index)
@mixin flex-center
display: flex
justify-content: center
align-items: center
.btn
&-primary
@extend %btn
+hover-btn($primary, 10)
&-secondary
@extend %btn
+hover-btn($secondary, 30)
&-warning
@extend %btn
+hover-btn($warning, 100)
詳細可參考 Flow Control Rules
@each
@each
適合搭配我們自己定義好的變數(list, map)來使用
在 Sass 也有類似 JS 的 array 的資料格式,在 Sass 叫做 list,語法是這樣:
$direction-types: center, start, end
@each
搭配 list 的應用style.sass:
$type
就是 $direction-types
裡面的每一個 type
$direction-types: center, start, end
@each $type in $direction-types
.flex-#{$type}
display: flex
justify-content: $type
align-items: center
style.css:
在這裡用 list 是不適合的,因為這樣 justify-content 後面就只會有 start, end,但應該要是 flex-start, flex-end
因此,要改為使用 map(後面的範例)
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.flex-start {
display: flex;
justify-content: start;
align-items: center;
}
.flex-end {
display: flex;
justify-content: end;
align-items: center;
}
在 Sass 也有類似 JS 的 object 的資料格式,在 Sass 叫做 map,語法是這樣:
$direction-types: (center: center, start: flex-start, end: flex-end)
@each
搭配 map 的應用style.sass:
$type
就是 $direction-types
裡面的每一個 key
$value
就是 key 裡面的每一個 value
$direction-types: (center: center, start: flex-start, end: flex-end)
@each $type, $value in $direction-types
.flex-#{$type}
display: flex
justify-content: $value
align-items: center
style.css:
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.flex-start {
display: flex;
justify-content: flex-start;
align-items: center;
}
.flex-end {
display: flex;
justify-content: flex-end;
align-items: center;
}
@for
@for
適合用在當我想要預先開好一系列的字級、位置
style.sass:
@for $i from 0 through 5
.h#{5 - $i + 1}
font-size: 1 + 0.2rem * $i
style.css:
.h6 {
font-size: 1rem;
}
.h5 {
font-size: 1.2rem;
}
.h4 {
font-size: 1.4rem;
}
.h3 {
font-size: 1.6rem;
}
.h2 {
font-size: 1.8rem;
}
.h1 {
font-size: 2rem;
}
@if...@else
style.sass:
==
」,不是「三個等號 ===
」@for $i from 0 through 5
.h#{5 - $i + 1}
@if $i % 2 == 0
font-size: 1 + 0.2rem * $i
@else
font-size: 1 + 0.3rem * $i
變數命名方式分為兩類:
$color-primary
$red-01
把一些「跨頁的元件」整理成 components
像是按鈕、標題
把每頁共同的區塊,整理到 layouts 裡面
像是:navbar, footer
最後,再用一個 pages 把剩下的版面排好後,把每個部分都 import 進來,就會是一包完整的 sass 檔案
原本的 CSS 是不具有「程式」的概念的,但我現在可以用程式的概念去寫 CSS(例如用 function、迴圈、變數來寫,可以讓開發更加順利),再用 CSS 預處理器幫我把這些新的語法轉換成 CSS 的標準語法
style.css.map 這個檔案就是所謂的 source map,幫我把 style.css 的內容對應到 style.scss(實際在開發的檔案)
也可以傳入參數
如果沒有使用像是 Sass 這樣的預處理器,CSS 在開發上會有這些問題:
在最後的 production 階段,如果想要壓縮 css 檔案,就在 terminal 輸入:
sass --style=compressed main.sass main.css
extend 就像是大一點的變數,例如 object
style.sass:
用 %btn
先寫好一個 template
%btn
padding: 1rem 2rem
color: green
font-size: 1rem
.btn
&-primary
@extend %btn
&-secondary
@extend %btn
&-warning
@extend %btn
style.css:
會用逗號的方式去幫三個 class 套用同一個 template
.btn-warning, .btn-secondary, .btn-primary {
padding: 1rem 2rem;
color: green;
font-size: 1rem;
}
style.sass:
@mixin btn
padding: 1rem 2rem
color: green
font-size: 1rem
.btn
&-primary
+btn
&-secondary
+btn
&-warning
+btn
style.css:
會分別幫三個 class 套用同一個 template(會複製貼上三次)
.btn-primary {
padding: 1rem 2rem;
color: green;
font-size: 1rem;
}
.btn-secondary {
padding: 1rem 2rem;
color: green;
font-size: 1rem;
}
.btn-warning {
padding: 1rem 2rem;
color: green;
font-size: 1rem;
}
把水平置中寫死,但是因為不一定需要垂直置中,所以 top
只有先給預設值是 50%,意思就是:如果沒有傳 $top
,那 $top
就會是 50%
@mixin absCenter($top: 50%)
position: absolute
top: $top
left: 50%
transform: translate(-50%, -50%)
當元素有共用樣式時
style.sass:
$primary: orange
$secondary: grey
$warning: red
%btn
padding: 1rem 2rem
color: green
font-size: 1rem
@mixin hover-btn($color)
&:hover
background-color: $color
.btn
&-primary
@extend %btn
+hover-btn($primary)
&-secondary
@extend %btn
+hover-btn($secondary)
&-warning
@extend %btn
+hover-btn($warning)
style.css:
.btn-warning, .btn-secondary, .btn-primary {
padding: 1rem 2rem;
color: green;
font-size: 1rem;
}
.btn-primary:hover {
background-color: orange;
}
.btn-secondary:hover {
background-color: grey;
}
.btn-warning:hover {
background-color: red;
}
@mixin flex-center
display: flex
justify-content: center
align-items: center
function 跟 mixin 的差別在於:function 可以回傳數值
以下範例是 function 搭配 mixin 使用
style.sass:
可以看到
function 適合用在「當我需要在一個屬性下做運算」時,function 可以回傳我想要的值,例如這裡的 letter-spacing
mixin 適合用在「當我需要打包一組功能」時
$primary: orange
$secondary: grey
$warning: red
%btn
padding: 1rem 2rem
color: green
font-size: 1rem
@function letter-spacing($font-index)
@return $font-index/10 * 0.2rem
@mixin hover-btn($color, $font-index)
&:hover
background-color: $color
letter-spacing: letter-spacing($font-index)
@mixin flex-center
display: flex
justify-content: center
align-items: center
.btn
&-primary
@extend %btn
+hover-btn($primary, 10)
&-secondary
@extend %btn
+hover-btn($secondary, 30)
&-warning
@extend %btn
+hover-btn($warning, 100)
詳細可參考 Flow Control Rules
@each
@each
適合搭配我們自己定義好的變數(list, map)來使用
在 Sass 也有類似 JS 的 array 的資料格式,在 Sass 叫做 list,語法是這樣:
$direction-types: center, start, end
@each
搭配 list 的應用style.sass:
$type
就是 $direction-types
裡面的每一個 type
$direction-types: center, start, end
@each $type in $direction-types
.flex-#{$type}
display: flex
justify-content: $type
align-items: center
style.css:
在這裡用 list 是不適合的,因為這樣 justify-content 後面就只會有 start, end,但應該要是 flex-start, flex-end
因此,要改為使用 map(後面的範例)
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.flex-start {
display: flex;
justify-content: start;
align-items: center;
}
.flex-end {
display: flex;
justify-content: end;
align-items: center;
}
在 Sass 也有類似 JS 的 object 的資料格式,在 Sass 叫做 map,語法是這樣:
$direction-types: (center: center, start: flex-start, end: flex-end)
@each
搭配 map 的應用style.sass:
$type
就是 $direction-types
裡面的每一個 key
$value
就是 key 裡面的每一個 value
$direction-types: (center: center, start: flex-start, end: flex-end)
@each $type, $value in $direction-types
.flex-#{$type}
display: flex
justify-content: $value
align-items: center
style.css:
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.flex-start {
display: flex;
justify-content: flex-start;
align-items: center;
}
.flex-end {
display: flex;
justify-content: flex-end;
align-items: center;
}
@for
@for
適合用在當我想要預先開好一系列的字級、位置
style.sass:
@for $i from 0 through 5
.h#{5 - $i + 1}
font-size: 1 + 0.2rem * $i
style.css:
.h6 {
font-size: 1rem;
}
.h5 {
font-size: 1.2rem;
}
.h4 {
font-size: 1.4rem;
}
.h3 {
font-size: 1.6rem;
}
.h2 {
font-size: 1.8rem;
}
.h1 {
font-size: 2rem;
}
@if...@else
style.sass:
==
」,不是「三個等號 ===
」@for $i from 0 through 5
.h#{5 - $i + 1}
@if $i % 2 == 0
font-size: 1 + 0.2rem * $i
@else
font-size: 1 + 0.3rem * $i
變數命名方式分為兩類:
$color-primary
$red-01
把一些「跨頁的元件」整理成 components
像是按鈕、標題
把每頁共同的區塊,整理到 layouts 裡面
像是:navbar, footer
最後,再用一個 pages 把剩下的版面排好後,把每個部分都 import 進來,就會是一包完整的 sass 檔案
因此:
最外層什麼時候要用.row
去包,什麼時候要用.d-flex
去包?
在需要使用格線系統時就可使用 .row
,僅有使用 flex 就使用 .d-flex
如果不清楚一些特殊使用的方法,這樣是最安全且正確的執行方式
格線系統會有額外的屬性 (gutter, 補回空間等),原理在前面的章節有介紹過
可以再參考前面章節,並透過 Chrome 開發者工具再次檢驗看看
在課程中,有舉例 2 種不同型式的排版
(1) 最外層容器 container - 內容 box
(2) 最外層容器 container - row - column - 內容 box
有以下這幾點:
有很多的對齊屬性,與「主軸、交錯軸」都有很大的關係
排列方式:
可以對齊於「主軸的起點 or 終點」
(對齊於:主軸起點)
(對齊於:主軸終點)
* 也可以「等距排列」
![](https://i.imgur.com/E6pecdC.jpg)
在具有 flex 屬性的 flexbox containers 中,使用「align-items utilities」來使物件對齊
flex-direction: row
,就會以「y 軸」來做對齊flex-direction: column
,就會以「x 軸」來做對齊這裡用「.align-items-center
」來做範例講解
(codepen 範例)
flex-direction: row
,加上.align-items-center
後,就會以「y 軸」來做「置中對齊」 <div class="row align-items-center">
<div class="col-3">
<div class="box"></div>
</div>
<div class="col-3">
<div class="box"></div>
</div>
<div class="col-3">
<div class="box"></div>
</div>
<div class="col-3">
<div class="box"></div>
</div>
</div>
flex-direction: column
,加上.align-items-center
後,就會以「x 軸」來做「置中對齊」 <div class="row flex-column align-items-center">
<div class="col-3">
<div class="box"></div>
</div>
<div class="col-3">
<div class="box"></div>
</div>
<div class="col-3">
<div class="box"></div>
</div>
<div class="col-3">
<div class="box"></div>
</div>
</div>
在「交錯軸」上,有一個「align-items」的對齊屬性
(對齊於:交錯軸起點)
(對齊於:交錯軸終點)
因為「flex-direction」這個屬性,可以「改變主軸的方向性」
「flex-direction」不只可以將主軸改為垂直,還可以將主軸「反轉」
下圖中,「標示粗體字」是「比較重要的」屬性
以下將一一介紹重要的屬性:
在使用 flex 的 grid system 時,.row
已經有加入display: flex;
這個屬性
決定 flex 的軸線方向,是相當重要的一個屬性
主軸的對齊
交錯軸的對齊
自己這個元件的交錯軸對齊
「flex-direction」有 4 個值,分別是:
flex-direction: row
flex-direction: row-reverse
flex-direction: column
flex-direction: column-reverse
也可以想成是「物件之間的間隔方法」
下圖中所列的是「justify-content 的所有屬性」
justify-content: flex-start
justify-content: flex-end
flex-start
來說)將元件排列之後,貼齊「主軸終點」justify-content: center
justify-content: between
justify-content: space-around
下圖中所列的是「align-items 的所有屬性」
align-items: flex-start
align-items: flex-end
align-items: center
justify-content 的「flex-start、flex-end、center」
和
align-items 的「flex-start、flex-end、center」
是相同的(值相當接近)
align-items: baseline
align-items: stretch
align-self 跟「align-items」很像,但是
所以,align-self 並不會影響到其他元件,只針對自己這個元件去做調整
下圖中所列的是「align-self 的所有屬性」
屬性與「align-items」都一樣,
唯一的差別是:align-self 是針對「自己的屬性」來做設定,因此,不會影響到其他元件的設定值
https://codepen.io/saffranwang/pen/RzBaOv
請問關於這個範例他的寬度是用 flex 去設定的,我看一些文件的敘述,想問問我的理解對不對
而比較不能理解這個範例是當 寬度比較大時,item1 會比 item2 還要寬,但當寬度縮小時,item1 寬度會比 item2還要小,請問這是為什麼呢?
A:
Flex 影片我們有在線上問答介紹過
相關影片有放在 Youtube 上
你可以先參考看看
因此:
最外層什麼時候要用.row
去包,什麼時候要用.d-flex
去包?
在需要使用格線系統時就可使用 .row
,僅有使用 flex 就使用 .d-flex
如果不清楚一些特殊使用的方法,這樣是最安全且正確的執行方式
格線系統會有額外的屬性 (gutter, 補回空間等),原理在前面的章節有介紹過
可以再參考前面章節,並透過 Chrome 開發者工具再次檢驗看看
在課程中,有舉例 2 種不同型式的排版
(1) 最外層容器 container - 內容 box
(2) 最外層容器 container - row - column - 內容 box
有以下這幾點:
有很多的對齊屬性,與「主軸、交錯軸」都有很大的關係
排列方式:
可以對齊於「主軸的起點 or 終點」
(對齊於:主軸起點)
(對齊於:主軸終點)
* 也可以「等距排列」
![](https://i.imgur.com/E6pecdC.jpg)
在具有 flex 屬性的 flexbox containers 中,使用「align-items utilities」來使物件對齊
flex-direction: row
,就會以「y 軸」來做對齊flex-direction: column
,就會以「x 軸」來做對齊這裡用「.align-items-center
」來做範例講解
(codepen 範例)
flex-direction: row
,加上.align-items-center
後,就會以「y 軸」來做「置中對齊」 <div class="row align-items-center">
<div class="col-3">
<div class="box"></div>
</div>
<div class="col-3">
<div class="box"></div>
</div>
<div class="col-3">
<div class="box"></div>
</div>
<div class="col-3">
<div class="box"></div>
</div>
</div>
flex-direction: column
,加上.align-items-center
後,就會以「x 軸」來做「置中對齊」 <div class="row flex-column align-items-center">
<div class="col-3">
<div class="box"></div>
</div>
<div class="col-3">
<div class="box"></div>
</div>
<div class="col-3">
<div class="box"></div>
</div>
<div class="col-3">
<div class="box"></div>
</div>
</div>
在「交錯軸」上,有一個「align-items」的對齊屬性
(對齊於:交錯軸起點)
(對齊於:交錯軸終點)
因為「flex-direction」這個屬性,可以「改變主軸的方向性」
「flex-direction」不只可以將主軸改為垂直,還可以將主軸「反轉」
下圖中,「標示粗體字」是「比較重要的」屬性
以下將一一介紹重要的屬性:
在使用 flex 的 grid system 時,.row
已經有加入display: flex;
這個屬性
決定 flex 的軸線方向,是相當重要的一個屬性
主軸的對齊
交錯軸的對齊
自己這個元件的交錯軸對齊
「flex-direction」有 4 個值,分別是:
flex-direction: row
flex-direction: row-reverse
flex-direction: column
flex-direction: column-reverse
也可以想成是「物件之間的間隔方法」
下圖中所列的是「justify-content 的所有屬性」
justify-content: flex-start
justify-content: flex-end
flex-start
來說)將元件排列之後,貼齊「主軸終點」justify-content: center
justify-content: between
justify-content: space-around
下圖中所列的是「align-items 的所有屬性」
align-items: flex-start
align-items: flex-end
align-items: center
justify-content 的「flex-start、flex-end、center」
和
align-items 的「flex-start、flex-end、center」
是相同的(值相當接近)
align-items: baseline
align-items: stretch
align-self 跟「align-items」很像,但是
所以,align-self 並不會影響到其他元件,只針對自己這個元件去做調整
下圖中所列的是「align-self 的所有屬性」
屬性與「align-items」都一樣,
唯一的差別是:align-self 是針對「自己的屬性」來做設定,因此,不會影響到其他元件的設定值
https://codepen.io/saffranwang/pen/RzBaOv
請問關於這個範例他的寬度是用 flex 去設定的,我看一些文件的敘述,想問問我的理解對不對
而比較不能理解這個範例是當 寬度比較大時,item1 會比 item2 還要寬,但當寬度縮小時,item1 寬度會比 item2還要小,請問這是為什麼呢?
A:
Flex 影片我們有在線上問答介紹過
相關影片有放在 Youtube 上
你可以先參考看看
文件網址:
https://getbootstrap.com/docs/4.3/layout/grid/#grid-options
如果要做「響應式」的話,要透過上面文件中 Grid options 的設定
Bootstrap 中斷點,說明如下:
:::danger
「垂直的手機」在 Bootstrap 裡面,就是「預設的版型」--> 從「垂直的手機」作為起點開始寫
:::
:::success
「xs 省略」的意思是:
就直接寫col-*
就可以了
:::
以下面的範例來做說明
html:
<div class="row">
<div class="col-sm-4 side-bar"></div>
<div class="col-sm-8 content"></div>
</div>
:::danger
[說明]
當小於 sm(寬度小於 576px)就會切換成 xs 的排版
col-*
,就會採用col-12
「12 欄寬(預設就是寬度 100%)」==,來呈現欄寬(平常製作 RWD 時也不需要填入 col-x)col-*
,就會依照設定的值,來呈現欄寬:::
col-sm-*
當我使用col-sm-*
時,會有什麼變化?
html:
<div class="row">
<div class="col-sm-4 side-bar"></div>
<div class="col-sm-8 content"></div>
</div>
是「兩欄式」的排版
就會切換成「單欄式」的排版
:::info
「4欄寬 + 8欄寬」會調整成「統一的 12 欄寬 (寬度 100%)」
:::
col-md-*
當我使用col-md-*
時,會有什麼變化?
html:
<div class="row">
<div class="col-md-4 side-bar"></div>
<div class="col-md-8 content"></div>
</div>
會是「兩欄式」的排版
就會切換成「單欄式」的排版(寬度為 100%)
col-sm-*
html:
<div class="container mt-3">
<!-- 兩欄式的排版 -->
<div class="row">
<div class="col-sm-6">
<div class="box"></div>
</div>
<div class="col-sm-6">
<div class="box"></div>
</div>
</div>
<hr>
<!-- 三欄式的排版 -->
<div class="row">
<div class="col-sm-4">
<div class="box"></div>
</div>
<div class="col-sm-4">
<div class="box"></div>
</div>
<div class="col-sm-4">
<div class="box"></div>
</div>
</div>
</div>
用 chrome 開發者工具來看
就會切換成「單欄」的排版
html:
大裝置「4欄」:設定col-md-3
在寬度 >= 768px 時,都會是「4欄」
小裝置「2欄」:設定col-sm-6
在寬度是 576~767px 時,都會是「2欄」
<div class="container mt-3">
<div class="row">
<div class="col-md-3 col-sm-6">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6">
<div class="box"></div>
</div>
</div>
</div>
:::success
若要設定 col 的margin-top/bottom
,應寫在哪個位置呢?
A:
在此只需要了解一個重點 - 不要調整水平的 margin
垂直部分可以使用 my-3 的方式調整
並且直接應用在 col-sm-6
my-3 的相關概念在後面通用類別會介紹到喔
:::
col-4
(承接上面範例)html:
<div class="container mt-3">
<div class="row">
<div class="col-md-3 col-sm-6 col-4">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6 col-4">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6 col-4">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6 col-4">
<div class="box"></div>
</div>
</div>
</div>
html:
在寬度 < 575px 時,設定
col-3
col-3
col-6
col-12
(寬度為 100%) <div class="container mt-3">
<div class="row">
<div class="col-md-3 col-sm-6 col-3">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6 col-3">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6 col-6">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6 col-12">
<div class="box"></div>
</div>
</div>
</div>
就可以設計出非常有變化性的排版:
class 中的撰寫順序是沒有差別的
而老師是習慣從大到小(從 md sm xs 寫下來)
當然你也可以依據自己或團隊需求做決定
例如:
<div class="col-md-3 col-sm-6 col-3"></div>
這樣 className 不會太長嗎?
這是與 CSS 架構概念有關
Bootstrap 的目的是為了增加每個 className 的可複用性
這個概念下,甚至能不再撰寫 CSS 就能完成畫面
過去我也會糾結於 Bootstrap 的 className 太多
但在實際開發過幾個網站後
會了解到這種概念是正確的
原因在於,沒有足夠可複用性的 className 連管理都會有問題
(每一段樣式都需要開一個新的 className,這樣其實失去 CSS 樣式表的意義)
:::danger
一般實務上,也不㑹將全部尺寸都放上去,只會用一兩個 ex: col-md-6
只有部分情境下,特定寬度排版沒那麼合適時,才額外增加中斷點調整
:::
<div class="box"></div>
想請問在影片範例中的<div class="box"></div>
,它只有套用自定義的 .box
的 css,而未套用 bootstrap 的col-*
或是col-sm-*
之類的 class 屬性,為何它也能自適應螢幕寬度去縮放呢?
A:
這是因為盒模型的特性,.box
本身就是 100% 寬度
在 bootstrap 文件中響應式斷點的地方
http://bootstrap.hexschool.com/docs/4.0/layout/overview/#responsive-breakpoints
有下面這些斷點,
這些不能直接寫在自己的 scss 裡套用嗎?
@include media-breakpoint-up(xs) { ... }
@include media-breakpoint-up(sm) { ... }
@include media-breakpoint-up(md) { ... }
@include media-breakpoint-up(lg) { ... }
@include media-breakpoint-up(xl) { ... }
// Example usage:
@include media-breakpoint-up(sm) {
.some-class {
display: block;
}
}
A:
下面這段就是他已經寫好的 @mixin,所以如果你有載入 BS4 的 SCSS,自然能使用他的 @mixin 哩
@include media-breakpoint-up(xs) { ... }
@include media-breakpoint-up(sm) { ... }
@include media-breakpoint-up(md) { ... }
@include media-breakpoint-up(lg) { ... }
@include media-breakpoint-up(xl) { ... }
// Example usage:
@include media-breakpoint-up(sm) {
.some-class {
display: block;
}
}
如果要載入 BS4 的 SCSS 是要載入哪個檔案呢?
這部分課程有的,在章節 11「Bootstrap 與 Sass」有說明該如何載入
文件網址:
https://getbootstrap.com/docs/4.3/layout/grid/#grid-options
如果要做「響應式」的話,要透過上面文件中 Grid options 的設定
Bootstrap 中斷點,說明如下:
:::danger
「垂直的手機」在 Bootstrap 裡面,就是「預設的版型」--> 從「垂直的手機」作為起點開始寫
:::
:::success
「xs 省略」的意思是:
就直接寫col-*
就可以了
:::
以下面的範例來做說明
html:
<div class="row">
<div class="col-sm-4 side-bar"></div>
<div class="col-sm-8 content"></div>
</div>
:::danger
[說明]
當小於 sm(寬度小於 576px)就會切換成 xs 的排版
col-*
,就會採用col-12
「12 欄寬(預設就是寬度 100%)」==,來呈現欄寬(平常製作 RWD 時也不需要填入 col-x)col-*
,就會依照設定的值,來呈現欄寬:::
col-sm-*
當我使用col-sm-*
時,會有什麼變化?
html:
<div class="row">
<div class="col-sm-4 side-bar"></div>
<div class="col-sm-8 content"></div>
</div>
是「兩欄式」的排版
就會切換成「單欄式」的排版
:::info
「4欄寬 + 8欄寬」會調整成「統一的 12 欄寬 (寬度 100%)」
:::
col-md-*
當我使用col-md-*
時,會有什麼變化?
html:
<div class="row">
<div class="col-md-4 side-bar"></div>
<div class="col-md-8 content"></div>
</div>
會是「兩欄式」的排版
就會切換成「單欄式」的排版(寬度為 100%)
col-sm-*
html:
<div class="container mt-3">
<!-- 兩欄式的排版 -->
<div class="row">
<div class="col-sm-6">
<div class="box"></div>
</div>
<div class="col-sm-6">
<div class="box"></div>
</div>
</div>
<hr>
<!-- 三欄式的排版 -->
<div class="row">
<div class="col-sm-4">
<div class="box"></div>
</div>
<div class="col-sm-4">
<div class="box"></div>
</div>
<div class="col-sm-4">
<div class="box"></div>
</div>
</div>
</div>
用 chrome 開發者工具來看
就會切換成「單欄」的排版
html:
大裝置「4欄」:設定col-md-3
在寬度 >= 768px 時,都會是「4欄」
小裝置「2欄」:設定col-sm-6
在寬度是 576~767px 時,都會是「2欄」
<div class="container mt-3">
<div class="row">
<div class="col-md-3 col-sm-6">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6">
<div class="box"></div>
</div>
</div>
</div>
:::success
若要設定 col 的margin-top/bottom
,應寫在哪個位置呢?
A:
在此只需要了解一個重點 - 不要調整水平的 margin
垂直部分可以使用 my-3 的方式調整
並且直接應用在 col-sm-6
my-3 的相關概念在後面通用類別會介紹到喔
:::
col-4
(承接上面範例)html:
<div class="container mt-3">
<div class="row">
<div class="col-md-3 col-sm-6 col-4">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6 col-4">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6 col-4">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6 col-4">
<div class="box"></div>
</div>
</div>
</div>
html:
在寬度 < 575px 時,設定
col-3
col-3
col-6
col-12
(寬度為 100%) <div class="container mt-3">
<div class="row">
<div class="col-md-3 col-sm-6 col-3">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6 col-3">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6 col-6">
<div class="box"></div>
</div>
<div class="col-md-3 col-sm-6 col-12">
<div class="box"></div>
</div>
</div>
</div>
就可以設計出非常有變化性的排版:
class 中的撰寫順序是沒有差別的
而老師是習慣從大到小(從 md sm xs 寫下來)
當然你也可以依據自己或團隊需求做決定
例如:
<div class="col-md-3 col-sm-6 col-3"></div>
這樣 className 不會太長嗎?
這是與 CSS 架構概念有關
Bootstrap 的目的是為了增加每個 className 的可複用性
這個概念下,甚至能不再撰寫 CSS 就能完成畫面
過去我也會糾結於 Bootstrap 的 className 太多
但在實際開發過幾個網站後
會了解到這種概念是正確的
原因在於,沒有足夠可複用性的 className 連管理都會有問題
(每一段樣式都需要開一個新的 className,這樣其實失去 CSS 樣式表的意義)
:::danger
一般實務上,也不㑹將全部尺寸都放上去,只會用一兩個 ex: col-md-6
只有部分情境下,特定寬度排版沒那麼合適時,才額外增加中斷點調整
:::
<div class="box"></div>
想請問在影片範例中的<div class="box"></div>
,它只有套用自定義的 .box
的 css,而未套用 bootstrap 的col-*
或是col-sm-*
之類的 class 屬性,為何它也能自適應螢幕寬度去縮放呢?
A:
這是因為盒模型的特性,.box
本身就是 100% 寬度
在 bootstrap 文件中響應式斷點的地方
http://bootstrap.hexschool.com/docs/4.0/layout/overview/#responsive-breakpoints
有下面這些斷點,
這些不能直接寫在自己的 scss 裡套用嗎?
@include media-breakpoint-up(xs) { ... }
@include media-breakpoint-up(sm) { ... }
@include media-breakpoint-up(md) { ... }
@include media-breakpoint-up(lg) { ... }
@include media-breakpoint-up(xl) { ... }
// Example usage:
@include media-breakpoint-up(sm) {
.some-class {
display: block;
}
}
A:
下面這段就是他已經寫好的 @mixin,所以如果你有載入 BS4 的 SCSS,自然能使用他的 @mixin 哩
@include media-breakpoint-up(xs) { ... }
@include media-breakpoint-up(sm) { ... }
@include media-breakpoint-up(md) { ... }
@include media-breakpoint-up(lg) { ... }
@include media-breakpoint-up(xl) { ... }
// Example usage:
@include media-breakpoint-up(sm) {
.some-class {
display: block;
}
}
如果要載入 BS4 的 SCSS 是要載入哪個檔案呢?
這部分課程有的,在章節 11「Bootstrap 與 Sass」有說明該如何載入
在 jQuery 官網的 Download 裡面,如果想用「沒壓縮過」的版本,就選擇 Download the uncompressed, development jQuery 這個,複製網址後,把網址貼到 index.html 的 <script src="">
裡面:
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://code.jquery.com/jquery-3.5.1.js"></script>
<title>Document</title>
</head>
如果有成功引入 jQuery,就會建立一個 global 的變數叫做 jQuery
因此在 JS 寫上 console.log(jQuery);
,如果有印出東西,就代表有成功引入 jQuery 了
.text()
可以更改元素裡面的文字.show()
和 .hide()
來顯示/隱藏元素範例:
點一下按鈕就顯示 .box
,再點一下就隱藏 .box
$('.btn')
也可以寫成 jQuery('.btn')
,因為 $ === jQuery
, $
只是一個變數的符號而已
$(document).ready(function() {
var isHide = false;
$('.btn').click(function (e) {
if (isHide) {
$('.box').show();
} else {
$('.box').hide();
}
isHide = !isHide;
})
})
.fadeIn()
和 .fadeOut()
來顯示/隱藏元素() 裡面可以傳入毫秒數,例如 .fadeIn(2000)
.val()
可以設定/取得元素的值.val()
裡面有傳參數,就是 SET(設定值)例如:用 $('.todo-input').val('smile')
就可以把 .todo-input
的值設為 smile
例如:用 $('.todo-input').val('')
就可以把 .todo-input
的值給清空
.val()
裡面沒有傳參數,就是 GET(取得值)例如:用 $('.todo-input').val()
就可以取得 .todo-input
的值
.append()
可以依序往後插入元素範例:
$(document).ready(function() {
$('.btn').click(function (e) {
const value = $('.todo-input').val(); // 取出 .todo-input 的值
$('.todo-input').val(''); // 清空 .todo-input 的值
$('.todos').append(`<div class="todo">${value}</div>`);
})
})
.prepend()
可以依序往前插入元素.css('color', 'red')
可以調整元素的 css,把 color 設為 red.empty()
可以清空元素裡面的所有東西.empty()
就等於是 .innerHTML = ''
,元素本身還是會留著
但如果是用 .remove()
的話,連元素本身也都會被移除掉
範例:
在 .todos
加上 event listener,監聽 .todos
裡面的 .btn-delete
,只要 .btn-delete
有 click 事件,就會執行後面 function 的程式碼 $(e.target).parent().fadeOut();
// 刪除功能
$('.todos').on('click', '.btn-delete', function(e) {
$(e.target).parent().fadeOut();
})
html:
<input type="text" class="todo-input">
<button class="btn">Add todo</button>
<button class="btn-remove-all">Remove all todos</button>
<div class="todos">
</div>
all.js:
$(document).ready(function() {
$('.btn').click(function (e) {
const value = $('.todo-input').val(); // 取出 .todo-input 的值
$('.todo-input').val(''); // 清空 .todo-input 的值
$('.todos').append(`
<div class="todo">
${value}
<button class="btn-mark">標記完成</button>
<button class="btn-delete">刪除</button>
</div>
`);
})
// 移除所有的 todo
$('.btn-remove-all').click(() => {
$('.todos').empty();
})
// 刪除功能
$('.todos').on('click', '.btn-delete', function(e) {
$(e.target).parent().fadeOut();
})
// 標記完成/未完成
$('.todos').on('click', '.btn-mark', function(e) {
const todo = $(e.target).parent();
if (todo.hasClass('completed')) { // 變成未完成
todo.css('color', 'black');
todo.removeClass('completed');
$(e.target).text('標記完成');
} else { // 變成已完成
todo.css('color', 'green');
todo.addClass('completed');
$(e.target).text('標記未完成');
}
})
})
jQuery 與 Ajax 的官方文件,可參考 Low-Level Interface
在接下來的範例中,會使用 REST COUNTRIES 這個 api
$.ajax()
就是一個 function,小括弧裡面要傳入幾個參數,參數可以有幾種形式:
$.ajax({
method: "POST",
url: "some.php",
data: { name: "John", location: "Boston" }
})
.done(function( msg ) {
alert( "Data Saved: " + msg );
});
$.ajax()
這個 function 來發送 request下面這段程式碼,就可以發送一個 GET 的 request 到 https://restcountries.eu/rest/v2/name/germany 這個 api 去
$.ajax({
method: 'GET',
url: 'https://restcountries.eu/rest/v2/name/germany'
})
.done()
來拿到 response發送完 request 之後,要接收 response:
用 .done()
來接收結果,小括號裡面傳入一個 callback function,把拿到的 data
印出來
$.ajax({
method: 'GET',
url: 'https://restcountries.eu/rest/v2/name/germany'
}).done(function(data) {
console.log(data);
})
用 .fail()
可以接收錯誤訊息
$.ajax({
method: 'GET',
url: 'https://aaarestcountries.eu/rest/v2/name/germany'
}).done(function(data) {
console.log(data);
}).fail(function(err) {
console.log('error: ', err);
})
在 You Might Not Need jQuery 有提供另一種寫法:
同樣也可以拿到結果和錯誤處理
$.ajax({
method: 'GET',
url: 'https://aaarestcountries.eu/rest/v2/name/germany',
success: data => console.log(data),
error: err => console.log('error: ', err)
})
按下送出後去 api 拿資料,再把拿到的資料顯示在下方
html:
Name: <input type="text" name="country-name">
<button class="btn">送出</button>
<div class="list">
</div>
all.js:
$(document).ready(() => {
$('.btn').click(() => {
const value = $('input[name=country-name]').val();
// 如果欄位沒有填寫
if (value === '') {
alert('必須輸入名稱!');
return;
}
$('.list').empty(); // 先把 .list 清空
// 如果欄位有填寫
$.ajax({
method: 'GET',
url: 'https://restcountries.eu/rest/v2/name/' + value,
success: countries => {
for (let country of countries) {
$('.list').append(`<div>${country.alpha2Code} ${country.name} ${country.nativeName}</div>`)
}
},
error: err => {
alert('系統不穩定!');
}
})
})
})
]]>在 jQuery 官網的 Download 裡面,如果想用「沒壓縮過」的版本,就選擇 Download the uncompressed, development jQuery 這個,複製網址後,把網址貼到 index.html 的 <script src="">
裡面:
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://code.jquery.com/jquery-3.5.1.js"></script>
<title>Document</title>
</head>
如果有成功引入 jQuery,就會建立一個 global 的變數叫做 jQuery
因此在 JS 寫上 console.log(jQuery);
,如果有印出東西,就代表有成功引入 jQuery 了
.text()
可以更改元素裡面的文字.show()
和 .hide()
來顯示/隱藏元素範例:
點一下按鈕就顯示 .box
,再點一下就隱藏 .box
$('.btn')
也可以寫成 jQuery('.btn')
,因為 $ === jQuery
, $
只是一個變數的符號而已
$(document).ready(function() {
var isHide = false;
$('.btn').click(function (e) {
if (isHide) {
$('.box').show();
} else {
$('.box').hide();
}
isHide = !isHide;
})
})
.fadeIn()
和 .fadeOut()
來顯示/隱藏元素() 裡面可以傳入毫秒數,例如 .fadeIn(2000)
.val()
可以設定/取得元素的值.val()
裡面有傳參數,就是 SET(設定值)例如:用 $('.todo-input').val('smile')
就可以把 .todo-input
的值設為 smile
例如:用 $('.todo-input').val('')
就可以把 .todo-input
的值給清空
.val()
裡面沒有傳參數,就是 GET(取得值)例如:用 $('.todo-input').val()
就可以取得 .todo-input
的值
.append()
可以依序往後插入元素範例:
$(document).ready(function() {
$('.btn').click(function (e) {
const value = $('.todo-input').val(); // 取出 .todo-input 的值
$('.todo-input').val(''); // 清空 .todo-input 的值
$('.todos').append(`<div class="todo">${value}</div>`);
})
})
.prepend()
可以依序往前插入元素.css('color', 'red')
可以調整元素的 css,把 color 設為 red.empty()
可以清空元素裡面的所有東西.empty()
就等於是 .innerHTML = ''
,元素本身還是會留著
但如果是用 .remove()
的話,連元素本身也都會被移除掉
範例:
在 .todos
加上 event listener,監聽 .todos
裡面的 .btn-delete
,只要 .btn-delete
有 click 事件,就會執行後面 function 的程式碼 $(e.target).parent().fadeOut();
// 刪除功能
$('.todos').on('click', '.btn-delete', function(e) {
$(e.target).parent().fadeOut();
})
html:
<input type="text" class="todo-input">
<button class="btn">Add todo</button>
<button class="btn-remove-all">Remove all todos</button>
<div class="todos">
</div>
all.js:
$(document).ready(function() {
$('.btn').click(function (e) {
const value = $('.todo-input').val(); // 取出 .todo-input 的值
$('.todo-input').val(''); // 清空 .todo-input 的值
$('.todos').append(`
<div class="todo">
${value}
<button class="btn-mark">標記完成</button>
<button class="btn-delete">刪除</button>
</div>
`);
})
// 移除所有的 todo
$('.btn-remove-all').click(() => {
$('.todos').empty();
})
// 刪除功能
$('.todos').on('click', '.btn-delete', function(e) {
$(e.target).parent().fadeOut();
})
// 標記完成/未完成
$('.todos').on('click', '.btn-mark', function(e) {
const todo = $(e.target).parent();
if (todo.hasClass('completed')) { // 變成未完成
todo.css('color', 'black');
todo.removeClass('completed');
$(e.target).text('標記完成');
} else { // 變成已完成
todo.css('color', 'green');
todo.addClass('completed');
$(e.target).text('標記未完成');
}
})
})
jQuery 與 Ajax 的官方文件,可參考 Low-Level Interface
在接下來的範例中,會使用 REST COUNTRIES 這個 api
$.ajax()
就是一個 function,小括弧裡面要傳入幾個參數,參數可以有幾種形式:
$.ajax({
method: "POST",
url: "some.php",
data: { name: "John", location: "Boston" }
})
.done(function( msg ) {
alert( "Data Saved: " + msg );
});
$.ajax()
這個 function 來發送 request下面這段程式碼,就可以發送一個 GET 的 request 到 https://restcountries.eu/rest/v2/name/germany 這個 api 去
$.ajax({
method: 'GET',
url: 'https://restcountries.eu/rest/v2/name/germany'
})
.done()
來拿到 response發送完 request 之後,要接收 response:
用 .done()
來接收結果,小括號裡面傳入一個 callback function,把拿到的 data
印出來
$.ajax({
method: 'GET',
url: 'https://restcountries.eu/rest/v2/name/germany'
}).done(function(data) {
console.log(data);
})
用 .fail()
可以接收錯誤訊息
$.ajax({
method: 'GET',
url: 'https://aaarestcountries.eu/rest/v2/name/germany'
}).done(function(data) {
console.log(data);
}).fail(function(err) {
console.log('error: ', err);
})
在 You Might Not Need jQuery 有提供另一種寫法:
同樣也可以拿到結果和錯誤處理
$.ajax({
method: 'GET',
url: 'https://aaarestcountries.eu/rest/v2/name/germany',
success: data => console.log(data),
error: err => console.log('error: ', err)
})
按下送出後去 api 拿資料,再把拿到的資料顯示在下方
html:
Name: <input type="text" name="country-name">
<button class="btn">送出</button>
<div class="list">
</div>
all.js:
$(document).ready(() => {
$('.btn').click(() => {
const value = $('input[name=country-name]').val();
// 如果欄位沒有填寫
if (value === '') {
alert('必須輸入名稱!');
return;
}
$('.list').empty(); // 先把 .list 清空
// 如果欄位有填寫
$.ajax({
method: 'GET',
url: 'https://restcountries.eu/rest/v2/name/' + value,
success: countries => {
for (let country of countries) {
$('.list').append(`<div>${country.alpha2Code} ${country.name} ${country.nativeName}</div>`)
}
},
error: err => {
alert('系統不穩定!');
}
})
})
})
]]>
透過 PHP 從 MySQL 資料庫拿資料的步驟如下:
query()
$conn->query('select now();');
就是在「向資料庫拿資料」$result
是否有拿到結果$result
是空的)就代表 query()
發生錯誤。這時,就把錯誤印出來,程式碼不再繼續往下執行$result
有拿到結果,就用 $row = $result->fetch_assoc();
fetch_assoc()
是在把相對應的結果取出來放到 $row
裡面。$row
就是 MySQL query 之後的結果,會根據我 select 的東西給我一個 array,陣列的 key 就是「我 select 的東西 now()
」,value 就是「now()
所對應到的值」now()
是 MySQL 提供的一個 function,可以取得現在的時間
data.php:
<?php
require_once('conn.php');
$result = $conn->query('select now() as n;');
if (!$result) {
die($conn->error);
}
$row = $result->fetch_assoc();
print_r($row);
echo '<br> now: ' . $row['n'];
?>
output:
現在,在 MySQL 資料庫裡有這三筆資料:
<?php
require_once('conn.php');
$result = $conn->query('select * from users;');
if (!$result) {
die($conn->error);
}
$row = $result->fetch_assoc();
print_r($row);
?>
output:
可以看到,每執行一次 $row = $result->fetch_assoc();
只會印出一筆資料而已
因此,如果想要取得每一筆資料,就需要跑 while
迴圈
while
迴圈取得 table 中的每一筆資料在跑每圈 while
迴圈時,實際上是分成兩個步驟
$result->fetch_assoc()
,並把結果放到 $row
while
實際上會判斷的是 $row
,判斷 $row
是否為空(當資料都拿完後,$row
就會是空的),$row
是空的就是 false<?php
require_once('conn.php');
$result = $conn->query('select * from users;');
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
print_r($row);
}
?>
output:
可以看到,這樣就可以拿到 table 中的每一筆資料了
把資料用我想要的樣子顯示出來:
<?php
require_once('conn.php');
$result = $conn->query('select * from users;');
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
echo 'id: '. $row['id'] . '<br>';
echo 'username: ' . $row['username'] . '<br>';
}
?>
output:
add.php:
insert into users(username) values("apple")
這就是一個 SQL query<?php
require_once('conn.php');
$result = $conn->query('insert into users(username) values("apple")');
if (!$result) {
die($conn->error);
}
print_r($result);
?>
print_r($result);
的 output 會是 1,
1 就代表 true (有成功)的意思
會發現,當我在 add.php 的頁面第二次重新整理後,回到 phpMyAdmin 看,就會出現第二個 apple
add.php:
$username
取代$sql
印出來,看是不是正確的 SQL query把 SQL query 獨立出來變成一個變數 $sql
,這樣程式碼的可讀性比較高。在 debug 時,也可以先把 $sql
印出來 echo $sql;
,看是不是正確的 SQL query
debug 完之後,記得要把 echo $sql;
拿掉
sprintf()
函式來寫 SQL query,提高程式碼可讀性在 PHP 有一個叫做 sprintf()
的函式,建議是把 SQL query 用 sprintf()
來寫,讓整個 SQL query 字串更好寫也更好看懂:
%d
和 "%s"
,類似於 placeholder 的感覺%d
代表:我要放入的是一個 number (這裡的 d 是 decimal 十進位的意思)"%s"
代表:我要放入的是一個 string會按照順序,把 15
帶到 %d
的位置,把 $username
帶到 "%s"
的位置
<?php
require_once('conn.php');
$username = 'apple';
$sql = sprintf(
'insert into users(id, username) values(%d, "%s")',
15,
$username
);
echo $sql;
exit();
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
print_r($result);
?>
組出來的 SQL query 會長這樣:
index.php:
<?php
require_once('conn.php');
$result = $conn->query('select * from users;');
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
echo 'id: '. $row['id'] . '<br>';
echo 'username: ' . $row['username'] . '<br>';
}
?>
<h2>新增 user</h2>
<form method="POST" action="add.php">
username: <input name="username">
<input type="submit">
</form>
header('Location: index.php');
意思就是:我要回傳一個 response Header 叫做 Location: index.php
。瀏覽器接收到這個 response Header 後,就知道我的目的是要跳轉到 index.php,因此就會自動幫我跳轉回到 index.php 這個檔案去 (因為跳轉的太快了,甚至不會看到中間的 add.php 的畫面,就直接跳轉到 index.php 去了)
add.php:
<?php
require_once('conn.php');
if (empty($_POST['username'])) {
die('請輸入 username');
}
$username = $_POST['username'];
$sql = sprintf(
'insert into users(username) values("%s")',
$username
);
echo 'sql: ' . $sql. '<br>';
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
// 如果有新增成功
echo '新增成功!';
header('Location: index.php');
?>
上面 add.php 的程式碼,做錯誤處理的地方有兩個:
$_POST['username']
是否為空值$conn->query($sql);
這段$result
是 false,那就會執行 die($conn->error);
現在,我把 username 欄位設為 unique (代表:不能有重複的 username)
然後我到 index.php 輸入一個重複的 username 叫做 aaa,按下 submit 後就會出現一行錯誤訊息「Duplicate entry 'aaa' for key 'username'」,就是因為在執行到 $conn->query($sql);
時發生錯誤($result
會是 false),因此這行錯誤訊息就是從 die($conn->error);
這行印出來的
「資料庫的讀取結果」的排序不一定會按照 id 順序,如果想要按照 id 排序的話,就在 index.php 的 SQL query 加上 order by id asc
或是 order by id desc
asc
是「由小到大」排列<?php
require_once('conn.php');
$result = $conn->query('select * from users order by id asc;');
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
echo 'id: '. $row['id'] . '<br>';
echo 'username: ' . $row['username'] . '<br>';
}
?>
<h2>新增 user</h2>
<form method="POST" action="add.php">
username: <input name="username">
<input type="submit">
</form>
desc
是「由大到小」排列<?php
require_once('conn.php');
$result = $conn->query('select * from users order by id desc;');
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
echo 'id: '. $row['id'] . '<br>';
echo 'username: ' . $row['username'] . '<br>';
}
?>
<h2>新增 user</h2>
<form method="POST" action="add.php">
username: <input name="username">
<input type="submit">
</form>
conn.php:
<?php
$server_name = 'localhost';
$username = 'saffran';
$password = 'rox';
$db_name = 'saffran_db';
$conn = new mysqli($server_name, $username, $password, $db_name);
if ($conn->connect_error) {
die('資料庫連線錯誤:' . $conn->connect_error);
}
$conn->query('SET NAMES UTF8');
$conn->query('SET time_zone = "+8:00"');
?>
index.php:
<?php
require_once('conn.php');
$result = $conn->query('select * from users order by id desc;');
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
echo 'id: '. $row['id'] . '<br>';
echo 'username: ' . $row['username'] . '<br>';
}
?>
<h2>新增 user</h2>
<form method="POST" action="add.php">
username: <input name="username">
<input type="submit">
</form>
add.php:
<?php
require_once('conn.php');
if (empty($_POST['username'])) {
die('請輸入 username');
}
$username = $_POST['username'];
$sql = sprintf(
'insert into users(username) values("%s")',
$username
);
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
// 如果有新增成功
header('Location: index.php');
?>
index.php:
echo '<a href="delete.php?id=' . $row['id'] . '">刪除</a>';
用 GET(query string)的方式把 id 帶去給 delete.php 這個檔案
通常,刪除功能會用 POST 來傳參數(傳多個參數尤其要用 POST),這邊只是為了方便講解所以用 GET 來傳參數
如果要用 POST 來傳參數的話,就要把 '<a href="delete.php?id=' . $row['id'] . '">刪除</a>'
這段改成 form 的形式
<?php
require_once('conn.php');
$result = $conn->query('select * from users order by id asc;');
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
echo 'id: ' . $row['id'];
echo '<a href="delete.php?id=' . $row['id'] . '">刪除</a>';
echo '<br>';
echo 'username: ' . $row['username'] . '<br>';
}
?>
<h2>新增 user</h2>
<form method="POST" action="add.php">
username: <input name="username">
<input type="submit">
</form>
output:
這時,當我點擊 id: 7 的刪除按鈕時,就會跳到 http://localhost:8080/saffran/delete.php?id=7 這裡(delete.php 就可以用 $_GET
這個變數來拿到 id 的值)
在 delete.php 就這樣寫,就寫好刪除的功能了:
<?php
require_once('conn.php');
if (empty($_GET['id'])) {
die('請輸入 id');
}
$id = $_GET['id'];
$sql = sprintf(
'delete from users where id = %d',
$id
);
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
// 如果刪除成功
header('Location: index.php');
?>
現在,在資料庫裡面有下面這些資料:
我在網址列執行 http://localhost:8080/saffran/delete.php?id=330 並不會跳出錯誤訊息
或者是
我在 phpMyAdmin 裡面執行 DELETE FROM users WHERE id = 330
這個 query,結果也會是「執行成功」
雖然我在資料庫裡面沒有 id = 330 這筆資料,但是這個 query 還是會執行成功,只是它是刪除了一個不存在的資料(影響了 0 列),但這不算是錯誤
執行錯誤指的是:例如 table 名稱輸入錯誤、欄位名稱輸入錯誤等等
$conn->affected_rows
來得知「影響了幾列」$conn->affected_rows
是 0,就代表「影響了 0 列」,刪除了一筆不存在的資料$conn->affected_rows
>= 1,就代表「有刪除了 xx 筆資料庫的資料」delete.php:
<?php
require_once('conn.php');
if (empty($_GET['id'])) {
die('請輸入 id');
}
$id = $_GET['id'];
$sql = sprintf(
'delete from users where id = %d',
$id
);
echo $sql . '<br>';
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
// 影響了幾列
if ($conn->affected_rows >= 1) {
echo '已成功刪除此筆資料!';
} else {
echo '查無此資料';
}
// 如果刪除成功
// header('Location: index.php');
?>
index.php:
<?php
require_once('conn.php');
$result = $conn->query('select * from users order by id asc;');
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
echo 'id: ' . $row['id'];
echo '<a href="delete.php?id=' . $row['id'] . '">刪除</a>';
echo '<br>';
echo 'username: ' . $row['username'] . '<br>';
}
?>
<h2>新增 user</h2>
<form method="POST" action="add.php">
username: <input name="username">
<input type="submit">
</form>
<h2>編輯 user</h2>
<form method="POST" action="update.php">
id: <input name="id">
username: <input name="username">
<input type="submit">
</form>
輸入 id 和 username 就可以編輯資料
update.php:
'update users set username = "%s" where id = %d'
這句 SQL query 的意思是:會找到 where id = %d
所指定的 id,再針對該 id 的那一列去更新它的 username
<?php
require_once('conn.php');
if (empty($_POST['id'] || empty($_POST['username']))) {
die('請輸入 id 與 username');
}
$id = $_POST['id'];
$username = $_POST['username'];
$sql = sprintf(
'update users set username = "%s" where id = %d',
$username,
$id
);
echo $sql . '<br>';
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
// 如果編輯成功
header('Location: index.php');
?>
<?php
// 連線資料庫
$server_name = 'localhost';
$username = 'huli';
$password = 'huli';
$db_name = 'huli';
$conn = new mysqli($server_name, $username, $password, $db_name);
if ($conn->connect_error) {
die('資料庫連線錯誤:' . $conn->connect_error);
}
$conn->query('SET NAMES UTF8');
$conn->query('SET time_zone = "+8:00"');
// 新增資料
$username = $_POST['username'];
$sql = sprintf(
"insert into users(username) values('%s')",
$username
);
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
// 讀取資料
$result = $conn->query("SELECT * FROM users ORDER BY id ASC;");
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
echo "id:" . $row['id'];
}
// 修改資料
$id = $_POST['id'];
$username = $_POST['username'];
$sql = sprintf(
"update users set username='%s' where id=%d",
$username,
$id
);
echo $sql . '<br>';
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
// 刪除資料
$id = $_GET['id'];
$sql = sprintf(
"delete from users where id = %d",
$id
);
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
if ($conn->affected_rows >= 1) {
echo '刪除成功';
} else {
echo '查無資料';
}
?>
]]>透過 PHP 從 MySQL 資料庫拿資料的步驟如下:
query()
$conn->query('select now();');
就是在「向資料庫拿資料」$result
是否有拿到結果$result
是空的)就代表 query()
發生錯誤。這時,就把錯誤印出來,程式碼不再繼續往下執行$result
有拿到結果,就用 $row = $result->fetch_assoc();
fetch_assoc()
是在把相對應的結果取出來放到 $row
裡面。$row
就是 MySQL query 之後的結果,會根據我 select 的東西給我一個 array,陣列的 key 就是「我 select 的東西 now()
」,value 就是「now()
所對應到的值」now()
是 MySQL 提供的一個 function,可以取得現在的時間
data.php:
<?php
require_once('conn.php');
$result = $conn->query('select now() as n;');
if (!$result) {
die($conn->error);
}
$row = $result->fetch_assoc();
print_r($row);
echo '<br> now: ' . $row['n'];
?>
output:
現在,在 MySQL 資料庫裡有這三筆資料:
<?php
require_once('conn.php');
$result = $conn->query('select * from users;');
if (!$result) {
die($conn->error);
}
$row = $result->fetch_assoc();
print_r($row);
?>
output:
可以看到,每執行一次 $row = $result->fetch_assoc();
只會印出一筆資料而已
因此,如果想要取得每一筆資料,就需要跑 while
迴圈
while
迴圈取得 table 中的每一筆資料在跑每圈 while
迴圈時,實際上是分成兩個步驟
$result->fetch_assoc()
,並把結果放到 $row
while
實際上會判斷的是 $row
,判斷 $row
是否為空(當資料都拿完後,$row
就會是空的),$row
是空的就是 false<?php
require_once('conn.php');
$result = $conn->query('select * from users;');
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
print_r($row);
}
?>
output:
可以看到,這樣就可以拿到 table 中的每一筆資料了
把資料用我想要的樣子顯示出來:
<?php
require_once('conn.php');
$result = $conn->query('select * from users;');
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
echo 'id: '. $row['id'] . '<br>';
echo 'username: ' . $row['username'] . '<br>';
}
?>
output:
add.php:
insert into users(username) values("apple")
這就是一個 SQL query<?php
require_once('conn.php');
$result = $conn->query('insert into users(username) values("apple")');
if (!$result) {
die($conn->error);
}
print_r($result);
?>
print_r($result);
的 output 會是 1,
1 就代表 true (有成功)的意思
會發現,當我在 add.php 的頁面第二次重新整理後,回到 phpMyAdmin 看,就會出現第二個 apple
add.php:
$username
取代$sql
印出來,看是不是正確的 SQL query把 SQL query 獨立出來變成一個變數 $sql
,這樣程式碼的可讀性比較高。在 debug 時,也可以先把 $sql
印出來 echo $sql;
,看是不是正確的 SQL query
debug 完之後,記得要把 echo $sql;
拿掉
sprintf()
函式來寫 SQL query,提高程式碼可讀性在 PHP 有一個叫做 sprintf()
的函式,建議是把 SQL query 用 sprintf()
來寫,讓整個 SQL query 字串更好寫也更好看懂:
%d
和 "%s"
,類似於 placeholder 的感覺%d
代表:我要放入的是一個 number (這裡的 d 是 decimal 十進位的意思)"%s"
代表:我要放入的是一個 string會按照順序,把 15
帶到 %d
的位置,把 $username
帶到 "%s"
的位置
<?php
require_once('conn.php');
$username = 'apple';
$sql = sprintf(
'insert into users(id, username) values(%d, "%s")',
15,
$username
);
echo $sql;
exit();
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
print_r($result);
?>
組出來的 SQL query 會長這樣:
index.php:
<?php
require_once('conn.php');
$result = $conn->query('select * from users;');
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
echo 'id: '. $row['id'] . '<br>';
echo 'username: ' . $row['username'] . '<br>';
}
?>
<h2>新增 user</h2>
<form method="POST" action="add.php">
username: <input name="username">
<input type="submit">
</form>
header('Location: index.php');
意思就是:我要回傳一個 response Header 叫做 Location: index.php
。瀏覽器接收到這個 response Header 後,就知道我的目的是要跳轉到 index.php,因此就會自動幫我跳轉回到 index.php 這個檔案去 (因為跳轉的太快了,甚至不會看到中間的 add.php 的畫面,就直接跳轉到 index.php 去了)
add.php:
<?php
require_once('conn.php');
if (empty($_POST['username'])) {
die('請輸入 username');
}
$username = $_POST['username'];
$sql = sprintf(
'insert into users(username) values("%s")',
$username
);
echo 'sql: ' . $sql. '<br>';
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
// 如果有新增成功
echo '新增成功!';
header('Location: index.php');
?>
上面 add.php 的程式碼,做錯誤處理的地方有兩個:
$_POST['username']
是否為空值$conn->query($sql);
這段$result
是 false,那就會執行 die($conn->error);
現在,我把 username 欄位設為 unique (代表:不能有重複的 username)
然後我到 index.php 輸入一個重複的 username 叫做 aaa,按下 submit 後就會出現一行錯誤訊息「Duplicate entry 'aaa' for key 'username'」,就是因為在執行到 $conn->query($sql);
時發生錯誤($result
會是 false),因此這行錯誤訊息就是從 die($conn->error);
這行印出來的
「資料庫的讀取結果」的排序不一定會按照 id 順序,如果想要按照 id 排序的話,就在 index.php 的 SQL query 加上 order by id asc
或是 order by id desc
asc
是「由小到大」排列<?php
require_once('conn.php');
$result = $conn->query('select * from users order by id asc;');
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
echo 'id: '. $row['id'] . '<br>';
echo 'username: ' . $row['username'] . '<br>';
}
?>
<h2>新增 user</h2>
<form method="POST" action="add.php">
username: <input name="username">
<input type="submit">
</form>
desc
是「由大到小」排列<?php
require_once('conn.php');
$result = $conn->query('select * from users order by id desc;');
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
echo 'id: '. $row['id'] . '<br>';
echo 'username: ' . $row['username'] . '<br>';
}
?>
<h2>新增 user</h2>
<form method="POST" action="add.php">
username: <input name="username">
<input type="submit">
</form>
conn.php:
<?php
$server_name = 'localhost';
$username = 'saffran';
$password = 'rox';
$db_name = 'saffran_db';
$conn = new mysqli($server_name, $username, $password, $db_name);
if ($conn->connect_error) {
die('資料庫連線錯誤:' . $conn->connect_error);
}
$conn->query('SET NAMES UTF8');
$conn->query('SET time_zone = "+8:00"');
?>
index.php:
<?php
require_once('conn.php');
$result = $conn->query('select * from users order by id desc;');
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
echo 'id: '. $row['id'] . '<br>';
echo 'username: ' . $row['username'] . '<br>';
}
?>
<h2>新增 user</h2>
<form method="POST" action="add.php">
username: <input name="username">
<input type="submit">
</form>
add.php:
<?php
require_once('conn.php');
if (empty($_POST['username'])) {
die('請輸入 username');
}
$username = $_POST['username'];
$sql = sprintf(
'insert into users(username) values("%s")',
$username
);
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
// 如果有新增成功
header('Location: index.php');
?>
index.php:
echo '<a href="delete.php?id=' . $row['id'] . '">刪除</a>';
用 GET(query string)的方式把 id 帶去給 delete.php 這個檔案
通常,刪除功能會用 POST 來傳參數(傳多個參數尤其要用 POST),這邊只是為了方便講解所以用 GET 來傳參數
如果要用 POST 來傳參數的話,就要把 '<a href="delete.php?id=' . $row['id'] . '">刪除</a>'
這段改成 form 的形式
<?php
require_once('conn.php');
$result = $conn->query('select * from users order by id asc;');
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
echo 'id: ' . $row['id'];
echo '<a href="delete.php?id=' . $row['id'] . '">刪除</a>';
echo '<br>';
echo 'username: ' . $row['username'] . '<br>';
}
?>
<h2>新增 user</h2>
<form method="POST" action="add.php">
username: <input name="username">
<input type="submit">
</form>
output:
這時,當我點擊 id: 7 的刪除按鈕時,就會跳到 http://localhost:8080/saffran/delete.php?id=7 這裡(delete.php 就可以用 $_GET
這個變數來拿到 id 的值)
在 delete.php 就這樣寫,就寫好刪除的功能了:
<?php
require_once('conn.php');
if (empty($_GET['id'])) {
die('請輸入 id');
}
$id = $_GET['id'];
$sql = sprintf(
'delete from users where id = %d',
$id
);
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
// 如果刪除成功
header('Location: index.php');
?>
現在,在資料庫裡面有下面這些資料:
我在網址列執行 http://localhost:8080/saffran/delete.php?id=330 並不會跳出錯誤訊息
或者是
我在 phpMyAdmin 裡面執行 DELETE FROM users WHERE id = 330
這個 query,結果也會是「執行成功」
雖然我在資料庫裡面沒有 id = 330 這筆資料,但是這個 query 還是會執行成功,只是它是刪除了一個不存在的資料(影響了 0 列),但這不算是錯誤
執行錯誤指的是:例如 table 名稱輸入錯誤、欄位名稱輸入錯誤等等
$conn->affected_rows
來得知「影響了幾列」$conn->affected_rows
是 0,就代表「影響了 0 列」,刪除了一筆不存在的資料$conn->affected_rows
>= 1,就代表「有刪除了 xx 筆資料庫的資料」delete.php:
<?php
require_once('conn.php');
if (empty($_GET['id'])) {
die('請輸入 id');
}
$id = $_GET['id'];
$sql = sprintf(
'delete from users where id = %d',
$id
);
echo $sql . '<br>';
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
// 影響了幾列
if ($conn->affected_rows >= 1) {
echo '已成功刪除此筆資料!';
} else {
echo '查無此資料';
}
// 如果刪除成功
// header('Location: index.php');
?>
index.php:
<?php
require_once('conn.php');
$result = $conn->query('select * from users order by id asc;');
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
echo 'id: ' . $row['id'];
echo '<a href="delete.php?id=' . $row['id'] . '">刪除</a>';
echo '<br>';
echo 'username: ' . $row['username'] . '<br>';
}
?>
<h2>新增 user</h2>
<form method="POST" action="add.php">
username: <input name="username">
<input type="submit">
</form>
<h2>編輯 user</h2>
<form method="POST" action="update.php">
id: <input name="id">
username: <input name="username">
<input type="submit">
</form>
輸入 id 和 username 就可以編輯資料
update.php:
'update users set username = "%s" where id = %d'
這句 SQL query 的意思是:會找到 where id = %d
所指定的 id,再針對該 id 的那一列去更新它的 username
<?php
require_once('conn.php');
if (empty($_POST['id'] || empty($_POST['username']))) {
die('請輸入 id 與 username');
}
$id = $_POST['id'];
$username = $_POST['username'];
$sql = sprintf(
'update users set username = "%s" where id = %d',
$username,
$id
);
echo $sql . '<br>';
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
// 如果編輯成功
header('Location: index.php');
?>
<?php
// 連線資料庫
$server_name = 'localhost';
$username = 'huli';
$password = 'huli';
$db_name = 'huli';
$conn = new mysqli($server_name, $username, $password, $db_name);
if ($conn->connect_error) {
die('資料庫連線錯誤:' . $conn->connect_error);
}
$conn->query('SET NAMES UTF8');
$conn->query('SET time_zone = "+8:00"');
// 新增資料
$username = $_POST['username'];
$sql = sprintf(
"insert into users(username) values('%s')",
$username
);
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
// 讀取資料
$result = $conn->query("SELECT * FROM users ORDER BY id ASC;");
if (!$result) {
die($conn->error);
}
while ($row = $result->fetch_assoc()) {
echo "id:" . $row['id'];
}
// 修改資料
$id = $_POST['id'];
$username = $_POST['username'];
$sql = sprintf(
"update users set username='%s' where id=%d",
$username,
$id
);
echo $sql . '<br>';
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
// 刪除資料
$id = $_GET['id'];
$sql = sprintf(
"delete from users where id = %d",
$id
);
$result = $conn->query($sql);
if (!$result) {
die($conn->error);
}
if ($conn->affected_rows >= 1) {
echo '刪除成功';
} else {
echo '查無資料';
}
?>
]]>
進入 http://localhost:8080 頁面,打開 devtool 的 Network tab,把 Disable cache 勾起來
如果之後碰到問題時,就要打開 devtool 切到 Network tab,按下重新整理,頁面的所有資源就會被重新載入(清空 cache)
注意!一定要在 devtool 打開的情況下才會清空 cache
例如:
style.css 的內容是這樣:
body{
background-color: orange;
}
當我用網頁去存取 style.css 時,檔案內容就會原封不動的傳回來
即使在 css 的檔案內容中有 php 的程式碼,但是對於 server 來說,「.css 結尾的」就是靜態檔案,因此還是會把檔案內容原封不動的傳回來
style.css:
body{
background-color: <?php echo 'orange'; ?>;
}
Apache server 會回傳 php 執行後的結果,是因為 Apache 這個 server 有做一些設定,它才會去執行 php 檔案
例如:
test.php 的內容是這樣:
這段 php 程式碼執行完後,會輸出的結果是「I am Mickey!」
<?php
echo 'I am Mickey!';
?>
當我用網頁去存取 test.php 時,回傳的內容會是上面那段 php 程式碼輸出的內容「I am Mickey!」,而不會是一整段 php 的程式碼
<?php ?>
包住的內容才會被 php 執行,在 <?php ?>
外面的內容也是會原封不動的傳回來(以純文字的方式)test.php:
<?php
echo 'I am Mickey!';
?>
body{
background-color: orange;
}
所以,就可以利用 php 的這個特性,來做出一個動態的網頁,例如:印出當下的時間
test.php:
<?php
echo 'I am Mickey!';
?>
<h1>Now: <?php echo date('Y-m-d H:i:s'); ?></h1>
例如,如果把 test.php 檔案丟到 GitHub 上面,也會看到是「原封不動的回傳檔案內容,是純文字」,而不會是 php 執行後的結果
可以用 GET 或是 POST 的方式發送 request
a=1
這個 query string 會自動存到 data.php 檔案的 $_GET
變數裡面(data.php 會自動幫我準備好 $_GET
這個特殊的變數)
$_GET
這個變數來取得我用 query string 所傳的參數範例如下
data.php:
<?php
echo 'Great! <br>';
echo 'a: ' . $_GET['a'] . '<br>';
echo 'b: ' . $_GET['b'] . '<br>';
print_r($_GET);
?>
output:
isset()
這個 function 來判斷是否有設置這個 index(key)用 isset($_GET['a'])
判斷:如果有設置 a 這個 index 的話,才執行 echo 'a: ' . $_GET['a'] . '<br>';
這段
<?php
echo 'Great! <br>';
if (isset($_GET['a'])) {
echo 'a: ' . $_GET['a'] . '<br>';
}
if (isset($_GET['b'])) {
echo 'b: ' . $_GET['b'] . '<br>';
}
print_r($_GET);
?>
output:
範例如下
我在 index.php 寫了一個 form
在 form 裡面有幾個參數要填:
input
的 name
屬性」就會是「變數 $_GET
所接收到的 index 值」index.php:
<form method="GET" action="data.php">
username: <input name="username">
age: <input name="age">
<input type="submit">
</form>
填好資料,按下 submit 後,就會跳到 data.php 的頁面
data.php 的內容如下:
<?php
echo 'Great! <br>';
print_r($_GET);
?>
瀏覽器就會用 GET 的方式,自動把我剛剛在 form 填寫的值帶到 URL 去
在 index.php 打開 devtool 的 Network tab,把 Preserve log 勾起來,一樣填好表單後按下 submit,在下方就可以看到:
form 幫我做的事情就是:用 query string 的方式把我填的值帶到 URL 去,然後送出一個 GET 的 request 到這個 URL
index.php:
<form method="GET" action="data.php">
username: <input name="username">
age: <input name="age">
<input type="submit">
</form>
isset()
來檢查欄位是否有填寫data.php:
<?php
if (!isset($_GET['username']) || !isset($_GET['age'])) {
echo '資料有缺,請再次填寫';
} else {
echo 'Hello! ' . $_GET['username']. '<br>';
echo 'Your age is ' . $_GET['age'];
}
?>
如果用 isset()
來檢查欄位是否有填寫,會發現:即使欄位沒有填寫,按下 submit 後還是不會印出「資料有缺,請再次填寫」的訊息
原因為:
isset()
是用來檢查「此 key 是否有被設置」的,所以就算我沒有填寫該欄位(value 是空值),但是因為兩個欄位的 key 是有傳入的(有設置 username
和 age
),所以 isset($_GET['username'])
和 isset($_GET['age'])
的結果都會是 true
必須要「沒有設置這個 key」,isset()
才會是 false
empty()
來檢查是否為空值針對這種欄位的檢查,要使用 empty()
來檢查是否為空值
另一個要補充的點是,比起上面用 else
,更推薦使用 exit()
來達到這樣的效果:出現「資料有缺,請再次填寫」後就跳開,讓程式碼不要繼續執行下去印出下面的 Hello! 這些字
data.php:
<?php
if (empty($_GET['username']) || empty($_GET['age'])) {
echo '資料有缺,請再次填寫';
exit();
}
echo 'Hello! ' . $_GET['username']. '<br>';
echo 'Your age is ' . $_GET['age'];
?>
如果把 method 改成 POST
index.php:
<form method="POST" action="data.php">
username: <input name="username">
age: <input name="age">
<input type="submit">
</form>
按下 submit 後,會發現 URL 不會帶入我填寫的值,原因為:
我沒有用 GET 的方式帶入 query string
打開 devtool 可以看到:
最底下有一個 Form Data 寫著我剛剛填寫的資料,代表:我用 POST 的方式,把表單的資料送到 data.php 了,只是因為 data.php 目前還沒去處理這些資料
$_POST
去存取資料已經用 POST 的方式,把資料送到 data.php 去了
URL: http://localhost:8080/saffran/data.php
method: POST
PHP 會幫我準備好一個特別的變數 $_POST
,去自動存取 Form Data 的資料
因此,我就可以用 $_POST
去存取到我用 POST 送到 data.php 的資料了
index.php:
<form method="POST" action="data.php">
username: <input name="username">
age: <input name="age">
<input type="submit">
</form>
data.php:
<?php
if (empty($_POST['username']) || empty($_POST['age'])) {
echo '資料有缺,請再次填寫';
exit();
}
echo 'Hello! ' . $_POST['username']. '<br>';
echo 'Your age is ' . $_POST['age']. '<br>';
print_r($_POST);
?>
在 Operations 底下可以調整資料庫的編碼,選擇 utf8mb4_general_ci 或是 utf8mb4_unicode_ci
data.php:
<?php
$server_name = 'localhost';
$username = 'saffran';
$password = 'rox';
$db_name = 'saffran_db';
$conn = new mysqli($server_name, $username, $password, $db_name);
?>
$conn = new mysqli($server_name, $username, $password, $db_name);
意思就是:我要 new 一個 mysqli 的 instance 出來(用來跟資料庫建立連線),跟資料庫建立連線之後,會回傳一些資訊到 $conn
變數
conn
是 connection 的簡寫
->
這個符號$conn
變數會是一個物件,此物件裡面有一個屬性叫做 connect_error
<?php
$server_name = 'localhost';
$username = 'saffran';
$password = 'rox';
$db_name = 'saffran_db33';
$conn = new mysqli($server_name, $username, $password, $db_name);
echo $conn->connect_error;
?>
我故意輸入一個錯誤的 db 名稱,然後用 echo $conn->connect_error;
印出來看看
echo $conn->connect_error;
印出的是下方紅色箭頭那句
之所以上面還會出現那段 Warning,是因為我的 PHP 有打開這個「顯示 Warning」的設定,這樣比較方便 debug
因此,可以這樣寫:
如果 $conn->connect_error
不是空值,那就顯示「資料庫連線錯誤:...」的提示訊息
<?php
$server_name = 'localhost';
$username = 'saffran';
$password = 'rox';
$db_name = 'saffran_db33';
$conn = new mysqli($server_name, $username, $password, $db_name);
if (!empty($conn->connect_error)) {
echo '資料庫連線錯誤:' . $conn->connect_error . '<br>';
}
?>
或是也可以這樣寫:
die()
當資料庫連線錯誤時,就沒必要繼續往下執行程式碼了<?php
$server_name = 'localhost';
$username = 'saffran';
$password = 'rox';
$db_name = 'saffran_db33';
$conn = new mysqli($server_name, $username, $password, $db_name);
if (!empty($conn->connect_error)) {
die('資料庫連線錯誤:' . $conn->connect_error);
}
echo 'great!';
?>
die()
的作用是:會把 die()
小括號裡面的內容輸出之後,就讓整個 PHP 死掉,底下的程式碼都不會繼續執行
因此,echo 'great!';
不會被執行到
用 query()
這個 function 來改變資料庫的一些設定:編碼與時區
$conn->query('SET NAMES UTF8');
是「設定編碼」,如果沒有設定編碼的話,使用中文會出現亂碼$conn->query('SET time_zone = "+8:00"');
是「把資料庫的時區設為台灣的時區」注意!上面這兩行 query()
一定要放在 if (!empty($conn->connect_error)...
的後面,原因為:
連線完之後,要先檢查連線是否有錯誤。如果連線沒有錯誤,才繼續往下執行,這時候下 query()
去設定編碼與時區才不會出錯(如果連線錯誤,下 query()
就會出錯)
data.php:
<?php
$server_name = 'localhost';
$username = 'saffran';
$password = 'rox';
$db_name = 'saffran_db';
$conn = new mysqli($server_name, $username, $password, $db_name);
if (!empty($conn->connect_error) {
die('資料庫連線錯誤:' . $conn->connect_error);
}
$conn->query('SET NAMES UTF8');
$conn->query('SET time_zone = "+8:00"');
?>
也可以進一步簡化,把 empty()
拿掉:
<?php
$server_name = 'localhost';
$username = 'saffran';
$password = 'rox';
$db_name = 'saffran_db';
$conn = new mysqli($server_name, $username, $password, $db_name);
if ($conn->connect_error) {
die('資料庫連線錯誤:' . $conn->connect_error);
}
$conn->query('SET NAMES UTF8');
$conn->query('SET time_zone = "+8:00"');
?>
通常在實務上會這樣做:
建立一個檔案叫做 conn.php,裡面只放「連線資料庫的程式碼」
當我在 data.php 裡面要連線到資料庫時,我只需要用 require_once()
引入 conn.php 即可:
<?php
require_once('conn.php');
echo 'great!';
?>
當我要把我的程式碼放到 Git 上面時,會把 conn.php 排除掉(加在 .gitignore 裡面),因為 conn.php 裡面有我資料庫的位置、帳號密碼,不可以暴露在網路上
]]>進入 http://localhost:8080 頁面,打開 devtool 的 Network tab,把 Disable cache 勾起來
如果之後碰到問題時,就要打開 devtool 切到 Network tab,按下重新整理,頁面的所有資源就會被重新載入(清空 cache)
注意!一定要在 devtool 打開的情況下才會清空 cache
例如:
style.css 的內容是這樣:
body{
background-color: orange;
}
當我用網頁去存取 style.css 時,檔案內容就會原封不動的傳回來
即使在 css 的檔案內容中有 php 的程式碼,但是對於 server 來說,「.css 結尾的」就是靜態檔案,因此還是會把檔案內容原封不動的傳回來
style.css:
body{
background-color: <?php echo 'orange'; ?>;
}
Apache server 會回傳 php 執行後的結果,是因為 Apache 這個 server 有做一些設定,它才會去執行 php 檔案
例如:
test.php 的內容是這樣:
這段 php 程式碼執行完後,會輸出的結果是「I am Mickey!」
<?php
echo 'I am Mickey!';
?>
當我用網頁去存取 test.php 時,回傳的內容會是上面那段 php 程式碼輸出的內容「I am Mickey!」,而不會是一整段 php 的程式碼
<?php ?>
包住的內容才會被 php 執行,在 <?php ?>
外面的內容也是會原封不動的傳回來(以純文字的方式)test.php:
<?php
echo 'I am Mickey!';
?>
body{
background-color: orange;
}
所以,就可以利用 php 的這個特性,來做出一個動態的網頁,例如:印出當下的時間
test.php:
<?php
echo 'I am Mickey!';
?>
<h1>Now: <?php echo date('Y-m-d H:i:s'); ?></h1>
例如,如果把 test.php 檔案丟到 GitHub 上面,也會看到是「原封不動的回傳檔案內容,是純文字」,而不會是 php 執行後的結果
可以用 GET 或是 POST 的方式發送 request
a=1
這個 query string 會自動存到 data.php 檔案的 $_GET
變數裡面(data.php 會自動幫我準備好 $_GET
這個特殊的變數)
$_GET
這個變數來取得我用 query string 所傳的參數範例如下
data.php:
<?php
echo 'Great! <br>';
echo 'a: ' . $_GET['a'] . '<br>';
echo 'b: ' . $_GET['b'] . '<br>';
print_r($_GET);
?>
output:
isset()
這個 function 來判斷是否有設置這個 index(key)用 isset($_GET['a'])
判斷:如果有設置 a 這個 index 的話,才執行 echo 'a: ' . $_GET['a'] . '<br>';
這段
<?php
echo 'Great! <br>';
if (isset($_GET['a'])) {
echo 'a: ' . $_GET['a'] . '<br>';
}
if (isset($_GET['b'])) {
echo 'b: ' . $_GET['b'] . '<br>';
}
print_r($_GET);
?>
output:
範例如下
我在 index.php 寫了一個 form
在 form 裡面有幾個參數要填:
input
的 name
屬性」就會是「變數 $_GET
所接收到的 index 值」index.php:
<form method="GET" action="data.php">
username: <input name="username">
age: <input name="age">
<input type="submit">
</form>
填好資料,按下 submit 後,就會跳到 data.php 的頁面
data.php 的內容如下:
<?php
echo 'Great! <br>';
print_r($_GET);
?>
瀏覽器就會用 GET 的方式,自動把我剛剛在 form 填寫的值帶到 URL 去
在 index.php 打開 devtool 的 Network tab,把 Preserve log 勾起來,一樣填好表單後按下 submit,在下方就可以看到:
form 幫我做的事情就是:用 query string 的方式把我填的值帶到 URL 去,然後送出一個 GET 的 request 到這個 URL
index.php:
<form method="GET" action="data.php">
username: <input name="username">
age: <input name="age">
<input type="submit">
</form>
isset()
來檢查欄位是否有填寫data.php:
<?php
if (!isset($_GET['username']) || !isset($_GET['age'])) {
echo '資料有缺,請再次填寫';
} else {
echo 'Hello! ' . $_GET['username']. '<br>';
echo 'Your age is ' . $_GET['age'];
}
?>
如果用 isset()
來檢查欄位是否有填寫,會發現:即使欄位沒有填寫,按下 submit 後還是不會印出「資料有缺,請再次填寫」的訊息
原因為:
isset()
是用來檢查「此 key 是否有被設置」的,所以就算我沒有填寫該欄位(value 是空值),但是因為兩個欄位的 key 是有傳入的(有設置 username
和 age
),所以 isset($_GET['username'])
和 isset($_GET['age'])
的結果都會是 true
必須要「沒有設置這個 key」,isset()
才會是 false
empty()
來檢查是否為空值針對這種欄位的檢查,要使用 empty()
來檢查是否為空值
另一個要補充的點是,比起上面用 else
,更推薦使用 exit()
來達到這樣的效果:出現「資料有缺,請再次填寫」後就跳開,讓程式碼不要繼續執行下去印出下面的 Hello! 這些字
data.php:
<?php
if (empty($_GET['username']) || empty($_GET['age'])) {
echo '資料有缺,請再次填寫';
exit();
}
echo 'Hello! ' . $_GET['username']. '<br>';
echo 'Your age is ' . $_GET['age'];
?>
如果把 method 改成 POST
index.php:
<form method="POST" action="data.php">
username: <input name="username">
age: <input name="age">
<input type="submit">
</form>
按下 submit 後,會發現 URL 不會帶入我填寫的值,原因為:
我沒有用 GET 的方式帶入 query string
打開 devtool 可以看到:
最底下有一個 Form Data 寫著我剛剛填寫的資料,代表:我用 POST 的方式,把表單的資料送到 data.php 了,只是因為 data.php 目前還沒去處理這些資料
$_POST
去存取資料已經用 POST 的方式,把資料送到 data.php 去了
URL: http://localhost:8080/saffran/data.php
method: POST
PHP 會幫我準備好一個特別的變數 $_POST
,去自動存取 Form Data 的資料
因此,我就可以用 $_POST
去存取到我用 POST 送到 data.php 的資料了
index.php:
<form method="POST" action="data.php">
username: <input name="username">
age: <input name="age">
<input type="submit">
</form>
data.php:
<?php
if (empty($_POST['username']) || empty($_POST['age'])) {
echo '資料有缺,請再次填寫';
exit();
}
echo 'Hello! ' . $_POST['username']. '<br>';
echo 'Your age is ' . $_POST['age']. '<br>';
print_r($_POST);
?>
在 Operations 底下可以調整資料庫的編碼,選擇 utf8mb4_general_ci 或是 utf8mb4_unicode_ci
data.php:
<?php
$server_name = 'localhost';
$username = 'saffran';
$password = 'rox';
$db_name = 'saffran_db';
$conn = new mysqli($server_name, $username, $password, $db_name);
?>
$conn = new mysqli($server_name, $username, $password, $db_name);
意思就是:我要 new 一個 mysqli 的 instance 出來(用來跟資料庫建立連線),跟資料庫建立連線之後,會回傳一些資訊到 $conn
變數
conn
是 connection 的簡寫
->
這個符號$conn
變數會是一個物件,此物件裡面有一個屬性叫做 connect_error
<?php
$server_name = 'localhost';
$username = 'saffran';
$password = 'rox';
$db_name = 'saffran_db33';
$conn = new mysqli($server_name, $username, $password, $db_name);
echo $conn->connect_error;
?>
我故意輸入一個錯誤的 db 名稱,然後用 echo $conn->connect_error;
印出來看看
echo $conn->connect_error;
印出的是下方紅色箭頭那句
之所以上面還會出現那段 Warning,是因為我的 PHP 有打開這個「顯示 Warning」的設定,這樣比較方便 debug
因此,可以這樣寫:
如果 $conn->connect_error
不是空值,那就顯示「資料庫連線錯誤:...」的提示訊息
<?php
$server_name = 'localhost';
$username = 'saffran';
$password = 'rox';
$db_name = 'saffran_db33';
$conn = new mysqli($server_name, $username, $password, $db_name);
if (!empty($conn->connect_error)) {
echo '資料庫連線錯誤:' . $conn->connect_error . '<br>';
}
?>
或是也可以這樣寫:
die()
當資料庫連線錯誤時,就沒必要繼續往下執行程式碼了<?php
$server_name = 'localhost';
$username = 'saffran';
$password = 'rox';
$db_name = 'saffran_db33';
$conn = new mysqli($server_name, $username, $password, $db_name);
if (!empty($conn->connect_error)) {
die('資料庫連線錯誤:' . $conn->connect_error);
}
echo 'great!';
?>
die()
的作用是:會把 die()
小括號裡面的內容輸出之後,就讓整個 PHP 死掉,底下的程式碼都不會繼續執行
因此,echo 'great!';
不會被執行到
用 query()
這個 function 來改變資料庫的一些設定:編碼與時區
$conn->query('SET NAMES UTF8');
是「設定編碼」,如果沒有設定編碼的話,使用中文會出現亂碼$conn->query('SET time_zone = "+8:00"');
是「把資料庫的時區設為台灣的時區」注意!上面這兩行 query()
一定要放在 if (!empty($conn->connect_error)...
的後面,原因為:
連線完之後,要先檢查連線是否有錯誤。如果連線沒有錯誤,才繼續往下執行,這時候下 query()
去設定編碼與時區才不會出錯(如果連線錯誤,下 query()
就會出錯)
data.php:
<?php
$server_name = 'localhost';
$username = 'saffran';
$password = 'rox';
$db_name = 'saffran_db';
$conn = new mysqli($server_name, $username, $password, $db_name);
if (!empty($conn->connect_error) {
die('資料庫連線錯誤:' . $conn->connect_error);
}
$conn->query('SET NAMES UTF8');
$conn->query('SET time_zone = "+8:00"');
?>
也可以進一步簡化,把 empty()
拿掉:
<?php
$server_name = 'localhost';
$username = 'saffran';
$password = 'rox';
$db_name = 'saffran_db';
$conn = new mysqli($server_name, $username, $password, $db_name);
if ($conn->connect_error) {
die('資料庫連線錯誤:' . $conn->connect_error);
}
$conn->query('SET NAMES UTF8');
$conn->query('SET time_zone = "+8:00"');
?>
通常在實務上會這樣做:
建立一個檔案叫做 conn.php,裡面只放「連線資料庫的程式碼」
當我在 data.php 裡面要連線到資料庫時,我只需要用 require_once()
引入 conn.php 即可:
<?php
require_once('conn.php');
echo 'great!';
?>
當我要把我的程式碼放到 Git 上面時,會把 conn.php 排除掉(加在 .gitignore 裡面),因為 conn.php 裡面有我資料庫的位置、帳號密碼,不可以暴露在網路上
]]>會在 htdocs 資料夾裡面寫程式
檔案路徑都是從 htdocs 這個資料夾底下開始算
例如:
檔案路徑是 htdocs/saffran/index.php
網址就會是 http://localhost:8080/saffran/index.php
或是用 XAMPP 的 IP 位置也可以連到同一個位置去 http://192.168.64.2/saffran/index.php
詳細可參考 MacOSX启动XAMPP-VM报错:Error starting "XAMPP" stack: cannot calculate MAC address: signal: killed
情境如下:
我的 mac 升級版本之後,點擊 XAMPP 的 app icon 都毫無反應,因此需要重新安裝 XAMPP
~/.bitnami/stackman/helpers/hyperkit
用此種方式重新安裝 XAMPP,在 htdocs 裡面原有的檔案都會完整保留下來
步驟如下:
~/.bitnami/stackman/helpers/hyperkit
如果沒有先刪除「hyperkit」檔案,就重新安裝了 XAMPP,在 MacOS 啟動 XAMPP 時會報錯(無法啟動):
Error starting "XAMPP" stack: cannot calculate MAC address: signal: killed
;
$
$
來識別$
$a = 789;
echo $a;
// output: 789
.
來拼接字串注意!PHP 不是用 +
來拼接字串
$a = "sunday";
$b = "morning";
echo $a . $b;
// output: sundaymorning
範例一:
$score = 5;
if ($score >= 60) {
echo "pass!";
} else {
echo "fail";
}
// output: fail
範例二:
$score = 95;
if ($score >= 60 && $score < 80) {
echo "pass!";
} else if ($score >= 80) {
echo "great!";
} else {
echo "fail";
}
// output: great!
for ($i=1; $i<=10; $i++) {
echo $i;
}
// output: 12345678910
<br>
for ($i=1; $i<=10; $i++) {
echo $i . "<br>";
}
output:
1
2
3
4
5
6
7
8
9
10
檢視原始碼:
1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>9<br>10<br>
$arr = array('s', 'u', 'n', 'd', 'a', 'y');
echo "length:" . sizeof($arr) . "<br>"; // 印出陣列的長度
echo $arr[4]; // 印出 index 是 4 的元素
output:
length:6
a
例如我想要印出 $arr
這個陣列
寫成 echo $arr;
是沒有用的,會出現錯誤「Array to string conversion」
意思是:
要自己主動先把 array 轉成 string,才能用 echo
輸出
$arr = array('s', 'u', 'n', 'd', 'a', 'y');
echo $arr;
解決方式:
var_dump()
或是 print_r()
印出比較複雜的資料結構(不是單純的 number, string 這些)var_dump()
這個 function 來輸出陣列裡面每個元素的「type 和 value」$arr = array("sunday", 50, "dog", 33, 8);
var_dump($arr);
output:
array(5) { [0]=> string(6) "sunday" [1]=> int(50) [2]=> string(3) "dog" [3]=> int(33) [4]=> int(8) }
print_r()
這個 function 來輸出陣列裡面每個元素的「value」$arr = array("sunday", 50, "dog", 33, 8);
print_r($arr);
output:
Array ( [0] => sunday [1] => 50 [2] => dog [3] => 33 [4] => 8 )
function add($a, $b) {
return $a + $b;
}
echo add(5, 7);
// output: 12
[PHP教學] - 初學者最易混淆的include、include_once、require、require_once之比較
簡單談談PHP中的include、include_once、require以及require_once語句
深入理解require與require_once與include以及include_once的區別
include() 會將指定的檔案讀入,並執行檔案裡面的程式碼。
include_once() 和 include() 的作用幾乎相同,但是 include_once() 會先檢查要匯入的檔案是否已經被匯入過了,如果有的話就不會再重複匯入該檔案。
這個檢查有時候是很重要的,例如:在要匯入的檔案裡面有包含很多變數、自定義的函式,如果是用 include() 重複匯入該檔案,第二次匯入時就會發生錯誤,因為 PHP 不允許同名的變數、函式被重複宣告。
require() 會將目標檔案的內容讀入,並把自己本身替換成這些讀入的內容。
require(), require_once() 的差別也是在於: require_once() 會先檢查要匯入的檔案是否有被匯入過了,如果有的話就不會再重複匯入該檔案。
include(), include_once() 適合用來引入動態的程式碼。如果找不到要引入的檔案會出現錯誤訊息,但程式不會停止執行。
require(), require_once() 適合用來引入靜態的檔案內容。如果找不到要引入的檔案會出現錯誤訊息,且程式會停止執行。
執行 PHP 從頭到尾的流程如下:
request (test.php) => Apache (server) => PHP (執行 php test.php 這段指令) => output => Apache => response
我在 test.php 這個檔案發送 request,request 會先到 Apache 這個 server 去 => Apache 會幫我執行 php test.php
這段指令 => php test.php
這段指令會產生一個「output」 => 這個 output 會回到 Apache => 最後 Apache 再把 output 當作 response 傳回到 client 端
function run(request){
response = php(request) // 把 request 拿進來後送到 PHP
send response // 把拿回的 output 當作 response 傳回到 client 端
}
因此,在跑 Apache 這個 server 時,其實也可以跑其他的檔案,例如 index.html 或是 index.js 或是 all.css,Apache 都會幫我處理好
可以想成是:
server 會接收一個 request => 中間做一些處理後 => 再把 response 傳回來
function server(request){
...... // 中間做一些處理
return response;
}
test.php:
$num = 5;
echo "<br>" . $num . "<br>";
在 CLI 輸入 php test.php
就可以看到「被 PHP 轉化後的輸出」:
(因為執行環境不是在瀏覽器,所以 <br>
標籤不會被解析為「換行」,而是會用純文字顯示出來)
<br>5<br>
因此,整個流程就可以想成是:
Apache 收到 request 後會幫我執行 php test.php
這段指令 => 執行 php test.php
這段指令會輸出 <br>5<br>
=> Apache 再幫我把 <br>5<br>
當作 response 整包傳回來,傳回來到瀏覽器後,瀏覽器就會把 <br>5<br>
這些 HTML 標籤解析成我看到的畫面
因此
<br>5<br>
$num = 5;
echo "<br>" . $num . "<br>";
這是 Apache 預設的規則:
PHP 的網址會跟資料夾的結構相符,例如 http://localhost:8080/saffran/test.php
這個規則是可以去 server 更改的
因此,幾乎所有網站的網址後面都不會看到有什麼 facebook.com/user.php 之類的,而會是像 facebook.com/user/528183237 這種自訂的網址,就是因為有去更改 server 預設的設定
不需要把資料庫系統想的過於複雜
前面有講到,server 就只是一個程式(這個程式專門處理網路相關的 request 和 response)
「資料庫系統」也只是一個程式(這個程式專門處理資料)
在寫程式時,常常需要處理一些資料
假如沒有資料庫系統:
處理完的資料,可以存在變數、記憶體裡面,可是一旦我把程式關掉,這些資料就沒有了(資料無法被永久保存)
因此在早期,如果想要永久保存資料,就必須用「存檔」這種最陽春的方式:把資料存在檔案裡面,要用資料時就去讀取檔案。但存檔的缺點是「沒效率」:我必須自己手動去處理每一筆資料格式(例如:excel 的資料格式),會受到很多限制,很麻煩
資料庫系統,底層還是存在硬碟裡面,但我不用去在意它底層到底是怎麼存的
我只需要知道:資料庫系統提供了很友善的介面和指令,讓我可以用程式語言的語法很方便的去操作資料(把資料存進去、撈出來)
這就是資料庫系統的價值所在:讓「保存資料」變得更有效率、更好維護
存取的資料有一定的型態限制,例如:只能是 string, number
關聯式資料庫只是一個統稱,底下有許多真正實作出來的資料庫,其中,MySQL、Microsoft SQL Server 以及 PostgreSQL 就是最有名的幾個關聯式資料庫,這些資料庫在使用上其實都大同小異
存取的資料沒有型態限制,想要存成 array, object 這些都可以
比較有名的 NoSQL 有 MongoDB 這個資料庫
這兩種不同的資料庫系統,分別有不同的適用場合
在大部分情況下,關聯式資料庫是較常使用的
但當我想要存 log 時,就比較適合用 NoSQL,原因為 log 的一些特性:
{
studentId: 3,
score: [60, 85, 72],
student: {
name: peter,
phoneModel: 'y528'
}
}
但如果是在關聯式資料庫裡面,就一定得額外新增一個欄位(手機型號)才能存取新的資料,很麻煩
我可以用 SQL 這個程式語言,來操作關聯式資料庫裡面的資料
要連到資料庫有兩種方式:
phpMyAdmin 是一套「管理 MySQL 資料庫」的軟體
phpMyAdmin 其實就是一個 php 檔案,有人用 php 幫我寫好了這個網頁,讓我透過這個軟體介面更方便的去管理我的資料庫
運作機制就是:
例如我在 phpMyAdmin 新增一個表格,phpMyAdmin 就會在底層幫我下一些 SQL 的指令給資料庫,就可以在資料庫裡面建立一個表格
除了 phpMyAdmin 之外,也可以使用另一套軟體 Sequel Pro 來管理資料庫
Sequel Pro 不是用 php 寫的,而是一個直接跑在電腦上的程式
schema 就是「結構」的意思
每一個 table 都有一個結構
對於比較短的字串,例如 username,type 會存成 VARCHAR
主鍵,英文是 Primary key,簡稱為 PK
設定成主鍵的欄位,就是在整個 table 裡面最重要的欄位,必須滿足幾個條件:
假設我要設計一個員工的系統,就會把「員工編號」設為 PK,因為員工編號就代表每個員工,不會重複且是最重要的一個欄位
如果把此欄位設為 Unique,就不能再新增跟這個欄位同樣的值
使用時機:
例如我不希望不同的 user 使用同樣的 email
如果把此欄位設為 Index,在搜尋此欄位的資料時就可以比較快
也可以把兩個欄位設為一個 Index,例如 username 和 password,這樣我就可以同時查詢 username 和 password 的組合
資料來源 1 CHAR Data Type
資料來源 2 VARCHAR Data Type
資料來源 3 TEXT, TINYTEXT, MEDIUMTEXT, LONGTEXT
編碼會使用 UTF8 是因為:可以支援多種語言
CHAR、VARCHAR、TEXT 都是「string」的資料型別
以下是個別的介紹與比較:
CHAR(30) 代表:長度是 30 個 characters 的字串
如果我的字串是「hello」,只有用到 5 個 characters,MySQL 會在後面自動加上 25 個空白來補滿 30 個 characters,所以在欄位裡的資料就會是:
'hello '
但是,當我使用 SELECT
語法把資料撈出來時,MySQL 會自動把空白去掉,所以我拿到的資料會是:
'hello'
MySQL mode 有很多種模式,如果我去把其中一個模式叫做 PAD_CHAR_TO_FULL_LENGTH 給打開,這樣當我使用 SELECT
語法把資料撈出來時,MySQL 就不會把空白去掉了,所以我拿到的資料就會是「有 25 個空白的字串」:
'hello '
如果是使用 VARCHAR,MySQL 就要特別去記錄每筆資料的字串長度(因此每個字串都會佔用較多空間)
0 個字元就是:8 bit 能放入的最少字元 (可以是 null
、空字串 ''
)
255 個字元就是:8 bit 能放入的最多字元
十進位的 0 就是:二進位的 00000000
十進位的 255 就是:二進位的 11111111
VARCHAR 跟 CHAR 的差別是:
VARCHAR 不會用空白去補字串長度
在 UTF8 的編碼中,一個英文字母所佔的空間是 1 byte
其他語言(例如中文字),因為筆畫較多,一個字元在 MySQL 最多會佔 3 bytes 的空間
如果我的資料型別使用的是 VARCHAR(50)
當有一個字串 'hello'
,所佔的空間就是 5 bytes + 1 byte
VARCHAR 可以放入字串的最大空間是:65535 bytes
而 MySQL 的 row limit 也剛好是 65535 bytes
(row limit 就是:每個 row 可以存東西的最大空間)
65535 bytes 換算成字串長度就是 21,843 個字元:
65535 bytes / 3 - 2 bytes = 21843
因此,保險起見,VARCHAR 的字串長度不要超過 20,000 個字元
如果是用 VARCHAR,只要是 65535 bytes 以內的字串,我都可以存
但如果是用 TEXT,會根據不同的 size 又區分為四種型別:
TINYTEXT: 最多可以存 255 characters = 255 Bytes
TEXT: 最多可以存 65,535 characters = 64 KB
MEDIUMTEXT: 最多可以存 16,777,215 characters = 16 MB
LONGTEXT: 最多可以存 4,294,967,295 characters = 4 GB
原因為:
VARCHAR 是直接存在 table 裡面,所以會受到 row limit 的限制
但是 MySQL 會把 TEXT 存放在別的地方(不是直接存在 table 裡面),再用「reference」的方式去引入這個 TEXT
(無論存了多大的 TEXT,在 table 本身最多只會用到 12 bytes 而已)
因此,TEXT 可以比 VARCHAR 存更多的字元在欄位中
TEXT 這種資料型別也被稱為 CLOB(Character Large Object)
null
或是其他任何的值)null
*
是「欄位名稱」 (*
代表:所有欄位)blog posts
是「table 名稱」SELECT * FROM `blog posts`
只把 username, content 這兩個欄位的內容撈出來
SELECT username, content FROM `blog posts`
原本的欄位名稱是 username
,但我想要欄位名稱改為用 studentName
來顯示
SELECT username as studentName FROM `blog posts`
用 WHERE
來設定我要查詢的資料條件是 username = 'Harry'
'Harry'
要用單引號或雙引號包起來SELECT username FROM `blog posts` WHERE username = 'Harry'
and
設定多個條件多個條件之間用 and
來連接(需同時符合這幾個條件)
SELECT username FROM `blog posts` WHERE username = 'Harry' and id = 1
or
設定多個條件多個條件之間用 or
來連接(只需符合其中一個條件即可)
SELECT username FROM `blog posts` WHERE username = 'Harry' or id = 2
blog posts
是 table 名稱username
欄位新增一筆資料 Micky
content
欄位新增一筆資料 Hi, I'm a mouse.
INSERT INTO `blog posts`(`username`, `content`) VALUES ('Micky', "Hi, I'm a mouse.")
blog posts
是 table 名稱SET
後面接一系列我想要修改的欄位 + 資料,不同欄位之間用 ,
分開WHERE
後面接「我要限定的條件」,例如:我只要修改 id 是 2 的欄位注意!如果沒有加上 WHERE id = 2
,就會把整個 table 裡面每個 username 欄位的資料都改成 Katy,也會把整個 table 裡面每個 content 欄位的資料都改成 Check it out!
UPDATE `blog posts` SET username = 'Katy', content = 'Check it out!' WHERE id = 2
blog posts
是 table 名稱WHERE id = 2
代表:我想要刪除的是 id = 2 的這筆資料DELETE FROM `blog posts` WHERE id = 2
is_deleted
來決定此資料是否要呈現在 user 面前在很多情況下,系統並不會真的把資料刪掉
例如:
當 user 在前台選擇「刪除會員帳號」時,系統並不會真的把資料刪掉(因為 user 有可能是誤刪資料,如果真的把資料刪掉就無法救回來了)
系統的做法會是:
會在 table 最後新增一個欄位叫做 is_deleted
,它的 type 會是 Boolean
is_deleted
的值會是 0 (因為 0 代表 false)is_deleted
的值改為 1 (因為 1 代表 true)來當作「資料已經被刪掉了」的意思然後,user 在前台只會看到那些 is_deleted = 0
的資料而已,所以 user 就會認為那些 is_deleted = 1
的資料都是已經被刪掉的(但對資料庫來說,每一筆資料都還在)
SELECT * FROM `blog posts` WHERE is_deleted = 0
]]>會在 htdocs 資料夾裡面寫程式
檔案路徑都是從 htdocs 這個資料夾底下開始算
例如:
檔案路徑是 htdocs/saffran/index.php
網址就會是 http://localhost:8080/saffran/index.php
或是用 XAMPP 的 IP 位置也可以連到同一個位置去 http://192.168.64.2/saffran/index.php
詳細可參考 MacOSX启动XAMPP-VM报错:Error starting "XAMPP" stack: cannot calculate MAC address: signal: killed
情境如下:
我的 mac 升級版本之後,點擊 XAMPP 的 app icon 都毫無反應,因此需要重新安裝 XAMPP
~/.bitnami/stackman/helpers/hyperkit
用此種方式重新安裝 XAMPP,在 htdocs 裡面原有的檔案都會完整保留下來
步驟如下:
~/.bitnami/stackman/helpers/hyperkit
如果沒有先刪除「hyperkit」檔案,就重新安裝了 XAMPP,在 MacOS 啟動 XAMPP 時會報錯(無法啟動):
Error starting "XAMPP" stack: cannot calculate MAC address: signal: killed
;
$
$
來識別$
$a = 789;
echo $a;
// output: 789
.
來拼接字串注意!PHP 不是用 +
來拼接字串
$a = "sunday";
$b = "morning";
echo $a . $b;
// output: sundaymorning
範例一:
$score = 5;
if ($score >= 60) {
echo "pass!";
} else {
echo "fail";
}
// output: fail
範例二:
$score = 95;
if ($score >= 60 && $score < 80) {
echo "pass!";
} else if ($score >= 80) {
echo "great!";
} else {
echo "fail";
}
// output: great!
for ($i=1; $i<=10; $i++) {
echo $i;
}
// output: 12345678910
<br>
for ($i=1; $i<=10; $i++) {
echo $i . "<br>";
}
output:
1
2
3
4
5
6
7
8
9
10
檢視原始碼:
1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>9<br>10<br>
$arr = array('s', 'u', 'n', 'd', 'a', 'y');
echo "length:" . sizeof($arr) . "<br>"; // 印出陣列的長度
echo $arr[4]; // 印出 index 是 4 的元素
output:
length:6
a
例如我想要印出 $arr
這個陣列
寫成 echo $arr;
是沒有用的,會出現錯誤「Array to string conversion」
意思是:
要自己主動先把 array 轉成 string,才能用 echo
輸出
$arr = array('s', 'u', 'n', 'd', 'a', 'y');
echo $arr;
解決方式:
var_dump()
或是 print_r()
印出比較複雜的資料結構(不是單純的 number, string 這些)var_dump()
這個 function 來輸出陣列裡面每個元素的「type 和 value」$arr = array("sunday", 50, "dog", 33, 8);
var_dump($arr);
output:
array(5) { [0]=> string(6) "sunday" [1]=> int(50) [2]=> string(3) "dog" [3]=> int(33) [4]=> int(8) }
print_r()
這個 function 來輸出陣列裡面每個元素的「value」$arr = array("sunday", 50, "dog", 33, 8);
print_r($arr);
output:
Array ( [0] => sunday [1] => 50 [2] => dog [3] => 33 [4] => 8 )
function add($a, $b) {
return $a + $b;
}
echo add(5, 7);
// output: 12
[PHP教學] - 初學者最易混淆的include、include_once、require、require_once之比較
簡單談談PHP中的include、include_once、require以及require_once語句
深入理解require與require_once與include以及include_once的區別
include() 會將指定的檔案讀入,並執行檔案裡面的程式碼。
include_once() 和 include() 的作用幾乎相同,但是 include_once() 會先檢查要匯入的檔案是否已經被匯入過了,如果有的話就不會再重複匯入該檔案。
這個檢查有時候是很重要的,例如:在要匯入的檔案裡面有包含很多變數、自定義的函式,如果是用 include() 重複匯入該檔案,第二次匯入時就會發生錯誤,因為 PHP 不允許同名的變數、函式被重複宣告。
require() 會將目標檔案的內容讀入,並把自己本身替換成這些讀入的內容。
require(), require_once() 的差別也是在於: require_once() 會先檢查要匯入的檔案是否有被匯入過了,如果有的話就不會再重複匯入該檔案。
include(), include_once() 適合用來引入動態的程式碼。如果找不到要引入的檔案會出現錯誤訊息,但程式不會停止執行。
require(), require_once() 適合用來引入靜態的檔案內容。如果找不到要引入的檔案會出現錯誤訊息,且程式會停止執行。
執行 PHP 從頭到尾的流程如下:
request (test.php) => Apache (server) => PHP (執行 php test.php 這段指令) => output => Apache => response
我在 test.php 這個檔案發送 request,request 會先到 Apache 這個 server 去 => Apache 會幫我執行 php test.php
這段指令 => php test.php
這段指令會產生一個「output」 => 這個 output 會回到 Apache => 最後 Apache 再把 output 當作 response 傳回到 client 端
function run(request){
response = php(request) // 把 request 拿進來後送到 PHP
send response // 把拿回的 output 當作 response 傳回到 client 端
}
因此,在跑 Apache 這個 server 時,其實也可以跑其他的檔案,例如 index.html 或是 index.js 或是 all.css,Apache 都會幫我處理好
可以想成是:
server 會接收一個 request => 中間做一些處理後 => 再把 response 傳回來
function server(request){
...... // 中間做一些處理
return response;
}
test.php:
$num = 5;
echo "<br>" . $num . "<br>";
在 CLI 輸入 php test.php
就可以看到「被 PHP 轉化後的輸出」:
(因為執行環境不是在瀏覽器,所以 <br>
標籤不會被解析為「換行」,而是會用純文字顯示出來)
<br>5<br>
因此,整個流程就可以想成是:
Apache 收到 request 後會幫我執行 php test.php
這段指令 => 執行 php test.php
這段指令會輸出 <br>5<br>
=> Apache 再幫我把 <br>5<br>
當作 response 整包傳回來,傳回來到瀏覽器後,瀏覽器就會把 <br>5<br>
這些 HTML 標籤解析成我看到的畫面
因此
<br>5<br>
$num = 5;
echo "<br>" . $num . "<br>";
這是 Apache 預設的規則:
PHP 的網址會跟資料夾的結構相符,例如 http://localhost:8080/saffran/test.php
這個規則是可以去 server 更改的
因此,幾乎所有網站的網址後面都不會看到有什麼 facebook.com/user.php 之類的,而會是像 facebook.com/user/528183237 這種自訂的網址,就是因為有去更改 server 預設的設定
不需要把資料庫系統想的過於複雜
前面有講到,server 就只是一個程式(這個程式專門處理網路相關的 request 和 response)
「資料庫系統」也只是一個程式(這個程式專門處理資料)
在寫程式時,常常需要處理一些資料
假如沒有資料庫系統:
處理完的資料,可以存在變數、記憶體裡面,可是一旦我把程式關掉,這些資料就沒有了(資料無法被永久保存)
因此在早期,如果想要永久保存資料,就必須用「存檔」這種最陽春的方式:把資料存在檔案裡面,要用資料時就去讀取檔案。但存檔的缺點是「沒效率」:我必須自己手動去處理每一筆資料格式(例如:excel 的資料格式),會受到很多限制,很麻煩
資料庫系統,底層還是存在硬碟裡面,但我不用去在意它底層到底是怎麼存的
我只需要知道:資料庫系統提供了很友善的介面和指令,讓我可以用程式語言的語法很方便的去操作資料(把資料存進去、撈出來)
這就是資料庫系統的價值所在:讓「保存資料」變得更有效率、更好維護
存取的資料有一定的型態限制,例如:只能是 string, number
關聯式資料庫只是一個統稱,底下有許多真正實作出來的資料庫,其中,MySQL、Microsoft SQL Server 以及 PostgreSQL 就是最有名的幾個關聯式資料庫,這些資料庫在使用上其實都大同小異
存取的資料沒有型態限制,想要存成 array, object 這些都可以
比較有名的 NoSQL 有 MongoDB 這個資料庫
這兩種不同的資料庫系統,分別有不同的適用場合
在大部分情況下,關聯式資料庫是較常使用的
但當我想要存 log 時,就比較適合用 NoSQL,原因為 log 的一些特性:
{
studentId: 3,
score: [60, 85, 72],
student: {
name: peter,
phoneModel: 'y528'
}
}
但如果是在關聯式資料庫裡面,就一定得額外新增一個欄位(手機型號)才能存取新的資料,很麻煩
我可以用 SQL 這個程式語言,來操作關聯式資料庫裡面的資料
要連到資料庫有兩種方式:
phpMyAdmin 是一套「管理 MySQL 資料庫」的軟體
phpMyAdmin 其實就是一個 php 檔案,有人用 php 幫我寫好了這個網頁,讓我透過這個軟體介面更方便的去管理我的資料庫
運作機制就是:
例如我在 phpMyAdmin 新增一個表格,phpMyAdmin 就會在底層幫我下一些 SQL 的指令給資料庫,就可以在資料庫裡面建立一個表格
除了 phpMyAdmin 之外,也可以使用另一套軟體 Sequel Pro 來管理資料庫
Sequel Pro 不是用 php 寫的,而是一個直接跑在電腦上的程式
schema 就是「結構」的意思
每一個 table 都有一個結構
對於比較短的字串,例如 username,type 會存成 VARCHAR
主鍵,英文是 Primary key,簡稱為 PK
設定成主鍵的欄位,就是在整個 table 裡面最重要的欄位,必須滿足幾個條件:
假設我要設計一個員工的系統,就會把「員工編號」設為 PK,因為員工編號就代表每個員工,不會重複且是最重要的一個欄位
如果把此欄位設為 Unique,就不能再新增跟這個欄位同樣的值
使用時機:
例如我不希望不同的 user 使用同樣的 email
如果把此欄位設為 Index,在搜尋此欄位的資料時就可以比較快
也可以把兩個欄位設為一個 Index,例如 username 和 password,這樣我就可以同時查詢 username 和 password 的組合
資料來源 1 CHAR Data Type
資料來源 2 VARCHAR Data Type
資料來源 3 TEXT, TINYTEXT, MEDIUMTEXT, LONGTEXT
編碼會使用 UTF8 是因為:可以支援多種語言
CHAR、VARCHAR、TEXT 都是「string」的資料型別
以下是個別的介紹與比較:
CHAR(30) 代表:長度是 30 個 characters 的字串
如果我的字串是「hello」,只有用到 5 個 characters,MySQL 會在後面自動加上 25 個空白來補滿 30 個 characters,所以在欄位裡的資料就會是:
'hello '
但是,當我使用 SELECT
語法把資料撈出來時,MySQL 會自動把空白去掉,所以我拿到的資料會是:
'hello'
MySQL mode 有很多種模式,如果我去把其中一個模式叫做 PAD_CHAR_TO_FULL_LENGTH 給打開,這樣當我使用 SELECT
語法把資料撈出來時,MySQL 就不會把空白去掉了,所以我拿到的資料就會是「有 25 個空白的字串」:
'hello '
如果是使用 VARCHAR,MySQL 就要特別去記錄每筆資料的字串長度(因此每個字串都會佔用較多空間)
0 個字元就是:8 bit 能放入的最少字元 (可以是 null
、空字串 ''
)
255 個字元就是:8 bit 能放入的最多字元
十進位的 0 就是:二進位的 00000000
十進位的 255 就是:二進位的 11111111
VARCHAR 跟 CHAR 的差別是:
VARCHAR 不會用空白去補字串長度
在 UTF8 的編碼中,一個英文字母所佔的空間是 1 byte
其他語言(例如中文字),因為筆畫較多,一個字元在 MySQL 最多會佔 3 bytes 的空間
如果我的資料型別使用的是 VARCHAR(50)
當有一個字串 'hello'
,所佔的空間就是 5 bytes + 1 byte
VARCHAR 可以放入字串的最大空間是:65535 bytes
而 MySQL 的 row limit 也剛好是 65535 bytes
(row limit 就是:每個 row 可以存東西的最大空間)
65535 bytes 換算成字串長度就是 21,843 個字元:
65535 bytes / 3 - 2 bytes = 21843
因此,保險起見,VARCHAR 的字串長度不要超過 20,000 個字元
如果是用 VARCHAR,只要是 65535 bytes 以內的字串,我都可以存
但如果是用 TEXT,會根據不同的 size 又區分為四種型別:
TINYTEXT: 最多可以存 255 characters = 255 Bytes
TEXT: 最多可以存 65,535 characters = 64 KB
MEDIUMTEXT: 最多可以存 16,777,215 characters = 16 MB
LONGTEXT: 最多可以存 4,294,967,295 characters = 4 GB
原因為:
VARCHAR 是直接存在 table 裡面,所以會受到 row limit 的限制
但是 MySQL 會把 TEXT 存放在別的地方(不是直接存在 table 裡面),再用「reference」的方式去引入這個 TEXT
(無論存了多大的 TEXT,在 table 本身最多只會用到 12 bytes 而已)
因此,TEXT 可以比 VARCHAR 存更多的字元在欄位中
TEXT 這種資料型別也被稱為 CLOB(Character Large Object)
null
或是其他任何的值)null
*
是「欄位名稱」 (*
代表:所有欄位)blog posts
是「table 名稱」SELECT * FROM `blog posts`
只把 username, content 這兩個欄位的內容撈出來
SELECT username, content FROM `blog posts`
原本的欄位名稱是 username
,但我想要欄位名稱改為用 studentName
來顯示
SELECT username as studentName FROM `blog posts`
用 WHERE
來設定我要查詢的資料條件是 username = 'Harry'
'Harry'
要用單引號或雙引號包起來SELECT username FROM `blog posts` WHERE username = 'Harry'
and
設定多個條件多個條件之間用 and
來連接(需同時符合這幾個條件)
SELECT username FROM `blog posts` WHERE username = 'Harry' and id = 1
or
設定多個條件多個條件之間用 or
來連接(只需符合其中一個條件即可)
SELECT username FROM `blog posts` WHERE username = 'Harry' or id = 2
blog posts
是 table 名稱username
欄位新增一筆資料 Micky
content
欄位新增一筆資料 Hi, I'm a mouse.
INSERT INTO `blog posts`(`username`, `content`) VALUES ('Micky', "Hi, I'm a mouse.")
blog posts
是 table 名稱SET
後面接一系列我想要修改的欄位 + 資料,不同欄位之間用 ,
分開WHERE
後面接「我要限定的條件」,例如:我只要修改 id 是 2 的欄位注意!如果沒有加上 WHERE id = 2
,就會把整個 table 裡面每個 username 欄位的資料都改成 Katy,也會把整個 table 裡面每個 content 欄位的資料都改成 Check it out!
UPDATE `blog posts` SET username = 'Katy', content = 'Check it out!' WHERE id = 2
blog posts
是 table 名稱WHERE id = 2
代表:我想要刪除的是 id = 2 的這筆資料DELETE FROM `blog posts` WHERE id = 2
is_deleted
來決定此資料是否要呈現在 user 面前在很多情況下,系統並不會真的把資料刪掉
例如:
當 user 在前台選擇「刪除會員帳號」時,系統並不會真的把資料刪掉(因為 user 有可能是誤刪資料,如果真的把資料刪掉就無法救回來了)
系統的做法會是:
會在 table 最後新增一個欄位叫做 is_deleted
,它的 type 會是 Boolean
is_deleted
的值會是 0 (因為 0 代表 false)is_deleted
的值改為 1 (因為 1 代表 true)來當作「資料已經被刪掉了」的意思然後,user 在前台只會看到那些 is_deleted = 0
的資料而已,所以 user 就會認為那些 is_deleted = 1
的資料都是已經被刪掉的(但對資料庫來說,每一筆資料都還在)
SELECT * FROM `blog posts` WHERE is_deleted = 0
]]>
每一個 HTML 元素都可以看成是一個盒子,這個盒子會由四個部分所組成,從內到外分別是 content, padding, border, margin。打開開發者工具,就可以看到這個 box model(如下圖所示)
關於 box model,要注意的是 box-sizing 這個屬性
box-sizing 這個屬性可以決定「要用什麼樣的模式來顯示 box model」
box-sizing
的預設值是 content-box
,意思就是:假設我設定了一個 width, height 各是 100px 的元素,這 100px 只會是「content」的寬高而已,因此如果我想要計算整個元素的寬高,就還要再加上 border 和 padding 的部分。例如我加上 20px 的 padding,那麼元素的寬就會是 100px + 20px(left padding) + 20px(right padding) = 140px因為 content-box
對開發者來說,計算寬高還要加加減減很麻煩,因此通常都會把 box-sizing
設定為 border-box
box-sizing
設定為 border-box
,意思就是:假設我設定了一個 width, height 各是 100px 的元素,這 100px 就已經是包含 content + border + padding 的部分了。例如我加上 5px 的 border 後,content 就會往內縮,元素的寬高還是一樣會是 100pxborder-box
對開發者來說很方便,因為不需要再去加加減減的計算寬高,因此通常都會設定成 box-sizing: border-box
參考資料 話說 Box model 是什麼呢?
display: inline
display: block
inline-block 集合了 inline 和 block 的優點
display: inline-block
position: static
position: relative
就是「相對定位」position: static
時」的位置position: relative
,這樣就可以做為叉叉按鈕的參考點position: fixed
會相對於 viewport (瀏覽器窗口)做定位position: fixed
來固定在右下角的位置position: absolute
會相對於參考點去做定位,也就是「絕對定位」position: relative
,叉叉按鈕設定為 position: absolute
,叉叉就可以相對於彈跳視窗去做定位position: absolute
要怎麼找到參考點?往上找,找到的第一個「position 不是 static 的元素」就是參考點
.box
還沒到達 top: 30px 這個位置之前,.box
都會按照正常的排版流(static)
一旦到達了 top: 30px 這個位置,就會被黏住(變成 fixed)
.box:nth-child(2) {
background-color: pink;
position: sticky;
top: 30px;
z-index: 2;
}
]]>每一個 HTML 元素都可以看成是一個盒子,這個盒子會由四個部分所組成,從內到外分別是 content, padding, border, margin。打開開發者工具,就可以看到這個 box model(如下圖所示)
關於 box model,要注意的是 box-sizing 這個屬性
box-sizing 這個屬性可以決定「要用什麼樣的模式來顯示 box model」
box-sizing
的預設值是 content-box
,意思就是:假設我設定了一個 width, height 各是 100px 的元素,這 100px 只會是「content」的寬高而已,因此如果我想要計算整個元素的寬高,就還要再加上 border 和 padding 的部分。例如我加上 20px 的 padding,那麼元素的寬就會是 100px + 20px(left padding) + 20px(right padding) = 140px因為 content-box
對開發者來說,計算寬高還要加加減減很麻煩,因此通常都會把 box-sizing
設定為 border-box
box-sizing
設定為 border-box
,意思就是:假設我設定了一個 width, height 各是 100px 的元素,這 100px 就已經是包含 content + border + padding 的部分了。例如我加上 5px 的 border 後,content 就會往內縮,元素的寬高還是一樣會是 100pxborder-box
對開發者來說很方便,因為不需要再去加加減減的計算寬高,因此通常都會設定成 box-sizing: border-box
參考資料 話說 Box model 是什麼呢?
display: inline
display: block
inline-block 集合了 inline 和 block 的優點
display: inline-block
position: static
position: relative
就是「相對定位」position: static
時」的位置position: relative
,這樣就可以做為叉叉按鈕的參考點position: fixed
會相對於 viewport (瀏覽器窗口)做定位position: fixed
來固定在右下角的位置position: absolute
會相對於參考點去做定位,也就是「絕對定位」position: relative
,叉叉按鈕設定為 position: absolute
,叉叉就可以相對於彈跳視窗去做定位position: absolute
要怎麼找到參考點?往上找,找到的第一個「position 不是 static 的元素」就是參考點
.box
還沒到達 top: 30px 這個位置之前,.box
都會按照正常的排版流(static)
一旦到達了 top: 30px 這個位置,就會被黏住(變成 fixed)
.box:nth-child(2) {
background-color: pink;
position: sticky;
top: 30px;
z-index: 2;
}
]]>
RGBA stands for red green blue alpha.
outline 不佔有空間,不會影響到元素的寬高
就算沒有加上 border,也可以設定 border-radius
html:
<div class="boxOne">box1</div>
css:
.boxOne {
background-color: salmon;
width: 100px;
height: 100px;
border-radius: 10px;
}
html:
<div class="boxOne">box1</div>
css:
.boxOne {
background-color: lightgreen;
width: 30px;
height: 30px;
border-top: 100px solid slateblue;
border-right: 100px solid brown;
border-bottom: 100px solid orange;
border-left: 100px solid pink;
}
可以看到,上下左右的 border 其實各自都是一個梯形
.boxOne
的寬度、高度都設為 0 即可再看是要留哪一個三角形,就把其他三個 border 的顏色設為 transparent
css:
.boxOne {
background-color: transparent;
width: 0px;
height: 0px;
border-top: 100px solid transparent;
border-right: 100px solid brown;
border-bottom: 100px solid transparent;
border-left: 100px solid transparent;
}
想要調整三角形的寬高,就用其他三個 border 的寬度來調整,例如以下
css:
.boxOne {
background-color: transparent;
width: 0px;
height: 0px;
border-top: 50px solid transparent;
border-right: 100px solid brown;
border-bottom: 50px solid transparent;
border-left: 100px solid transparent;
}
html:
<div class="box">
hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello
</div>
css:
.box{
background-color: orange;
width: 200px;
height: 100px;
}
文字如果沒有空格的話,字就會超出寬度
word-break
這個屬性來決定要如何去把文字換行.box{
background-color: orange;
width: 200px;
height: 100px;
word-break: break-all;
}
break-all
跟 break-word
有什麼差別呢?html:
<div class="box">
The giraffe's chief distinguishing characteristics are its extremely long neck and legs, its horn-like ossicones, and
its distinctive coat patterns. It is classified under the family Giraffidae, along with its closest extant relative, the
okapi.
</div>
css:
.box{
background-color: orange;
width: 200px;
height: 100px;
}
如果是一段有空格的文字,預設就會是 word-break: break-word
break-word
會以完整的單字去換行css:
.box{
background-color: orange;
width: 200px;
height: 100px;
word-break: break-word;
}
break-all
不會去管是否有把字切斷,就直接換行css:
可以看到 distinguishing 這個字就因為換行而直接被切斷了
.box{
background-color: orange;
width: 200px;
height: 100px;
word-break: break-all;
}
css:
nowrap
代表「不用包起來,字可以超出寬度沒關係」,因此,字就會都在同一行
.box{
background-color: orange;
width: 200px;
height: 100px;
white-space: nowrap;
}
overflow 針對任何東西(圖片、文字)都可以使用
overflow: hidden
把超出的部分隱藏overflow: scroll
如果有超出的部分,就會有捲軸text-overflow 是只針對文字用的
ellipsis 是「省略號」的意思
text-overflow: ellipsis
有效果,必須滿足幾個條件:white-space: nowrap
,讓文字先變成同一行overflow: hidden
css:
用 text-overflow: ellipsis
把超出的文字加上省略號
.box{
background-color: orange;
width: 200px;
height: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
範例程式碼如下
html:
<div class="box">box1</div>
css:
.box {
background-color: orange;
color: white;
text-align: center;
width: 200px;
height: 100px;
line-height: 100px;
margin: 10px;
border-radius: 30px;
}
css:
加上 transform: scale(2)
就會以「.box
的中心點為基準」把 .box
變大兩倍
搭配 transition
來使用,就會有很酷的動畫效果
.box {
background-color: orange;
color: white;
text-align: center;
width: 200px;
height: 10px;
line-height: 100px;
margin: 100px;
border-radius: 30px;
transition: all 1s;
}
.box:hover {
transform: scale(2);
}
css:
.box {
background-color: orange;
color: white;
text-align: center;
width: 200px;
height: 100px;
line-height: 100px;
margin: 10px;
border-radius: 30px;
transition: all 1s;
}
.box:hover {
transform: rotate(180deg);
}
會以「元素原本的位置」為基準點去做偏移
css:
.box {
background-color: orange;
color: white;
text-align: center;
width: 200px;
height: 100px;
line-height: 100px;
margin: 10px;
border-radius: 30px;
transition: all 1s;
}
.box:hover {
transform: translate(50px, -30px);
}
transform
來做偏移,跟「用 top, right ,bottom, left 來做偏移」有什麼差別呢?差別在於:
用 transform
來做偏移,不會影響到其他元素的位置
因此,在動畫中要做偏移的話,通常都是使用 transform
.box
會水平又垂直置中於畫面
css:
transform: translate(-50%, -50%)
是移動「元素寬度的 -50%」和「元素高度的 -50%」
.box {
background-color: orange;
color: white;
text-align: center;
width: 200px;
height: 100px;
line-height: 100px;
margin: 10px;
border-radius: 30px;
transition: all 1s;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
]]>RGBA stands for red green blue alpha.
outline 不佔有空間,不會影響到元素的寬高
就算沒有加上 border,也可以設定 border-radius
html:
<div class="boxOne">box1</div>
css:
.boxOne {
background-color: salmon;
width: 100px;
height: 100px;
border-radius: 10px;
}
html:
<div class="boxOne">box1</div>
css:
.boxOne {
background-color: lightgreen;
width: 30px;
height: 30px;
border-top: 100px solid slateblue;
border-right: 100px solid brown;
border-bottom: 100px solid orange;
border-left: 100px solid pink;
}
可以看到,上下左右的 border 其實各自都是一個梯形
.boxOne
的寬度、高度都設為 0 即可再看是要留哪一個三角形,就把其他三個 border 的顏色設為 transparent
css:
.boxOne {
background-color: transparent;
width: 0px;
height: 0px;
border-top: 100px solid transparent;
border-right: 100px solid brown;
border-bottom: 100px solid transparent;
border-left: 100px solid transparent;
}
想要調整三角形的寬高,就用其他三個 border 的寬度來調整,例如以下
css:
.boxOne {
background-color: transparent;
width: 0px;
height: 0px;
border-top: 50px solid transparent;
border-right: 100px solid brown;
border-bottom: 50px solid transparent;
border-left: 100px solid transparent;
}
html:
<div class="box">
hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello
</div>
css:
.box{
background-color: orange;
width: 200px;
height: 100px;
}
文字如果沒有空格的話,字就會超出寬度
word-break
這個屬性來決定要如何去把文字換行.box{
background-color: orange;
width: 200px;
height: 100px;
word-break: break-all;
}
break-all
跟 break-word
有什麼差別呢?html:
<div class="box">
The giraffe's chief distinguishing characteristics are its extremely long neck and legs, its horn-like ossicones, and
its distinctive coat patterns. It is classified under the family Giraffidae, along with its closest extant relative, the
okapi.
</div>
css:
.box{
background-color: orange;
width: 200px;
height: 100px;
}
如果是一段有空格的文字,預設就會是 word-break: break-word
break-word
會以完整的單字去換行css:
.box{
background-color: orange;
width: 200px;
height: 100px;
word-break: break-word;
}
break-all
不會去管是否有把字切斷,就直接換行css:
可以看到 distinguishing 這個字就因為換行而直接被切斷了
.box{
background-color: orange;
width: 200px;
height: 100px;
word-break: break-all;
}
css:
nowrap
代表「不用包起來,字可以超出寬度沒關係」,因此,字就會都在同一行
.box{
background-color: orange;
width: 200px;
height: 100px;
white-space: nowrap;
}
overflow 針對任何東西(圖片、文字)都可以使用
overflow: hidden
把超出的部分隱藏overflow: scroll
如果有超出的部分,就會有捲軸text-overflow 是只針對文字用的
ellipsis 是「省略號」的意思
text-overflow: ellipsis
有效果,必須滿足幾個條件:white-space: nowrap
,讓文字先變成同一行overflow: hidden
css:
用 text-overflow: ellipsis
把超出的文字加上省略號
.box{
background-color: orange;
width: 200px;
height: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
範例程式碼如下
html:
<div class="box">box1</div>
css:
.box {
background-color: orange;
color: white;
text-align: center;
width: 200px;
height: 100px;
line-height: 100px;
margin: 10px;
border-radius: 30px;
}
css:
加上 transform: scale(2)
就會以「.box
的中心點為基準」把 .box
變大兩倍
搭配 transition
來使用,就會有很酷的動畫效果
.box {
background-color: orange;
color: white;
text-align: center;
width: 200px;
height: 10px;
line-height: 100px;
margin: 100px;
border-radius: 30px;
transition: all 1s;
}
.box:hover {
transform: scale(2);
}
css:
.box {
background-color: orange;
color: white;
text-align: center;
width: 200px;
height: 100px;
line-height: 100px;
margin: 10px;
border-radius: 30px;
transition: all 1s;
}
.box:hover {
transform: rotate(180deg);
}
會以「元素原本的位置」為基準點去做偏移
css:
.box {
background-color: orange;
color: white;
text-align: center;
width: 200px;
height: 100px;
line-height: 100px;
margin: 10px;
border-radius: 30px;
transition: all 1s;
}
.box:hover {
transform: translate(50px, -30px);
}
transform
來做偏移,跟「用 top, right ,bottom, left 來做偏移」有什麼差別呢?差別在於:
用 transform
來做偏移,不會影響到其他元素的位置
因此,在動畫中要做偏移的話,通常都是使用 transform
.box
會水平又垂直置中於畫面
css:
transform: translate(-50%, -50%)
是移動「元素寬度的 -50%」和「元素高度的 -50%」
.box {
background-color: orange;
color: white;
text-align: center;
width: 200px;
height: 100px;
line-height: 100px;
margin: 10px;
border-radius: 30px;
transition: all 1s;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
]]>
html:
現在,第一個 div 和 span 都有 class="bg-green"
但是,我只想選到「同時符合又是 div 又是 class="bg-green"
」的元素,該怎麼寫 css 的 selector 呢?
<div class="bg-green">
hello1
</div>
<div>
hello2
</div>
<span class="bg-green">
hello3
</span>
css:
選到「同時符合又是 div 又是 class="bg-green"
」的元素
div.bg-green{
background-color: green;
}
html:
現在,我只想選到「同時具有 bg-green
和 bg-real-green
這兩個 class」的元素,該怎麼寫 css 的 selector 呢?
<div class="bg-green bg-real-green">
hello1
</div>
<div>
hello2
</div>
<div class="bg-green">
hello3
</div>
css:
選到「同時具有 bg-green
和 bg-real-green
這兩個 class」的元素
.bg-green.bg-real-green{
background-color: green;
}
html:
<div class="levelOne">
lv1
<div>
lv2
<div>lv3</div>
<div>hello</div>
</div>
</div>
>
只會選到「.levelOne
的下一層」css:
就會選到 lv2
.levelOne > div {
background-color: salmon;
}
>
只會選到「.levelOne
的再下一層」css:
因為第三層有兩個 div,因此這兩個 div 都會被選到,因為這兩個 div 都是在 lv2 的下一層
.levelOne > div > div{
background-color: salmon;
}
>
就會選到「.levelOne
底下所有的 .bg-green
」html:
<div class="levelOne">
lv1
<div>
lv2
<div class="bg-green">lv3</div>
</div>
<div class="bg-green">hello</div>
</div>
不使用 >
,就可以選到 .levelOne
底下所有的 .bg-green
(不管是在第幾層,都會被選到)
.levelOne .bg-green{
background-color: green;
}
+
選到「同一層裡面,右邊那一個符合條件的元素」(一定要相鄰)在同一層中,「旁邊」指的就是「上下相鄰」的意思
規則:會被選到的只有 +
右邊的那個,+
左邊的不會被選到
html:
<div class="bg-red">div1</div>
<div>div2</div>
<div class="bg-red">div3</div>
<div class="bg-red">div4</div>
css:
這樣,會選到「.bg-red
右邊的那一個 .bg-red
」,也就是 <div class="bg-red">div4</div>
.bg-red + .bg-red {
background-color: red
}
html:
<div>123</div>
<span>456</span>
<span>7899</span>
css:
用 +
只會選到 div 右邊的那一個 span,因此就只會選到 <span>456</span>
而已
div + span {
background-color: lightgreen;
}
~
選到「同一層裡面,右邊所有符合條件的元素」(不一定要相鄰)規則:會被選到的只有 ~
右邊的元素,~
左邊的不會被選到
html:
<div>123</div>
<span>456</span>
<span>7899</span>
css:
用 ~
可以選到「div 右邊所有的 span」
div ~ span {
background-color: lightgreen;
}
+
和 ~
的使用時機html:
<span class="bg-red">span1</span>
<span class="bg-red">span2</span>
<span class="bg-red">span3</span>
<span class="bg-red">span4</span>
在做 navbar 的選項時,會希望:
最左邊沒有 margin,但是在每個元素中間有間距
因此,我只想要選到「後面三個元素」去套用 margin-left: 20px
css:
.bg-red ~ .bg-red {
background-color: red;
margin-left: 20px;
}
除了 :hover
之外,還有其他很多的 Pseudo-classes
nth-child 也是一種 Pseudo-class,可以幫我選到「這一層裡面的第 n 個子元素」
html:
<div class="wrapper">
<div>row1</div>
<div>row2</div>
<div>row3</div>
<div>row4</div>
<div>row5</div>
</div>
css:
用 :first-child
就可以只選到「.wrapper
這一層裡面的第一個子元素,且要是 div」
.wrapper div:first-child {
background-color: salmon
}
css:
用 :last-child
就可以只選到「.wrapper
這一層裡面的最後一個子元素,且要是 div」
.wrapper div:last-child {
background-color: salmon
}
css:
用 :nth-child(3)
就可以只選到「.wrapper
這一層裡面的第三個子元素,且要是 div」
會先看 :nth-child(3)
(是否為這一層裡面的第三個子元素),然後才看是否為 div
.wrapper div:nth-child(3) {
background-color: salmon
}
css:
用 :nth-child(odd)
就可以只選到「.wrapper
這一層裡面,被放在奇數列的子元素,且要是 div」
會先看 :nth-child(odd)
(是否為這一層裡面的奇數列的子元素),然後才看是否為 div
.wrapper div:nth-child(odd) {
background-color: salmon
}
html:
<div class="wrapper">
<div>row1</div>
<div>row2</div>
<div>row3</div>
<div>row4</div>
<div>row5</div>
<div>row6</div>
<div>row7</div>
<div>row8</div>
<div>row9</div>
</div>
css:
這裡的 (3n)
,n 會帶入 0, 1, 2, 3, 4...
所以,第 0, 3, 6, 9 個子元素且是 div 的就會被選到
.wrapper div:nth-child(3n) {
background-color: green;
}
小括弧內可以填入任何的運算
:nth-child()
會先看元素的順序現在,我想要選到「.wrapper
這一層裡面的第二個 .bg-green
」
html:
<div class="wrapper">
<div class="bg-green">row1</div>
<div>row2</div>
<div class="bg-green">row3</div>
<div>row4</div>
<div>row5</div>
</div>
css 這樣寫是錯的
.wrapper .bg-green:nth-child(2) {
background-color: green;
}
原因為:
.wrapper .bg-green:nth-child(2)
這個 selector 的意思是:選到 .wrapper
這一層裡面的第二個子元素,且要是 .bg-green
但是,.wrapper
裡面的第二個子元素是 <div>row2</div>
,並不是 .bg-green
,所以就選不到任何東西
要寫 .wrapper .bg-green:nth-child(3)
,才會選到「.wrapper
這一層裡面的第三個子元素,且要是 .bg-green
」
會先看 :nth-child(3)
(是否為這一層裡面的第三個子元素),然後才看是否為 .bg-green
.wrapper .bg-green:nth-child(3) {
background-color: green;
}
參考資料:偽元素一覽表
Pseudo Element (偽元素),可以選到「元素裡面的某個部份」
html:
<div class="price">
999
</div>
css:
content
,代表「偽元素裡面要裝的內容」content
的話,偽元素就不會出現content
的文字顏色,例如:.price::before {
content: '$';
color: orange;
}
attr()
抓出屬性的值html:
<div class="price">
999
</div>
css:
attr()
小括號裡面放入這個 html 標籤的屬性
可以用 attr(class)
把 class 這個屬性的值給抓出來
因此,attr(class)
就會是 price
.price::before {
content: attr(class);
color: orange;
}
html:
我自定一個屬性叫做 data-symbol="NTD"
<div class="price" data-symbol="NTD">
999
</div>
<div class="price" data-symbol="USD"">
30
</div>
css:
.price::after {
content: attr(data-symbol);
color: orange;
}
補充:
為什麼數字之間要有逗號而不是直接變成數字呢?那是因為如果你沒有逗號的話很容易誤解,像是用了 12 個 class 會變 120,用了一個 id 會變 100,你會以為 class 會蓋掉 id,但其實不是的,因為權重是:
12 個 class:0, 12, 0
1 個 id:1, 0, 0
意思是說無論你有幾個 class,你都不可能蓋掉 id,因為 id 權重永遠都比 class 高,不是逢十就能進位。
延伸閱讀:強烈推薦收藏好物 – CSS Specificity (CSS 權重一覽)
延伸閱讀:你對 CSS 權重真的足夠了解嗎?
!important > inline style > id > class > 標籤
!important
是 1, 0, 0, 0, 0舉例:
樣式 A 用了「一個 id、三個 class」就是 1, 3, 0
樣式 B 用了「15 個 class」就是 0, 15, 0
樣式 A 永遠都會蓋過樣式 B (不會逢十就進位)
html:
現在,第一個 div 和 span 都有 class="bg-green"
但是,我只想選到「同時符合又是 div 又是 class="bg-green"
」的元素,該怎麼寫 css 的 selector 呢?
<div class="bg-green">
hello1
</div>
<div>
hello2
</div>
<span class="bg-green">
hello3
</span>
css:
選到「同時符合又是 div 又是 class="bg-green"
」的元素
div.bg-green{
background-color: green;
}
html:
現在,我只想選到「同時具有 bg-green
和 bg-real-green
這兩個 class」的元素,該怎麼寫 css 的 selector 呢?
<div class="bg-green bg-real-green">
hello1
</div>
<div>
hello2
</div>
<div class="bg-green">
hello3
</div>
css:
選到「同時具有 bg-green
和 bg-real-green
這兩個 class」的元素
.bg-green.bg-real-green{
background-color: green;
}
html:
<div class="levelOne">
lv1
<div>
lv2
<div>lv3</div>
<div>hello</div>
</div>
</div>
>
只會選到「.levelOne
的下一層」css:
就會選到 lv2
.levelOne > div {
background-color: salmon;
}
>
只會選到「.levelOne
的再下一層」css:
因為第三層有兩個 div,因此這兩個 div 都會被選到,因為這兩個 div 都是在 lv2 的下一層
.levelOne > div > div{
background-color: salmon;
}
>
就會選到「.levelOne
底下所有的 .bg-green
」html:
<div class="levelOne">
lv1
<div>
lv2
<div class="bg-green">lv3</div>
</div>
<div class="bg-green">hello</div>
</div>
不使用 >
,就可以選到 .levelOne
底下所有的 .bg-green
(不管是在第幾層,都會被選到)
.levelOne .bg-green{
background-color: green;
}
+
選到「同一層裡面,右邊那一個符合條件的元素」(一定要相鄰)在同一層中,「旁邊」指的就是「上下相鄰」的意思
規則:會被選到的只有 +
右邊的那個,+
左邊的不會被選到
html:
<div class="bg-red">div1</div>
<div>div2</div>
<div class="bg-red">div3</div>
<div class="bg-red">div4</div>
css:
這樣,會選到「.bg-red
右邊的那一個 .bg-red
」,也就是 <div class="bg-red">div4</div>
.bg-red + .bg-red {
background-color: red
}
html:
<div>123</div>
<span>456</span>
<span>7899</span>
css:
用 +
只會選到 div 右邊的那一個 span,因此就只會選到 <span>456</span>
而已
div + span {
background-color: lightgreen;
}
~
選到「同一層裡面,右邊所有符合條件的元素」(不一定要相鄰)規則:會被選到的只有 ~
右邊的元素,~
左邊的不會被選到
html:
<div>123</div>
<span>456</span>
<span>7899</span>
css:
用 ~
可以選到「div 右邊所有的 span」
div ~ span {
background-color: lightgreen;
}
+
和 ~
的使用時機html:
<span class="bg-red">span1</span>
<span class="bg-red">span2</span>
<span class="bg-red">span3</span>
<span class="bg-red">span4</span>
在做 navbar 的選項時,會希望:
最左邊沒有 margin,但是在每個元素中間有間距
因此,我只想要選到「後面三個元素」去套用 margin-left: 20px
css:
.bg-red ~ .bg-red {
background-color: red;
margin-left: 20px;
}
除了 :hover
之外,還有其他很多的 Pseudo-classes
nth-child 也是一種 Pseudo-class,可以幫我選到「這一層裡面的第 n 個子元素」
html:
<div class="wrapper">
<div>row1</div>
<div>row2</div>
<div>row3</div>
<div>row4</div>
<div>row5</div>
</div>
css:
用 :first-child
就可以只選到「.wrapper
這一層裡面的第一個子元素,且要是 div」
.wrapper div:first-child {
background-color: salmon
}
css:
用 :last-child
就可以只選到「.wrapper
這一層裡面的最後一個子元素,且要是 div」
.wrapper div:last-child {
background-color: salmon
}
css:
用 :nth-child(3)
就可以只選到「.wrapper
這一層裡面的第三個子元素,且要是 div」
會先看 :nth-child(3)
(是否為這一層裡面的第三個子元素),然後才看是否為 div
.wrapper div:nth-child(3) {
background-color: salmon
}
css:
用 :nth-child(odd)
就可以只選到「.wrapper
這一層裡面,被放在奇數列的子元素,且要是 div」
會先看 :nth-child(odd)
(是否為這一層裡面的奇數列的子元素),然後才看是否為 div
.wrapper div:nth-child(odd) {
background-color: salmon
}
html:
<div class="wrapper">
<div>row1</div>
<div>row2</div>
<div>row3</div>
<div>row4</div>
<div>row5</div>
<div>row6</div>
<div>row7</div>
<div>row8</div>
<div>row9</div>
</div>
css:
這裡的 (3n)
,n 會帶入 0, 1, 2, 3, 4...
所以,第 0, 3, 6, 9 個子元素且是 div 的就會被選到
.wrapper div:nth-child(3n) {
background-color: green;
}
小括弧內可以填入任何的運算
:nth-child()
會先看元素的順序現在,我想要選到「.wrapper
這一層裡面的第二個 .bg-green
」
html:
<div class="wrapper">
<div class="bg-green">row1</div>
<div>row2</div>
<div class="bg-green">row3</div>
<div>row4</div>
<div>row5</div>
</div>
css 這樣寫是錯的
.wrapper .bg-green:nth-child(2) {
background-color: green;
}
原因為:
.wrapper .bg-green:nth-child(2)
這個 selector 的意思是:選到 .wrapper
這一層裡面的第二個子元素,且要是 .bg-green
但是,.wrapper
裡面的第二個子元素是 <div>row2</div>
,並不是 .bg-green
,所以就選不到任何東西
要寫 .wrapper .bg-green:nth-child(3)
,才會選到「.wrapper
這一層裡面的第三個子元素,且要是 .bg-green
」
會先看 :nth-child(3)
(是否為這一層裡面的第三個子元素),然後才看是否為 .bg-green
.wrapper .bg-green:nth-child(3) {
background-color: green;
}
參考資料:偽元素一覽表
Pseudo Element (偽元素),可以選到「元素裡面的某個部份」
html:
<div class="price">
999
</div>
css:
content
,代表「偽元素裡面要裝的內容」content
的話,偽元素就不會出現content
的文字顏色,例如:.price::before {
content: '$';
color: orange;
}
attr()
抓出屬性的值html:
<div class="price">
999
</div>
css:
attr()
小括號裡面放入這個 html 標籤的屬性
可以用 attr(class)
把 class 這個屬性的值給抓出來
因此,attr(class)
就會是 price
.price::before {
content: attr(class);
color: orange;
}
html:
我自定一個屬性叫做 data-symbol="NTD"
<div class="price" data-symbol="NTD">
999
</div>
<div class="price" data-symbol="USD"">
30
</div>
css:
.price::after {
content: attr(data-symbol);
color: orange;
}
補充:
為什麼數字之間要有逗號而不是直接變成數字呢?那是因為如果你沒有逗號的話很容易誤解,像是用了 12 個 class 會變 120,用了一個 id 會變 100,你會以為 class 會蓋掉 id,但其實不是的,因為權重是:
12 個 class:0, 12, 0
1 個 id:1, 0, 0
意思是說無論你有幾個 class,你都不可能蓋掉 id,因為 id 權重永遠都比 class 高,不是逢十就能進位。
延伸閱讀:強烈推薦收藏好物 – CSS Specificity (CSS 權重一覽)
延伸閱讀:你對 CSS 權重真的足夠了解嗎?
!important > inline style > id > class > 標籤
!important
是 1, 0, 0, 0, 0舉例:
樣式 A 用了「一個 id、三個 class」就是 1, 3, 0
樣式 B 用了「15 個 class」就是 0, 15, 0
樣式 A 永遠都會蓋過樣式 B (不會逢十就進位)
網頁一定要靠瀏覽器渲染,我們才能看得到畫面
HTML 的全名是 HyperText Markup Language (超文本標記語言)
HTML 是一個「標記語言」,不是一個 programming language(程式語言)
標記語言就是:有很多標籤,有一定的格式
瀏覽器就可以根據這些格式,把該有的樣子渲染出來
在 HTML 中,像是 <meta charset="utf-8" />
這種自己成對的標籤,最後面的那個反斜線可加可不加,因此寫成 <meta charset="utf-8">
也是可以的,但自己是習慣都會加上 /
參考資料:HTML 5: br tag
當我想要在 html 裡面顯示一段 JS 的程式碼時,
html:
<p>
function greeting () {
console.log('How are you?')
}
</p>
如果是使用 p 標籤,顯示出來的就會是這樣:
換行、縮排都不見了
<pre></pre>
標籤<pre>
是 preformatted text 的簡寫,會幫我把文字「預先做好格式化(瀏覽器會自動加上一些樣式)」,把我在 html 裡面打的程式碼,原封不動地顯示出來
(記得要把 html 程式碼推到最左,這樣顯示出來也會靠齊左側)
<pre>
function greeting () {
console.log('How are you?')
}
</pre>
要顯示一大堆空格也可以:
<pre>
function greeting () {
console.log('How are you?')
}
</pre>
<table>
表格
<tr>
table row
<td>
table cell
<th>
table header
<a>
標籤<a>
就是 anchor(錨點)
href 就是 hypertext reference
用 iframe 可以嵌入網頁
參考資料 其他更多 input 種類
SEO 在意的點就是:要幫助搜尋引擎去理解你的網頁
以下會介紹有助於 SEO 的標籤:
如果沒有這些標籤,搜尋引擎當然也是可以去爬你的網頁,藉由網頁內容去猜到「網頁標題、關鍵字、網頁敘述」這些,但可能會猜不準,或是需要花費很多時間去猜
因此才會提供這些標籤,讓網頁開發者可以透過格式化的方式,主動讓搜尋引擎知道「這個網頁提供了什麼內容」,搜尋引擎就可以更了解你的網頁,網頁在搜尋結果的排名就可能會更好
這裡以 Tripadvisor 的鼎泰丰(101店) 為例,打開 view page source 來看他的 html 原始碼
<meta name="keywords" content="信義區鼎泰豐(101店), 餐廳, 餐廳評論, 食物, 用餐"/>
<meta name="description" content="鼎泰豐(101店)(信義區): 讀讀4,651則則關於鼎泰豐(101店)客觀公正的美食評論,在Tripadvisor的5分滿分評等中得4.5分,在信義區的1,472家餐廳中排第12名。"/>
og 的全名是 Open Graph Protocol
因為要讓其他的 social media 更了解你的網頁,因此會有 Open Graph Protocol 的出現
<meta property="og:title" content="鼎泰豐(101店) (信義區) - 餐廳/美食評論 - Tripadvisor"/>
<meta property="og:description" content="鼎泰豐(101店)(信義區): 讀讀4,651則則關於鼎泰豐(101店)客觀公正的美食評論,在Tripadvisor的5分滿分評等中得4.5分,在信義區的1,472家餐廳中排第12名。"/>
<meta property="og:image" content="https://media-cdn.tripadvisor.com/media/photo-s/1a/4b/a8/19/101.jpg"/>
<meta property="og:image:width" content="338"/>
<meta property="og:image:height" content="450"/>
<meta property="og:type" content="restaurant"/>
<meta property="og:url" content="http://www.tripadvisor.com.tw/Restaurant_Review-g13808515-d2244808-Reviews-Din_Tai_Fung_101_Branch-Xinyi_District_Taipei.html"/>
<meta property="og:site_name" content="Tripadvisor"/>
這些 og 系列的標籤,主要是給 facebook 看的,可以使用這個 facebook 分享偵錯工具
在網址列把網址貼上後,按下「Debug」,就會告訴你這個網頁被 facebook 看起來是什麼樣子
底下這個 json 格式的東西叫做 JSON-ld,全名是 JSON for Linking Data
JSON-ld 的目的就跟 Open Graph Protocol 一樣:都是要讓你可以用一個格式化的方式來描述你的網頁
JSON-ld 通常是給 Google 看的,Tripadvisor 必須主動提供這些 JSON-ld 的資訊,Google 才可以在搜尋結果幫 Tripadvisor 的網站顯示更多資訊(下圖紅色框框處),有了這些資訊,也更容易吸引消費者點進去,讓網站排名上升
如果沒有這些固定格式和架構的話,搜尋引擎還需要特別去做一些資料分析才能讀懂你的網頁
<script type="application/ld+json">
{
"@context":"http:\u002F\u002Fschema.org",
"@type":"FoodEstablishment",
"name":"\u9F0E\u6CF0\u8C50(101\u5E97)",
"url":"\u002FRestaurant_Review-g13808515-d2244808-Reviews-Din_Tai_Fung_101_Branch-Xinyi_District_Taipei.html",
"image":"https:\u002F\u002Fmedia-cdn.tripadvisor.com\u002Fmedia\u002Fphoto-s\u002F1a\u002F4b\u002Fa8\u002F19\u002F101.jpg",
"priceRange":"$$ - $$$",
"aggregateRating":{
"@type":"AggregateRating",
"ratingValue":"4.5",
"reviewCount":"4651"},
"address":{
"@type":"PostalAddress",
"streetAddress":"\u5E02\u5E9C\u8DEF45\u865F B1",
"addressLocality":"\u4FE1\u7FA9\u5340",
"addressRegion":"",
"postalCode":"110",
"addressCountry":{
"@type":"Country",
"name":"\u53F0\u7063"
}
}
}
</script>
robots.txt 是「給網頁爬蟲看的檔案」(是一個純文字檔),通常都是放在根目錄底下,因此我只要在網址列輸入 https://www.tripadvisor.com.tw/robots.txt
就可以看到 Tripadvisor 的 robots.txt 檔案了:
這就是 robots.txt 檔案的作用
Sitemap 會是一個 XML 格式的檔案
Sitemap.xml 的作用是:把網站中每一個頁面的網址都列出來,讓搜尋引擎把這些網址都爬下來(搜尋引擎就不用自己一個一個去找網頁有哪些頁面)
可以確保:讓搜尋引擎知道網站中每一個頁面的存在
標籤的用途是:
跟搜尋引擎說「我這個網站有提供給其他國家用的語言」
假設這個網頁有中文版、英文版,同一個頁面但會有中文版和英文版兩個頁面,這時就可以利用這個標籤跟搜尋引擎說「其實這兩個頁面是同一個頁面,只是不同語言」
這樣的話,搜尋引擎給這個頁面的分數就可以集合在一起,不會因為被分成中文、英文版兩個頁面而分散掉
例如 "en-GB"
就是給 British English 用的版本,網址就是 href 寫的那段
<link rel="alternate" hreflang="en" href="https://www.tripadvisor.com/Restaurant_Review-g13808515-d2244808-Reviews-Din_Tai_Fung_101_Branch-Xinyi_District_Taipei.html" />
<link rel="alternate" hreflang="en-GB" href="https://www.tripadvisor.co.uk/Restaurant_Review-g13808515-d2244808-Reviews-Din_Tai_Fung_101_Branch-Xinyi_District_Taipei.html" />
底下的標籤是在跟搜尋引擎說「我這個網頁有 ios app,app 的名稱是 TripAdvisor,在 app store 的 ID 是 284876795」
加上這個標籤後,如果 user 是用 ios 的瀏覽器(safari)看 TripAdvisor 的網頁,瀏覽器就會跳出一個 banner 問 user 要不要下載 TripAdvisor 的 ios app
<meta property="al:ios:app_name" content="TripAdvisor">
<meta property="al:ios:app_store_id" content="284876795">
<meta property="al:ios:url" content="tripadvisor://www.tripadvisor.com.tw/Restaurant_Review-g13808515-d2244808-Reviews-Din_Tai_Fung_101_Branch-Xinyi_District_Taipei.html?m=33762">
底下的標籤也是一樣,用 twitter 瀏覽 TripAdvisor 的網頁,就會有其他效果
<meta property="twitter:app:id:ipad" name="twitter:app:id:ipad" content="284876795">
<meta property="twitter:app:id:iphone" name="twitter:app:id:iphone" content="284876795">
<meta property="twitter:app:url:iphone" name="twitter:app:url:iphone" content="tripadvisor://www.tripadvisor.com.tw/Restaurant_Review-g13808515-d2244808-Reviews-Din_Tai_Fung_101_Branch-Xinyi_District_Taipei.html?m=33762"><meta name="keywords" content="信義區鼎泰豐(101店), 餐廳, 餐廳評論, 食物, 用餐"/><meta name="description" content="鼎泰豐(101店)(信義區): 讀讀4,651則則關於鼎泰豐(101店)客觀公正的美食評論,在Tripadvisor的5分滿分評等中得4.5分,在信義區的1,472家餐廳中排第12名。"/>
Escape(跳脫)所代表的情況就是:這個字元本身有代表一個意思,但是我想把它當成純文字顯示出來
意思就是:會用其他方式來顯示這個字元,而不是把它當成「一組 html 標籤」來使用
例如:
在 div 標籤裡面,我想要把 <h1></h1>
當成純文字顯示在網頁上
<div>
<h1></h1>
</div>
作法:利用這些跳脫符號
& 用 & 取代
< 用 < 取代
> 用 > 取代
因此,就會變成這樣:
<div>
<h1></h1>
</div>
網頁上就會顯示出 <h1></h1>
了
網頁一定要靠瀏覽器渲染,我們才能看得到畫面
HTML 的全名是 HyperText Markup Language (超文本標記語言)
HTML 是一個「標記語言」,不是一個 programming language(程式語言)
標記語言就是:有很多標籤,有一定的格式
瀏覽器就可以根據這些格式,把該有的樣子渲染出來
在 HTML 中,像是 <meta charset="utf-8" />
這種自己成對的標籤,最後面的那個反斜線可加可不加,因此寫成 <meta charset="utf-8">
也是可以的,但自己是習慣都會加上 /
參考資料:HTML 5: br tag
當我想要在 html 裡面顯示一段 JS 的程式碼時,
html:
<p>
function greeting () {
console.log('How are you?')
}
</p>
如果是使用 p 標籤,顯示出來的就會是這樣:
換行、縮排都不見了
<pre></pre>
標籤<pre>
是 preformatted text 的簡寫,會幫我把文字「預先做好格式化(瀏覽器會自動加上一些樣式)」,把我在 html 裡面打的程式碼,原封不動地顯示出來
(記得要把 html 程式碼推到最左,這樣顯示出來也會靠齊左側)
<pre>
function greeting () {
console.log('How are you?')
}
</pre>
要顯示一大堆空格也可以:
<pre>
function greeting () {
console.log('How are you?')
}
</pre>
<table>
表格
<tr>
table row
<td>
table cell
<th>
table header
<a>
標籤<a>
就是 anchor(錨點)
href 就是 hypertext reference
用 iframe 可以嵌入網頁
參考資料 其他更多 input 種類
SEO 在意的點就是:要幫助搜尋引擎去理解你的網頁
以下會介紹有助於 SEO 的標籤:
如果沒有這些標籤,搜尋引擎當然也是可以去爬你的網頁,藉由網頁內容去猜到「網頁標題、關鍵字、網頁敘述」這些,但可能會猜不準,或是需要花費很多時間去猜
因此才會提供這些標籤,讓網頁開發者可以透過格式化的方式,主動讓搜尋引擎知道「這個網頁提供了什麼內容」,搜尋引擎就可以更了解你的網頁,網頁在搜尋結果的排名就可能會更好
這裡以 Tripadvisor 的鼎泰丰(101店) 為例,打開 view page source 來看他的 html 原始碼
<meta name="keywords" content="信義區鼎泰豐(101店), 餐廳, 餐廳評論, 食物, 用餐"/>
<meta name="description" content="鼎泰豐(101店)(信義區): 讀讀4,651則則關於鼎泰豐(101店)客觀公正的美食評論,在Tripadvisor的5分滿分評等中得4.5分,在信義區的1,472家餐廳中排第12名。"/>
og 的全名是 Open Graph Protocol
因為要讓其他的 social media 更了解你的網頁,因此會有 Open Graph Protocol 的出現
<meta property="og:title" content="鼎泰豐(101店) (信義區) - 餐廳/美食評論 - Tripadvisor"/>
<meta property="og:description" content="鼎泰豐(101店)(信義區): 讀讀4,651則則關於鼎泰豐(101店)客觀公正的美食評論,在Tripadvisor的5分滿分評等中得4.5分,在信義區的1,472家餐廳中排第12名。"/>
<meta property="og:image" content="https://media-cdn.tripadvisor.com/media/photo-s/1a/4b/a8/19/101.jpg"/>
<meta property="og:image:width" content="338"/>
<meta property="og:image:height" content="450"/>
<meta property="og:type" content="restaurant"/>
<meta property="og:url" content="http://www.tripadvisor.com.tw/Restaurant_Review-g13808515-d2244808-Reviews-Din_Tai_Fung_101_Branch-Xinyi_District_Taipei.html"/>
<meta property="og:site_name" content="Tripadvisor"/>
這些 og 系列的標籤,主要是給 facebook 看的,可以使用這個 facebook 分享偵錯工具
在網址列把網址貼上後,按下「Debug」,就會告訴你這個網頁被 facebook 看起來是什麼樣子
底下這個 json 格式的東西叫做 JSON-ld,全名是 JSON for Linking Data
JSON-ld 的目的就跟 Open Graph Protocol 一樣:都是要讓你可以用一個格式化的方式來描述你的網頁
JSON-ld 通常是給 Google 看的,Tripadvisor 必須主動提供這些 JSON-ld 的資訊,Google 才可以在搜尋結果幫 Tripadvisor 的網站顯示更多資訊(下圖紅色框框處),有了這些資訊,也更容易吸引消費者點進去,讓網站排名上升
如果沒有這些固定格式和架構的話,搜尋引擎還需要特別去做一些資料分析才能讀懂你的網頁
<script type="application/ld+json">
{
"@context":"http:\u002F\u002Fschema.org",
"@type":"FoodEstablishment",
"name":"\u9F0E\u6CF0\u8C50(101\u5E97)",
"url":"\u002FRestaurant_Review-g13808515-d2244808-Reviews-Din_Tai_Fung_101_Branch-Xinyi_District_Taipei.html",
"image":"https:\u002F\u002Fmedia-cdn.tripadvisor.com\u002Fmedia\u002Fphoto-s\u002F1a\u002F4b\u002Fa8\u002F19\u002F101.jpg",
"priceRange":"$$ - $$$",
"aggregateRating":{
"@type":"AggregateRating",
"ratingValue":"4.5",
"reviewCount":"4651"},
"address":{
"@type":"PostalAddress",
"streetAddress":"\u5E02\u5E9C\u8DEF45\u865F B1",
"addressLocality":"\u4FE1\u7FA9\u5340",
"addressRegion":"",
"postalCode":"110",
"addressCountry":{
"@type":"Country",
"name":"\u53F0\u7063"
}
}
}
</script>
robots.txt 是「給網頁爬蟲看的檔案」(是一個純文字檔),通常都是放在根目錄底下,因此我只要在網址列輸入 https://www.tripadvisor.com.tw/robots.txt
就可以看到 Tripadvisor 的 robots.txt 檔案了:
這就是 robots.txt 檔案的作用
Sitemap 會是一個 XML 格式的檔案
Sitemap.xml 的作用是:把網站中每一個頁面的網址都列出來,讓搜尋引擎把這些網址都爬下來(搜尋引擎就不用自己一個一個去找網頁有哪些頁面)
可以確保:讓搜尋引擎知道網站中每一個頁面的存在
標籤的用途是:
跟搜尋引擎說「我這個網站有提供給其他國家用的語言」
假設這個網頁有中文版、英文版,同一個頁面但會有中文版和英文版兩個頁面,這時就可以利用這個標籤跟搜尋引擎說「其實這兩個頁面是同一個頁面,只是不同語言」
這樣的話,搜尋引擎給這個頁面的分數就可以集合在一起,不會因為被分成中文、英文版兩個頁面而分散掉
例如 "en-GB"
就是給 British English 用的版本,網址就是 href 寫的那段
<link rel="alternate" hreflang="en" href="https://www.tripadvisor.com/Restaurant_Review-g13808515-d2244808-Reviews-Din_Tai_Fung_101_Branch-Xinyi_District_Taipei.html" />
<link rel="alternate" hreflang="en-GB" href="https://www.tripadvisor.co.uk/Restaurant_Review-g13808515-d2244808-Reviews-Din_Tai_Fung_101_Branch-Xinyi_District_Taipei.html" />
底下的標籤是在跟搜尋引擎說「我這個網頁有 ios app,app 的名稱是 TripAdvisor,在 app store 的 ID 是 284876795」
加上這個標籤後,如果 user 是用 ios 的瀏覽器(safari)看 TripAdvisor 的網頁,瀏覽器就會跳出一個 banner 問 user 要不要下載 TripAdvisor 的 ios app
<meta property="al:ios:app_name" content="TripAdvisor">
<meta property="al:ios:app_store_id" content="284876795">
<meta property="al:ios:url" content="tripadvisor://www.tripadvisor.com.tw/Restaurant_Review-g13808515-d2244808-Reviews-Din_Tai_Fung_101_Branch-Xinyi_District_Taipei.html?m=33762">
底下的標籤也是一樣,用 twitter 瀏覽 TripAdvisor 的網頁,就會有其他效果
<meta property="twitter:app:id:ipad" name="twitter:app:id:ipad" content="284876795">
<meta property="twitter:app:id:iphone" name="twitter:app:id:iphone" content="284876795">
<meta property="twitter:app:url:iphone" name="twitter:app:url:iphone" content="tripadvisor://www.tripadvisor.com.tw/Restaurant_Review-g13808515-d2244808-Reviews-Din_Tai_Fung_101_Branch-Xinyi_District_Taipei.html?m=33762"><meta name="keywords" content="信義區鼎泰豐(101店), 餐廳, 餐廳評論, 食物, 用餐"/><meta name="description" content="鼎泰豐(101店)(信義區): 讀讀4,651則則關於鼎泰豐(101店)客觀公正的美食評論,在Tripadvisor的5分滿分評等中得4.5分,在信義區的1,472家餐廳中排第12名。"/>
Escape(跳脫)所代表的情況就是:這個字元本身有代表一個意思,但是我想把它當成純文字顯示出來
意思就是:會用其他方式來顯示這個字元,而不是把它當成「一組 html 標籤」來使用
例如:
在 div 標籤裡面,我想要把 <h1></h1>
當成純文字顯示在網頁上
<div>
<h1></h1>
</div>
作法:利用這些跳脫符號
& 用 & 取代
< 用 < 取代
> 用 > 取代
因此,就會變成這樣:
<div>
<h1></h1>
</div>
網頁上就會顯示出 <h1></h1>
了
GET request 不會有 Body,因為我只是想要取得一些資訊,我不需要告訴 server 任何東西
我只要發送 GET request 到特定的網址即可(server 會有不同的網址去處理不同的 GET request)
而 request Header 就是帶一些額外的資訊
POST request 就會在 Body 放「我要傳送的資料」
而 request Header 就是帶一些額外的資訊
「server 會有不同的網址去處理不同的 request」,這句話的意思是:
Base URL 就是「這個 API 的網址」
一個 API 服務會提供很多不同的資源,不同的資源就會用不同的網址去做區分。只要在 Base URL 後面加上不同的 path,就是代表不同的資源
例如:
Base URL: https://lidemy-book-store.herokuapp.com
如果我要取得所有書籍的資料,就是要發一個 GET request 到 https://lidemy-book-store.herokuapp.com/books 這個網址去
要帶資訊到 server,有幾種方式:
參數中的 name, age 就是「key」,romeo, 28 就是「value」
因此我就可以把下面這個物件的資訊用網址傳送給 server,就可以這樣寫:
https://lidemy-http-challenge.herokuapp.com/lv1?name=romeo&age=28
const obj = {
name: 'romeo',
age: 28
}
當我用 GET 時,要帶資訊到 server 只有兩種方式:
?_limit=5
就是 query string當我用 GET 拿取資訊時,有時候我需要帶一些額外的資訊到 server,但又不能用 Header 帶,因為跟 Header 會帶的資訊是不同的種類
'Client-ID': 'XXXXX'
來驗證「我這個人是否有註冊過」因此就演變成:直接用網址來帶一些特定的資訊
例如:
在 week4 的 hw1,
當我用 GET 來拿取書籍資料時,如果我只是發一個 GET request 到網址 https://lidemy-book-store.herokuapp.com/books ,server 預設會回傳給我很多很多筆資料
但是我只想要 5 筆資料,我要怎麼把「只要 5 筆資料」這個資訊帶給 server 呢?
不可能用 POST(因為 POST 除了傳資料之外,其實有”新增“資料的意思)
因此,就要用「在網址後面帶參數」的方式把「只要 5 筆資料」這個資訊帶給 server (用 _limit
這個參數來限制回傳資料數量):
https://lidemy-book-store.herokuapp.com/books?_limit=5
這樣就可以只拿到 5 筆資料了!
在網址後面帶上 query string
const request = require('request');
request(
'https://lidemy-book-store.herokuapp.com/books?_limit=5',
function (error, response, body) {
console.log(body);
}
);
執行 node index.js
之後,就可以拿到 5 筆書籍的資料了!
不一定,要看 server 有沒有支援 Headers 和 query string 這兩種不同的資料來源
https://lidemy-http-challenge.herokuapp.com/lv1?token=xxx
可以想成是:這串網址,會幫我呼叫一個遠端的 lv1
function,它會拿到我帶的 Headers 和 query string,如果這個 function 會把 Headers 和 query string 都做檢查,那就是 server 有同時支援 Headers 和 query string 這兩種資料來源(意思就是:我要用 Headers 或是 query string 都可以帶資訊)
但有時候,server 只會針對其中一個做檢查。
所以,API 文件會告訴我「資訊是要帶在 Headers 還是要帶在 query string 裡面」
function lv1(headers, queryString){
if(headers['token']...) ...
if(queryString) ...
}
Twitch API 的文件 Getting a client ID 說「要傳送 client ID,Twitch API 有支援兩種方式」:
Content-Type
很重要的一個 request Header(根據 Content-Type
不同,Body 內容也會不同)在 POST 的時候,要在 request Body 寫上「我要 POST 的資訊」
在 Body 所寫的內容,可以有很多種格式:
有可能是 name=romeo
或是 name:romeo
在 request Headers 中,有一項很重要的叫做 Content-Type
,當我用 POST 或是 PATCH 時,我要用 Content-Type
來告訴 server「我傳送的資料內容是什麼格式」
要用哪種 Content-Type
,也是要看 server 有支援哪種
最常見的格式有:
application/x-www-form-urlencoded
(表單格式)用這種格式的話,Body 的內容就會像是這樣:
body: 'name=romeo&age=28'
例如:
在 request 套件的 Forms
request.post('http://service.com/upload', {
form: { name: 'romeo' }
})
這段的意思就是:我要用 POST method 發送一個 request 到 http://service.com/upload 這個網址,同時帶上一個「表單格式」的資料是 { name: 'romeo' }
當我在 Body 用 name: 'romeo'
這樣的格式寫完資料後,當這個 POST request 真的被發送出去時,request 套件會幫我把 Content-Type
這個 request Header 自動填上 application/x-www-form-urlencoded
這個格式(幫我跟 server 說:我現在的 Body 格式是 application/x-www-form-urlencoded
)。
然後,會把 { name: 'romeo' }
這個物件,按照「表單格式」轉成 'name=romeo'
這樣的字串,再放到 Body 裡面。
當 server 收到 request Header 寫的 Content-Type: application/x-www-form-urlencoded
時,才知道要怎麼去解析 'name=romeo'
這樣的字串
因此,儘管帶的資料是正確的 'name=romeo'
,但如果亂寫一個 Content-Type: kjjmmmm
,server 也不會知道要怎麼去解析我的資料
application/x-www-form-urlencoded
之所以會叫做「表單格式」,是因為在 html 的表單要送出元素時,也是用這種格式來送出
multipart/form-data
multipart/form-data
這個格式通常是用來上傳檔案(例如:圖片)用的
application/json
如果我的 request Header 是寫 Content-Type: application/json
,就是在跟 server 說:我在 request Body 傳送的內容會是一個 json 格式的字串
因此,在寫 POST 時,就要自己用 JSON.stringify()
來把「JavaScript 的物件」轉成一個「json 格式的字串」:
request.post({
url: 'https://api.github.com/repos/request/request',
headers: {
'content-type': 'application/json'
}
},
{
body: JSON.stringify({ name: 'romeo', age: 28 })
},
callback);
因此,body: JSON.stringify({ name: 'romeo', age: 28 })
這句就會變成是 body: "{"name":"romeo","age":28}"
(json 格式的字串)
當我在 request 套件用 Forms 傳資料時,用 request 包裝過後的形式來寫,比較簡短方便:
request.post('http://service.com/upload', {
form: {
name: 'romeo',
age: 28
}
})
就等同於是下面這段:用物件來寫,彈性比較大,可以自己加上很多東西
但是因為用物件寫這段太麻煩了,所以 request 套件才會把這段包裝成上面那樣很簡短的形式,方便開發者去寫
request({
method: 'POST',
url: 'http://service.com/upload',
headers: {
'content-type': 'application/x-www-form-urlencoded'
},
body: 'name=romeo&age=28'
})
application/x-www-form-urlencoded
會碰到的問題假設,在我要傳的資料中,有一項是 str: 'a&b=20'
request.post('http://service.com/upload', {
form: {
name: 'romeo',
age: 28,
str: 'a&b=20'
}
})
那在轉成表單格式後,就會是這樣:
request({
method: 'POST',
url: 'http://service.com/upload',
headers: {
'content-type': 'application/x-www-form-urlencoded'
},
body: 'name=romeo&age=28&str=a&b=20'
})
這句 str=a&b=20
會造成 server 的混淆(中間的 &
讓它變成兩個參數)
在 application/x-www-form-urlencoded
格式,其中的 encode 是「編碼」的意思-> 像是把英文字母變成摩斯密碼這樣,一個英文字母就對應到一個摩斯密碼(只是不同資料形式的轉換而已)
encodeURIComponent()
編碼在 JavaScript 有一個 function 可以用,叫做 encodeURIComponent()
,小括號裡面放入我要 encode 的字串
在 Console 輸入:
encodeURIComponent('a&b=20')
會得到:
"a%26b%3D20"
可以推敲出:
&
被編碼後會是 %26
=
被編碼後會是 %3D
當 server 看到 %26
就會知道它是 &
因此,在 Body 裡面就可以改成用編碼過後的 str=a%26b%3D20
:
這樣,就不會因為 value 有 &
和 =
而造成 server 的混淆了
request({
method: 'POST',
url: 'http://service.com/upload',
headers: {
'content-type': 'application/x-www-form-urlencoded'
},
body: 'name=romeo&age=28&str=a%26b%3D20'
})
但是有些時候,要傳的資料內容是動態的,我也無法預測會不會有這些會讓 server 混淆的字元出現。所以,就直接會先把每個 value 都進行編碼,這樣最保險
]]>GET request 不會有 Body,因為我只是想要取得一些資訊,我不需要告訴 server 任何東西
我只要發送 GET request 到特定的網址即可(server 會有不同的網址去處理不同的 GET request)
而 request Header 就是帶一些額外的資訊
POST request 就會在 Body 放「我要傳送的資料」
而 request Header 就是帶一些額外的資訊
「server 會有不同的網址去處理不同的 request」,這句話的意思是:
Base URL 就是「這個 API 的網址」
一個 API 服務會提供很多不同的資源,不同的資源就會用不同的網址去做區分。只要在 Base URL 後面加上不同的 path,就是代表不同的資源
例如:
Base URL: https://lidemy-book-store.herokuapp.com
如果我要取得所有書籍的資料,就是要發一個 GET request 到 https://lidemy-book-store.herokuapp.com/books 這個網址去
要帶資訊到 server,有幾種方式:
參數中的 name, age 就是「key」,romeo, 28 就是「value」
因此我就可以把下面這個物件的資訊用網址傳送給 server,就可以這樣寫:
https://lidemy-http-challenge.herokuapp.com/lv1?name=romeo&age=28
const obj = {
name: 'romeo',
age: 28
}
當我用 GET 時,要帶資訊到 server 只有兩種方式:
?_limit=5
就是 query string當我用 GET 拿取資訊時,有時候我需要帶一些額外的資訊到 server,但又不能用 Header 帶,因為跟 Header 會帶的資訊是不同的種類
'Client-ID': 'XXXXX'
來驗證「我這個人是否有註冊過」因此就演變成:直接用網址來帶一些特定的資訊
例如:
在 week4 的 hw1,
當我用 GET 來拿取書籍資料時,如果我只是發一個 GET request 到網址 https://lidemy-book-store.herokuapp.com/books ,server 預設會回傳給我很多很多筆資料
但是我只想要 5 筆資料,我要怎麼把「只要 5 筆資料」這個資訊帶給 server 呢?
不可能用 POST(因為 POST 除了傳資料之外,其實有”新增“資料的意思)
因此,就要用「在網址後面帶參數」的方式把「只要 5 筆資料」這個資訊帶給 server (用 _limit
這個參數來限制回傳資料數量):
https://lidemy-book-store.herokuapp.com/books?_limit=5
這樣就可以只拿到 5 筆資料了!
在網址後面帶上 query string
const request = require('request');
request(
'https://lidemy-book-store.herokuapp.com/books?_limit=5',
function (error, response, body) {
console.log(body);
}
);
執行 node index.js
之後,就可以拿到 5 筆書籍的資料了!
不一定,要看 server 有沒有支援 Headers 和 query string 這兩種不同的資料來源
https://lidemy-http-challenge.herokuapp.com/lv1?token=xxx
可以想成是:這串網址,會幫我呼叫一個遠端的 lv1
function,它會拿到我帶的 Headers 和 query string,如果這個 function 會把 Headers 和 query string 都做檢查,那就是 server 有同時支援 Headers 和 query string 這兩種資料來源(意思就是:我要用 Headers 或是 query string 都可以帶資訊)
但有時候,server 只會針對其中一個做檢查。
所以,API 文件會告訴我「資訊是要帶在 Headers 還是要帶在 query string 裡面」
function lv1(headers, queryString){
if(headers['token']...) ...
if(queryString) ...
}
Twitch API 的文件 Getting a client ID 說「要傳送 client ID,Twitch API 有支援兩種方式」:
Content-Type
很重要的一個 request Header(根據 Content-Type
不同,Body 內容也會不同)在 POST 的時候,要在 request Body 寫上「我要 POST 的資訊」
在 Body 所寫的內容,可以有很多種格式:
有可能是 name=romeo
或是 name:romeo
在 request Headers 中,有一項很重要的叫做 Content-Type
,當我用 POST 或是 PATCH 時,我要用 Content-Type
來告訴 server「我傳送的資料內容是什麼格式」
要用哪種 Content-Type
,也是要看 server 有支援哪種
最常見的格式有:
application/x-www-form-urlencoded
(表單格式)用這種格式的話,Body 的內容就會像是這樣:
body: 'name=romeo&age=28'
例如:
在 request 套件的 Forms
request.post('http://service.com/upload', {
form: { name: 'romeo' }
})
這段的意思就是:我要用 POST method 發送一個 request 到 http://service.com/upload 這個網址,同時帶上一個「表單格式」的資料是 { name: 'romeo' }
當我在 Body 用 name: 'romeo'
這樣的格式寫完資料後,當這個 POST request 真的被發送出去時,request 套件會幫我把 Content-Type
這個 request Header 自動填上 application/x-www-form-urlencoded
這個格式(幫我跟 server 說:我現在的 Body 格式是 application/x-www-form-urlencoded
)。
然後,會把 { name: 'romeo' }
這個物件,按照「表單格式」轉成 'name=romeo'
這樣的字串,再放到 Body 裡面。
當 server 收到 request Header 寫的 Content-Type: application/x-www-form-urlencoded
時,才知道要怎麼去解析 'name=romeo'
這樣的字串
因此,儘管帶的資料是正確的 'name=romeo'
,但如果亂寫一個 Content-Type: kjjmmmm
,server 也不會知道要怎麼去解析我的資料
application/x-www-form-urlencoded
之所以會叫做「表單格式」,是因為在 html 的表單要送出元素時,也是用這種格式來送出
multipart/form-data
multipart/form-data
這個格式通常是用來上傳檔案(例如:圖片)用的
application/json
如果我的 request Header 是寫 Content-Type: application/json
,就是在跟 server 說:我在 request Body 傳送的內容會是一個 json 格式的字串
因此,在寫 POST 時,就要自己用 JSON.stringify()
來把「JavaScript 的物件」轉成一個「json 格式的字串」:
request.post({
url: 'https://api.github.com/repos/request/request',
headers: {
'content-type': 'application/json'
}
},
{
body: JSON.stringify({ name: 'romeo', age: 28 })
},
callback);
因此,body: JSON.stringify({ name: 'romeo', age: 28 })
這句就會變成是 body: "{"name":"romeo","age":28}"
(json 格式的字串)
當我在 request 套件用 Forms 傳資料時,用 request 包裝過後的形式來寫,比較簡短方便:
request.post('http://service.com/upload', {
form: {
name: 'romeo',
age: 28
}
})
就等同於是下面這段:用物件來寫,彈性比較大,可以自己加上很多東西
但是因為用物件寫這段太麻煩了,所以 request 套件才會把這段包裝成上面那樣很簡短的形式,方便開發者去寫
request({
method: 'POST',
url: 'http://service.com/upload',
headers: {
'content-type': 'application/x-www-form-urlencoded'
},
body: 'name=romeo&age=28'
})
application/x-www-form-urlencoded
會碰到的問題假設,在我要傳的資料中,有一項是 str: 'a&b=20'
request.post('http://service.com/upload', {
form: {
name: 'romeo',
age: 28,
str: 'a&b=20'
}
})
那在轉成表單格式後,就會是這樣:
request({
method: 'POST',
url: 'http://service.com/upload',
headers: {
'content-type': 'application/x-www-form-urlencoded'
},
body: 'name=romeo&age=28&str=a&b=20'
})
這句 str=a&b=20
會造成 server 的混淆(中間的 &
讓它變成兩個參數)
在 application/x-www-form-urlencoded
格式,其中的 encode 是「編碼」的意思-> 像是把英文字母變成摩斯密碼這樣,一個英文字母就對應到一個摩斯密碼(只是不同資料形式的轉換而已)
encodeURIComponent()
編碼在 JavaScript 有一個 function 可以用,叫做 encodeURIComponent()
,小括號裡面放入我要 encode 的字串
在 Console 輸入:
encodeURIComponent('a&b=20')
會得到:
"a%26b%3D20"
可以推敲出:
&
被編碼後會是 %26
=
被編碼後會是 %3D
當 server 看到 %26
就會知道它是 &
因此,在 Body 裡面就可以改成用編碼過後的 str=a%26b%3D20
:
這樣,就不會因為 value 有 &
和 =
而造成 server 的混淆了
request({
method: 'POST',
url: 'http://service.com/upload',
headers: {
'content-type': 'application/x-www-form-urlencoded'
},
body: 'name=romeo&age=28&str=a%26b%3D20'
})
但是有些時候,要傳的資料內容是動態的,我也無法預測會不會有這些會讓 server 混淆的字元出現。所以,就直接會先把每個 value 都進行編碼,這樣最保險
]]>SOAP 的全名是 Simple Object Access Protocol,是一個協定,定義了另一種跟 server 溝通的方式
SOAP 的資料交換(request, response)都是透過 XML 的格式
因為 XML 的格式寫起來比較麻煩,因此都會透過一套 Node.js 的 library 叫做 node-soap 讓我更方便地去寫 request
例如:
先建立一個 server,在 server 提供一個 function 叫做 myFunction
client 端連接到 server 後,就可以用 client.MyFunction()
直接呼叫這個 myFunction
函式,這個 function 就是「server 提供給我呼叫的 function」
注意!底層還是一樣是用 XML 在溝通(也就是說,myFunction
會使用 XML 的格式,帶入一些參數)
var soap = require('soap');
var url = 'http://example.com/wsdl?wsdl';
var args = {name: 'value'};
soap.createClient(url, function(err, client) {
client.MyFunction(args, function(err, result) {
console.log(result);
});
});
HTTP API 主要分成兩類:
「SOAP 以外的 HTTP API」交換資料的方式是:
發送 request 到一個 API 網址,然後就會回傳一個 JSON 格式的 response
例如:PokéAPI、reqres
RESTful 不是一個協定,它只是一種「風格」
(建議你遵循 RESTful 這種風格,但不強制)
RESTful 風格就是:希望你可以好好的去使用這些 HTTP 的 methods
下圖中的 endpoints (也就是最右邊那欄)就是“沒有遵循” RESTful 風格的
可以看到在上圖的 API,要「刪除使用者」的話,是使用 POST 發送 request 到 /delete_user 這個網址去
這裡你可能會疑問的是:刪除不是要用 DELETE 嗎?
要注意的是:
「刪除」用 DELETE,是一個「良好的習慣」,並不是一個「規範」
只要後端 server 有處理好這件事情,用 POST 一樣也可以模擬「刪除」的功能
因為後端 server 也有 /delete_user 這個網址,所以,就算我是使用 POST,後端也會知道「所有被送到 /delete_user 這個網址的 request 都是要“刪除使用者”的」,並不會跟「新增使用者的 POST」搞混
但是,「不遵循良好習慣」的壞處是:
例如:
「新增使用者」這個功能,有人取名為 /new_user,另一個人取名為 /create_user,另一個人取名為 /create_new_user
雖然說使用 POST 一樣也可以做到「刪除」的功能,但是「用 DELETE 來刪除」才是具有語意的作法
就像在寫 html 時,所有的標籤都可以用 <div>
, <span>
來寫,但這樣是沒有語意的。應該要使用 <session>
, <article>
這些有語意的標籤比較好
下圖中的 endpoints (也就是最右邊那欄)就是有遵循 RESTful 風格的
例如:
reqres 就是一個 RESTful API
其實,有很多 API 是沒有建立在 HTTP 之上的,而是建立在其他的 protocol 上,並使用不同的資料格式來交換資料
輸入指令 curl https://github.com/yuwensaf
意思就是:我要發一個 GET 的 request 到 https://github.com/yuwensaf 這個網址(我的 GitHub 頁面)去,因此,得到的 response 就會是這個網頁的 html 程式碼
使用指令 curl https://github.com/yuwensaf > myGithub.html
可以把這個 response 導向到一個新的檔案(myGithub.html),這樣我就成功地把我的 GitHub 頁面下載下來了
打開 myGithub.html,就可以看到我的 GitHub 頁面
加上參數 -I
,意思是:我只要 Header,我不要 Body
因此,輸入指令 curl -I https://github.com/yuwensaf
就可以看到 https://github.com/yuwensaf 這個網頁的 Header 內容了
用 curl 來 POST JSON 格式的資料,寫法是這樣:
這裡的 \
是「換行」的意思
curl --header "Content-Type: application/json" \
--request POST \
--data '{"username":"xyz","password":"xyz"}' \
http://localhost:3000/api/login
拿之前講過的 reqres API 自己改一下:
curl --header "Content-Type: application/json" \
--request POST \
--data '{"name":"Romeo","job":"farmer"}' \
https://reqres.in/api/users
意思就是:
對 https://reqres.in/api/users 這個網址發一個 POST 的 request,是 JSON 格式的 request:{"name":"Romeo","job":"farmer"}
執行指令後就可以看到回傳的 response,代表我成功建立了一個 Romeo 這個新的 user:
{"name":"Romeo","job":"farmer","id":"887","createdAt":"2020-07-24T13:46:55.365Z"}%
使用指令 nslookup github.com
,就會回傳 github.com 這個 domain 的 IP 地址
使用指令 ping google.com
,就會一直丟封包到 google.com,藉此來測試自己是否可以連到 google.com 這台 server(是一個測試的步驟)
如果出現錯誤,那就代表有一方的網路壞掉了
按下 ctrl+C 可以結束指令
telnet 有很多用途,最簡單的用途就是:去 ping 一個指定的 port(連到一個指定的 port)
例如:
我用 nslookup github.com
查詢到 github.com 的 IP 地址是 140.82.112.4,我想要針對它的 80
port 去做 telnet(80
是 HTTP 的 port)
telnet 140.82.113.4 80
,意思就是:我要連到 140.82.113.4 的 80
port結果回傳「Connected...」就代表:140.82.113.4 的 80
port 是有打開的
如果等很久都沒有回傳「Connected…」 ,就代表那個 port 沒有開
輸入指令 telnet 140.82.113.4 80
後,可以打一些字(傳資料給 github.com)
因為 github.com 是一個 HTTP 的 server,所以可以使用 GET 這個 HTTP method 來取得這個網頁的 html 程式碼
寫法如下:
telnet [SERVER] [PORT]
Trying xxx.xxx.xxx.xxx...
Connected to [SERVER].
Escape character is '^]'.
GET [WEB PAGE] HTTP/1.1
HOST: [SERVER]
<Press ENTER>
所以我輸入的指令是:
telnet 140.82.114.3 80
Trying 140.82.114.3...
Connected to lb-140-82-114-3-iad.github.com.
Escape character is '^]'.
GET / HTTP/1.1
HOST: github.com
但是就只有回傳這樣:
HTTP/1.1 301 Moved Permanently
Content-length: 0
Location: https://github.com/
說明:
回傳 301 Moved Permanently 就是轉址的意思,所以它叫你去找 https://github.com/
但用 telnet 應該是沒辦法直接連 https,因為有些憑證相關的東西要處理
輸入指令 telnet ptt.cc
,意思就是:我要連到 ptt.cc 的 23
port
ptt 這套系統就是基於 telnet 協定
telnet 預設給 ppt 的 port 是 23,因此沒有輸入 port 也可以,telnet ptt.cc
預設就會是 telnet ptt.cc 23
網路的本質就是為了要「溝通」
因為希望溝通可以被「規模化」,因此就需要訂定一些「協定」讓大家去遵循這些標準
而身為網路相關的工程師,就必須去了解這些協定
]]>SOAP 的全名是 Simple Object Access Protocol,是一個協定,定義了另一種跟 server 溝通的方式
SOAP 的資料交換(request, response)都是透過 XML 的格式
因為 XML 的格式寫起來比較麻煩,因此都會透過一套 Node.js 的 library 叫做 node-soap 讓我更方便地去寫 request
例如:
先建立一個 server,在 server 提供一個 function 叫做 myFunction
client 端連接到 server 後,就可以用 client.MyFunction()
直接呼叫這個 myFunction
函式,這個 function 就是「server 提供給我呼叫的 function」
注意!底層還是一樣是用 XML 在溝通(也就是說,myFunction
會使用 XML 的格式,帶入一些參數)
var soap = require('soap');
var url = 'http://example.com/wsdl?wsdl';
var args = {name: 'value'};
soap.createClient(url, function(err, client) {
client.MyFunction(args, function(err, result) {
console.log(result);
});
});
HTTP API 主要分成兩類:
「SOAP 以外的 HTTP API」交換資料的方式是:
發送 request 到一個 API 網址,然後就會回傳一個 JSON 格式的 response
例如:PokéAPI、reqres
RESTful 不是一個協定,它只是一種「風格」
(建議你遵循 RESTful 這種風格,但不強制)
RESTful 風格就是:希望你可以好好的去使用這些 HTTP 的 methods
下圖中的 endpoints (也就是最右邊那欄)就是“沒有遵循” RESTful 風格的
可以看到在上圖的 API,要「刪除使用者」的話,是使用 POST 發送 request 到 /delete_user 這個網址去
這裡你可能會疑問的是:刪除不是要用 DELETE 嗎?
要注意的是:
「刪除」用 DELETE,是一個「良好的習慣」,並不是一個「規範」
只要後端 server 有處理好這件事情,用 POST 一樣也可以模擬「刪除」的功能
因為後端 server 也有 /delete_user 這個網址,所以,就算我是使用 POST,後端也會知道「所有被送到 /delete_user 這個網址的 request 都是要“刪除使用者”的」,並不會跟「新增使用者的 POST」搞混
但是,「不遵循良好習慣」的壞處是:
例如:
「新增使用者」這個功能,有人取名為 /new_user,另一個人取名為 /create_user,另一個人取名為 /create_new_user
雖然說使用 POST 一樣也可以做到「刪除」的功能,但是「用 DELETE 來刪除」才是具有語意的作法
就像在寫 html 時,所有的標籤都可以用 <div>
, <span>
來寫,但這樣是沒有語意的。應該要使用 <session>
, <article>
這些有語意的標籤比較好
下圖中的 endpoints (也就是最右邊那欄)就是有遵循 RESTful 風格的
例如:
reqres 就是一個 RESTful API
其實,有很多 API 是沒有建立在 HTTP 之上的,而是建立在其他的 protocol 上,並使用不同的資料格式來交換資料
輸入指令 curl https://github.com/yuwensaf
意思就是:我要發一個 GET 的 request 到 https://github.com/yuwensaf 這個網址(我的 GitHub 頁面)去,因此,得到的 response 就會是這個網頁的 html 程式碼
使用指令 curl https://github.com/yuwensaf > myGithub.html
可以把這個 response 導向到一個新的檔案(myGithub.html),這樣我就成功地把我的 GitHub 頁面下載下來了
打開 myGithub.html,就可以看到我的 GitHub 頁面
加上參數 -I
,意思是:我只要 Header,我不要 Body
因此,輸入指令 curl -I https://github.com/yuwensaf
就可以看到 https://github.com/yuwensaf 這個網頁的 Header 內容了
用 curl 來 POST JSON 格式的資料,寫法是這樣:
這裡的 \
是「換行」的意思
curl --header "Content-Type: application/json" \
--request POST \
--data '{"username":"xyz","password":"xyz"}' \
http://localhost:3000/api/login
拿之前講過的 reqres API 自己改一下:
curl --header "Content-Type: application/json" \
--request POST \
--data '{"name":"Romeo","job":"farmer"}' \
https://reqres.in/api/users
意思就是:
對 https://reqres.in/api/users 這個網址發一個 POST 的 request,是 JSON 格式的 request:{"name":"Romeo","job":"farmer"}
執行指令後就可以看到回傳的 response,代表我成功建立了一個 Romeo 這個新的 user:
{"name":"Romeo","job":"farmer","id":"887","createdAt":"2020-07-24T13:46:55.365Z"}%
使用指令 nslookup github.com
,就會回傳 github.com 這個 domain 的 IP 地址
使用指令 ping google.com
,就會一直丟封包到 google.com,藉此來測試自己是否可以連到 google.com 這台 server(是一個測試的步驟)
如果出現錯誤,那就代表有一方的網路壞掉了
按下 ctrl+C 可以結束指令
telnet 有很多用途,最簡單的用途就是:去 ping 一個指定的 port(連到一個指定的 port)
例如:
我用 nslookup github.com
查詢到 github.com 的 IP 地址是 140.82.112.4,我想要針對它的 80
port 去做 telnet(80
是 HTTP 的 port)
telnet 140.82.113.4 80
,意思就是:我要連到 140.82.113.4 的 80
port結果回傳「Connected...」就代表:140.82.113.4 的 80
port 是有打開的
如果等很久都沒有回傳「Connected…」 ,就代表那個 port 沒有開
輸入指令 telnet 140.82.113.4 80
後,可以打一些字(傳資料給 github.com)
因為 github.com 是一個 HTTP 的 server,所以可以使用 GET 這個 HTTP method 來取得這個網頁的 html 程式碼
寫法如下:
telnet [SERVER] [PORT]
Trying xxx.xxx.xxx.xxx...
Connected to [SERVER].
Escape character is '^]'.
GET [WEB PAGE] HTTP/1.1
HOST: [SERVER]
<Press ENTER>
所以我輸入的指令是:
telnet 140.82.114.3 80
Trying 140.82.114.3...
Connected to lb-140-82-114-3-iad.github.com.
Escape character is '^]'.
GET / HTTP/1.1
HOST: github.com
但是就只有回傳這樣:
HTTP/1.1 301 Moved Permanently
Content-length: 0
Location: https://github.com/
說明:
回傳 301 Moved Permanently 就是轉址的意思,所以它叫你去找 https://github.com/
但用 telnet 應該是沒辦法直接連 https,因為有些憑證相關的東西要處理
輸入指令 telnet ptt.cc
,意思就是:我要連到 ptt.cc 的 23
port
ptt 這套系統就是基於 telnet 協定
telnet 預設給 ppt 的 port 是 23,因此沒有輸入 port 也可以,telnet ptt.cc
預設就會是 telnet ptt.cc 23
網路的本質就是為了要「溝通」
因為希望溝通可以被「規模化」,因此就需要訂定一些「協定」讓大家去遵循這些標準
而身為網路相關的工程師,就必須去了解這些協定
]]>API 全名是 Application Programming Interface(應用程式介面)
最重要的就是這個 Interface(介面)
透過這個介面來跟別人溝通
例如:
USB 是一個介面
中間是透過 USB 這個介面來溝通的
當我跟某人要東西時,有時沒辦法直接跟他要,就必須透過他提供的 API 來跟他要東西
假設我是提供資料的人,我不希望別人都可以直接來存取我的資料庫(我不會直接把資料庫權限給別人),所以我需要提供一個 API,讓別人透過這個介面來存取資料
在這個介面上,我可以定義出「哪些東西可以給、哪些東西不能給」
以下舉幾個例子:
作業系統有提供一個 API,來讓我知道網路狀況
我可以寫程式呼叫這個 API,來知道目前的網路狀況
讀取檔案一樣要透過作業系統,都是透過作業系統提供的 API 去讀取檔案(例如 JS 檔案、css 檔案等等)
我必須去串接 Facebook 的 API,才能拿到上面的好友資料
我需要提供 API 給別人,別人就可以透過 API 拿到我網站上的資訊
我提供給別人「新增資料的 API」,別人就可以透過 API 在我網站上新增資料
API 跟 Web API 有什麼不同呢?
API 的提供者可以來自世界各地、四面八方,不一定要透過網路才能提供 API
例如:作業系統不用透過網路,也可以提供給我「讀取檔案、新增檔案」的 API
Web API 顧名思義就是會有「網路」
HTTP API 例如:
裡面有提供一個 Resource URL https://api.twitter.com/1.1/statuses/home_timeline.json ,
只要呼叫這個 api 網址(也可以傳入一些參數)
request 就會長這樣(就是一個 HTTP 的 request)
GET https://api.twitter.com/1.1/statuses/home_timeline.json
SDK (software development kit)
可以把 SDK 想成是一個 library,裡面幫我做好了很多個 API
以上面 twitter 的例子來說,因為沒有 SDK,所以我需要自己呼叫 https://api.twitter.com/1.1/statuses/home_timeline.json 這個 api 網址
但如果 twitter 有提供 SDK 的話,我就只需要呼叫 twitter.getTimeline()
這個 function 就可以了(因為 SDK 已經幫我包裝好了)
我只要發送 request 到 https://picsum.photos/200/300 這個網址,得到的 response 就會是一張圖片
這裡要使用的 API 是 Reqres
它有提供一系列讓我測試用的 API
用之前講過的 request 套件 來改:
request 套件只能在 Node.js 上面執行,無法在瀏覽器執行
底下這個預設的模板,沒有地方讓我寫 HTTP method,但是預設就會使用 GET
因此,這段的意思就是:
呼叫這個 request()
函式,就會幫我發送一個 GET request 到 https://reqres.in/api/users 這個網址去,然後把 response Body 印出來
request()
函式裡面的是一個 callback function,我用這個 callback function 來看到回傳的內容
const request = require('request');
request(
'https://reqres.in/api/users',
function (error, response, body) {
console.log(body);
}
);
在 request()
函式裡面,描述「這個 request 所帶的 Header」會帶有這些資訊:
request 這個套件就會幫我發送「帶有這些資訊的 request」出去
method: GET
URL: https://reqres.in/api/users
執行 node index.js
,就可以看到 https://reqres.in/api/users 這個 API 所返回的資料(response Body)
根據這個 API 的格式,只要在 API 網址後面加上 /2
就可以拿到「ID = 2」這個 user 的資料
const request = require('request');
request(
'https://reqres.in/api/users/2',
function (error, response, body) {
console.log(body);
}
);
process.argv
現在,我要做的功能是:
使用的 API 網址是 https://reqres.in/api/users ,但是當我輸入指令 node index.js 6
就可以拿到「ID = 6」這個 user 的資料
作法如下:
Node.js 有提供一個內建的 module 叫做 process
先把 process 引入進來
接著印出 process.argv
argv
的全名是 argument variables
,也就是「參數」const request = require('request');
const process = require('process');
console.log(process.argv)
request(
'https://reqres.in/api/users',
function (error, response, body) {
console.log(body);
}
);
執行 node index.js 6
,可以看到 console.log(process.argv)
印出的結果是一個 array:
'/usr/local/bin/node'
就是剛剛輸入的指令 node index.js 6
裡面的 node
(指令的第一個參數)'/Users/saffran/Desktop/test/index.js'
就是剛剛輸入的指令 node index.js 6
裡面的 index.js
(指令的第二個參數)'6'
就是剛剛輸入的指令 node index.js 6
裡面的 6
(指令的第三個參數),也就是「我要的 ID」[ '/usr/local/bin/node', '/Users/saffran/Desktop/test/index.js', '6' ]
因此,程式碼就可以改成這樣:
process.argv[2]
來取得 array 的第三個元素(指令的第三個參數)const request = require('request');
const process = require('process');
request(
'https://reqres.in/api/users/' + process.argv[2],
function (error, response, body) {
console.log(body);
}
);
這時,當我執行 node index.js 6
時,就可以拿到「ID = 6」這個 user 的資料了
/
,因為 process.argv[2]
只會取得 6
,而不是 /6
參考文件 request-Forms
const request = require('request');
const process = require('process');
request.post(
{
url: 'https://reqres.in/api/users',
form: {
name: 'Harry',
job: 'wizard'
}
},
function (error, response, body) {
console.log(body);
}
);
執行 node index.js
就可以看到我剛剛新增的資料了
const request = require('request');
request.delete(
'https://reqres.in/api/users/2',
function (error, response, body) {
const json = JSON.parse(body); // 小括號裡面一定要是一個「JSON 格式的字串」才行
console.log(json);
}
);
執行 node index.js
,卻出現了錯誤:Unexpected end of JSON input
這是為什麼呢?
const request = require('request');
request.delete(
'https://reqres.in/api/users/2',
function (error, response, body) {
console.log('body: ', body) // 先把 body 印出來看看
const json = JSON.parse(body); // 小括號裡面一定要是一個「JSON 格式的字串」才行
console.log(json);
}
);
執行 node index.js
,會看到:body 印出來是空的,因為 body 並不是一個 JSON 格式的字串,因此下一行的 JSON.parse(body)
就會出現錯誤「Unexpected end of JSON input」
const request = require('request');
request.delete(
'https://reqres.in/api/users/2',
function (error, response, body) {
console.log(response.statusCode)
}
);
執行 node index.js
,會看到:204,代表「成功刪除,但 body 沒有內容」,這是很正常的,因為我是使用 DELETE,刪除完之後也沒什麼資料需要回給我
印出 status code 和 body 看看
const request = require('request');
request.patch(
{
url: 'https://reqres.in/api/users/2',
form: {
name: 'Romeo'
}
},
function (error, response, body) {
console.log(response.statusCode)
console.log(body)
}
);
output:
回傳 200 代表修改成功了
200
{"name":"Romeo","updatedAt":"2020-07-24T10:02:26.286Z"}
request 套件也可以客製化 HTTP Headers
範例程式碼:
這裡所帶的 'User-Agent': 'request'
只是為了要示範「可以客製化 HTTP Headers」這件事,因此要帶什麼 Headers 可以自己決定
在 headers
這個 key 裡面,對應到的 value 就是我想要帶的 Headers
const request = require('request');
const options = {
url: 'https://api.github.com/repos/request/request',
headers: {
'User-Agent': 'request'
}
};
function callback(error, response, body) {
if (!error && response.statusCode == 200) {
const info = JSON.parse(body);
console.log(info.stargazers_count + " Stars");
console.log(info.forks_count + " Forks");
}
}
request(options, callback);
上面的程式碼,如果把 options
和 callback
都直接放到 request()
函式裡面,就會是這樣:
const request = require('request');
request({
url: 'https://api.github.com/repos/request/request',
headers: {
'User-Agent': 'request'
}
},
function (error, response, body) {
if (!error && response.statusCode == 200) {
const info = JSON.parse(body);
console.log(info.stargazers_count + " Stars");
console.log(info.forks_count + " Forks");
}
});
而 User-Agent
(user 的代理人)通常指的就是「瀏覽器」,也就是「幫我送 request 的是哪個程式」,因為瀏覽器會代理 user 去發送 request
(User-Agent
是比較文言文的講法)
例如:
開啟這個網址 https://lidemy-book-store.herokuapp.com/books?_limit=5 打開 devtool 的 Network tab,在 Request Headers 可以看到 User-Agent 會寫一些跟瀏覽器相關的資訊:
Chrome/84.0.4147.89
就是我現在的 Chrome 版本User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36
關於資料結構,在傳送、接收資料時,資料都會有一定的格式
兩種最常用的資料格式,就是:
XML 的全名是 Extensible Markup Language,跟 HTML 一樣都是一種標記語言
用標籤的形式來表示資料
JSON 的全名是 JavaScript Object Notation,是一種資料格式
JSON 會如此熱門的原因:
JSON.parse()
把「JSON 格式的字串」轉成物件這裡再用前面講到的 Reqres API 為例
const request = require('request');
request(
'https://reqres.in/api/users/2',
function (error, response, body) {
console.log(body);
}
);
執行 node index.js
會印出:
{"data":{"id":2,"email":"janet.weaver@reqres.in","first_name":"Janet","last_name":"Weaver","avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/josephstein/128.jpg"},"ad":{"company":"StatusCode Weekly","url":"http://statuscode.org/","text":"A weekly newsletter focusing on software development, infrastructure, the server, performance, and the stack end of things."}}
console.log(body)
看起來是一個「JavaScript 的物件」,但其實 印出的是一個「字串」(一個 JSON 格式的字串)
因此,要先把字串轉成物件,才能用「物件的方式」存取到裡面的值:
可以用一個 JS 的函式叫做 JSON.parse()
,來把「JSON 格式的字串」轉成「JavaScript 的物件」
JSON.parse()
的小括號裡面一定要是一個「JSON 格式的字串」才行const request = require('request');
request(
'https://reqres.in/api/users/2',
function (error, response, body) {
const json = JSON.parse(body); // 小括號裡面一定要是一個「JSON 格式的字串」才行
console.log(json);
}
);
這時,印出的 console.log(json)
就會是「JavaScript 的物件」了
{
data: {
id: 2,
email: 'janet.weaver@reqres.in',
first_name: 'Janet',
last_name: 'Weaver',
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/josephstein/128.jpg'
},
ad: {
company: 'StatusCode Weekly',
url: 'http://statuscode.org/',
text: 'A weekly newsletter focusing on software development, infrastructure, the server, performance, and the stack end of things.'
}
}
因此,我就可以使用 console.log(json.data.first_name)
印出 Janet
了!
JSON.stringify()
把物件轉成「JSON 格式的字串」const obj = {
name: 'Harry',
job: 'wizard'
}
console.log(obj)
這時,印出的 obj
就是一個「JavaScript 的物件」:
{ name: 'Harry', job: 'wizard' }
JSON.stringify()
const obj = {
name: 'Harry',
job: 'wizard'
}
console.log(JSON.stringify(obj))
這時,印出的 JSON.stringify(obj)
就是一個「JSON 格式的字串」:
{"name":"Harry","job":"wizard"}
]]>API 全名是 Application Programming Interface(應用程式介面)
最重要的就是這個 Interface(介面)
透過這個介面來跟別人溝通
例如:
USB 是一個介面
中間是透過 USB 這個介面來溝通的
當我跟某人要東西時,有時沒辦法直接跟他要,就必須透過他提供的 API 來跟他要東西
假設我是提供資料的人,我不希望別人都可以直接來存取我的資料庫(我不會直接把資料庫權限給別人),所以我需要提供一個 API,讓別人透過這個介面來存取資料
在這個介面上,我可以定義出「哪些東西可以給、哪些東西不能給」
以下舉幾個例子:
作業系統有提供一個 API,來讓我知道網路狀況
我可以寫程式呼叫這個 API,來知道目前的網路狀況
讀取檔案一樣要透過作業系統,都是透過作業系統提供的 API 去讀取檔案(例如 JS 檔案、css 檔案等等)
我必須去串接 Facebook 的 API,才能拿到上面的好友資料
我需要提供 API 給別人,別人就可以透過 API 拿到我網站上的資訊
我提供給別人「新增資料的 API」,別人就可以透過 API 在我網站上新增資料
API 跟 Web API 有什麼不同呢?
API 的提供者可以來自世界各地、四面八方,不一定要透過網路才能提供 API
例如:作業系統不用透過網路,也可以提供給我「讀取檔案、新增檔案」的 API
Web API 顧名思義就是會有「網路」
HTTP API 例如:
裡面有提供一個 Resource URL https://api.twitter.com/1.1/statuses/home_timeline.json ,
只要呼叫這個 api 網址(也可以傳入一些參數)
request 就會長這樣(就是一個 HTTP 的 request)
GET https://api.twitter.com/1.1/statuses/home_timeline.json
SDK (software development kit)
可以把 SDK 想成是一個 library,裡面幫我做好了很多個 API
以上面 twitter 的例子來說,因為沒有 SDK,所以我需要自己呼叫 https://api.twitter.com/1.1/statuses/home_timeline.json 這個 api 網址
但如果 twitter 有提供 SDK 的話,我就只需要呼叫 twitter.getTimeline()
這個 function 就可以了(因為 SDK 已經幫我包裝好了)
我只要發送 request 到 https://picsum.photos/200/300 這個網址,得到的 response 就會是一張圖片
這裡要使用的 API 是 Reqres
它有提供一系列讓我測試用的 API
用之前講過的 request 套件 來改:
request 套件只能在 Node.js 上面執行,無法在瀏覽器執行
底下這個預設的模板,沒有地方讓我寫 HTTP method,但是預設就會使用 GET
因此,這段的意思就是:
呼叫這個 request()
函式,就會幫我發送一個 GET request 到 https://reqres.in/api/users 這個網址去,然後把 response Body 印出來
request()
函式裡面的是一個 callback function,我用這個 callback function 來看到回傳的內容
const request = require('request');
request(
'https://reqres.in/api/users',
function (error, response, body) {
console.log(body);
}
);
在 request()
函式裡面,描述「這個 request 所帶的 Header」會帶有這些資訊:
request 這個套件就會幫我發送「帶有這些資訊的 request」出去
method: GET
URL: https://reqres.in/api/users
執行 node index.js
,就可以看到 https://reqres.in/api/users 這個 API 所返回的資料(response Body)
根據這個 API 的格式,只要在 API 網址後面加上 /2
就可以拿到「ID = 2」這個 user 的資料
const request = require('request');
request(
'https://reqres.in/api/users/2',
function (error, response, body) {
console.log(body);
}
);
process.argv
現在,我要做的功能是:
使用的 API 網址是 https://reqres.in/api/users ,但是當我輸入指令 node index.js 6
就可以拿到「ID = 6」這個 user 的資料
作法如下:
Node.js 有提供一個內建的 module 叫做 process
先把 process 引入進來
接著印出 process.argv
argv
的全名是 argument variables
,也就是「參數」const request = require('request');
const process = require('process');
console.log(process.argv)
request(
'https://reqres.in/api/users',
function (error, response, body) {
console.log(body);
}
);
執行 node index.js 6
,可以看到 console.log(process.argv)
印出的結果是一個 array:
'/usr/local/bin/node'
就是剛剛輸入的指令 node index.js 6
裡面的 node
(指令的第一個參數)'/Users/saffran/Desktop/test/index.js'
就是剛剛輸入的指令 node index.js 6
裡面的 index.js
(指令的第二個參數)'6'
就是剛剛輸入的指令 node index.js 6
裡面的 6
(指令的第三個參數),也就是「我要的 ID」[ '/usr/local/bin/node', '/Users/saffran/Desktop/test/index.js', '6' ]
因此,程式碼就可以改成這樣:
process.argv[2]
來取得 array 的第三個元素(指令的第三個參數)const request = require('request');
const process = require('process');
request(
'https://reqres.in/api/users/' + process.argv[2],
function (error, response, body) {
console.log(body);
}
);
這時,當我執行 node index.js 6
時,就可以拿到「ID = 6」這個 user 的資料了
/
,因為 process.argv[2]
只會取得 6
,而不是 /6
參考文件 request-Forms
const request = require('request');
const process = require('process');
request.post(
{
url: 'https://reqres.in/api/users',
form: {
name: 'Harry',
job: 'wizard'
}
},
function (error, response, body) {
console.log(body);
}
);
執行 node index.js
就可以看到我剛剛新增的資料了
const request = require('request');
request.delete(
'https://reqres.in/api/users/2',
function (error, response, body) {
const json = JSON.parse(body); // 小括號裡面一定要是一個「JSON 格式的字串」才行
console.log(json);
}
);
執行 node index.js
,卻出現了錯誤:Unexpected end of JSON input
這是為什麼呢?
const request = require('request');
request.delete(
'https://reqres.in/api/users/2',
function (error, response, body) {
console.log('body: ', body) // 先把 body 印出來看看
const json = JSON.parse(body); // 小括號裡面一定要是一個「JSON 格式的字串」才行
console.log(json);
}
);
執行 node index.js
,會看到:body 印出來是空的,因為 body 並不是一個 JSON 格式的字串,因此下一行的 JSON.parse(body)
就會出現錯誤「Unexpected end of JSON input」
const request = require('request');
request.delete(
'https://reqres.in/api/users/2',
function (error, response, body) {
console.log(response.statusCode)
}
);
執行 node index.js
,會看到:204,代表「成功刪除,但 body 沒有內容」,這是很正常的,因為我是使用 DELETE,刪除完之後也沒什麼資料需要回給我
印出 status code 和 body 看看
const request = require('request');
request.patch(
{
url: 'https://reqres.in/api/users/2',
form: {
name: 'Romeo'
}
},
function (error, response, body) {
console.log(response.statusCode)
console.log(body)
}
);
output:
回傳 200 代表修改成功了
200
{"name":"Romeo","updatedAt":"2020-07-24T10:02:26.286Z"}
request 套件也可以客製化 HTTP Headers
範例程式碼:
這裡所帶的 'User-Agent': 'request'
只是為了要示範「可以客製化 HTTP Headers」這件事,因此要帶什麼 Headers 可以自己決定
在 headers
這個 key 裡面,對應到的 value 就是我想要帶的 Headers
const request = require('request');
const options = {
url: 'https://api.github.com/repos/request/request',
headers: {
'User-Agent': 'request'
}
};
function callback(error, response, body) {
if (!error && response.statusCode == 200) {
const info = JSON.parse(body);
console.log(info.stargazers_count + " Stars");
console.log(info.forks_count + " Forks");
}
}
request(options, callback);
上面的程式碼,如果把 options
和 callback
都直接放到 request()
函式裡面,就會是這樣:
const request = require('request');
request({
url: 'https://api.github.com/repos/request/request',
headers: {
'User-Agent': 'request'
}
},
function (error, response, body) {
if (!error && response.statusCode == 200) {
const info = JSON.parse(body);
console.log(info.stargazers_count + " Stars");
console.log(info.forks_count + " Forks");
}
});
而 User-Agent
(user 的代理人)通常指的就是「瀏覽器」,也就是「幫我送 request 的是哪個程式」,因為瀏覽器會代理 user 去發送 request
(User-Agent
是比較文言文的講法)
例如:
開啟這個網址 https://lidemy-book-store.herokuapp.com/books?_limit=5 打開 devtool 的 Network tab,在 Request Headers 可以看到 User-Agent 會寫一些跟瀏覽器相關的資訊:
Chrome/84.0.4147.89
就是我現在的 Chrome 版本User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36
關於資料結構,在傳送、接收資料時,資料都會有一定的格式
兩種最常用的資料格式,就是:
XML 的全名是 Extensible Markup Language,跟 HTML 一樣都是一種標記語言
用標籤的形式來表示資料
JSON 的全名是 JavaScript Object Notation,是一種資料格式
JSON 會如此熱門的原因:
JSON.parse()
把「JSON 格式的字串」轉成物件這裡再用前面講到的 Reqres API 為例
const request = require('request');
request(
'https://reqres.in/api/users/2',
function (error, response, body) {
console.log(body);
}
);
執行 node index.js
會印出:
{"data":{"id":2,"email":"janet.weaver@reqres.in","first_name":"Janet","last_name":"Weaver","avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/josephstein/128.jpg"},"ad":{"company":"StatusCode Weekly","url":"http://statuscode.org/","text":"A weekly newsletter focusing on software development, infrastructure, the server, performance, and the stack end of things."}}
console.log(body)
看起來是一個「JavaScript 的物件」,但其實 印出的是一個「字串」(一個 JSON 格式的字串)
因此,要先把字串轉成物件,才能用「物件的方式」存取到裡面的值:
可以用一個 JS 的函式叫做 JSON.parse()
,來把「JSON 格式的字串」轉成「JavaScript 的物件」
JSON.parse()
的小括號裡面一定要是一個「JSON 格式的字串」才行const request = require('request');
request(
'https://reqres.in/api/users/2',
function (error, response, body) {
const json = JSON.parse(body); // 小括號裡面一定要是一個「JSON 格式的字串」才行
console.log(json);
}
);
這時,印出的 console.log(json)
就會是「JavaScript 的物件」了
{
data: {
id: 2,
email: 'janet.weaver@reqres.in',
first_name: 'Janet',
last_name: 'Weaver',
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/josephstein/128.jpg'
},
ad: {
company: 'StatusCode Weekly',
url: 'http://statuscode.org/',
text: 'A weekly newsletter focusing on software development, infrastructure, the server, performance, and the stack end of things.'
}
}
因此,我就可以使用 console.log(json.data.first_name)
印出 Janet
了!
JSON.stringify()
把物件轉成「JSON 格式的字串」const obj = {
name: 'Harry',
job: 'wizard'
}
console.log(obj)
這時,印出的 obj
就是一個「JavaScript 的物件」:
{ name: 'Harry', job: 'wizard' }
JSON.stringify()
const obj = {
name: 'Harry',
job: 'wizard'
}
console.log(JSON.stringify(obj))
這時,印出的 JSON.stringify(obj)
就是一個「JSON 格式的字串」:
{"name":"Harry","job":"wizard"}
]]>
最底層:最後會透過一些實體的東西(例如:我的電腦連接到數據機的網路線、中華電信的海底電纜)把資料傳到 server 去
往上一層:作業系統:如何去包裝我的 request、如何加上目的地的 IP 位置
在網路中,有一個協議叫做「OSI 模型」,又稱為「OSI 七層網路架構」:
有一個組織,把網路標準化,把網路分成七個層級,每一層都負責不同的事情
但是,因為這個 OSI 模型偏理論且太複雜,
因此當我們在實作時,都會去參考另一個模型叫做「TCP/IP」,只會有四層而已
TCP/IP 模型的四層,分別是:
下面是「OSI 模型和 TCP/IP 模型的對照圖」,可以看到:
TCP/IP 模型其實就是 OSI 模型簡化後的版本,兩個模型提出的目的都是要幫網路分層,來解決網路的一些問題
參考資料 鳥哥的 Linux 私房菜
在 TCP/IP 模型中,每一層都有相關的通訊協定
協定是有分層的,好處是:只要處理那一層的問題即可,不用去管其他層在做什麼
HTTP 是建立在 TCP 上面,
TCP 又是建立在 IP 上面 (IP 的全名是「Internet Protocol」)
對應到傳紙條故事:
「訂便當協定、訂飲料協定」都是建立在「三次連接的協定」上面,
「三次連接的協定」又是建立在「傳紙條的協定」上面
IP 的全名是「Internet Protocol」,就是「網路協定」
IP 這個協定有分為兩個:
192.0.2.235
2001:0db8:86a3:08d3:1319:8a2e:0370:7344
IPv4 和 IPv6 最主要的差異就是:
IP 位置的格式不同
打開我的電腦中的網路偏好設定,可以看到:
Wi-Fi 已連接到 192.168.0.16 這個 IP 地址
但是,其他人並無法透過 192.168.0.16 這個 IP 地址連到我的電腦
原因是:這個是在內網裡面的虛擬 IP
IP 有分不同類型,下面會介紹 IP 的不同類型以及用途
這是最理想的情況:
一台電腦就是一個 IP 地址
因此,我只要有某人的 IP 地址,我就可以連到他的電腦,每一台電腦都可以“直接的”互相連線
這樣的 IP 地址叫做「固定 IP」:
例如:
每次連接到網路的時候,我的電腦的 IP 位置都會不一樣
原因一:
一般用戶根本就不需要有固定的 IP 位置,就算有朋友要跟我連線,這次的 IP 地址跟上次不一樣也沒關係,就把現在的 IP 地址貼給他就好
因此,電信商就可以節省資源,不需要每個用戶都特別給他一個固定 IP
只需要給數據機一個固定 IP 即可
原因二:
如果被駭客知道了我的 IP 位置也沒關係,因為下次我再連線時,IP 位置又會不一樣了,因此駭客無法攻擊我
因此,網路會是像下圖這個樣子:
我家有三台電腦,每台電腦都有一個「虛擬 IP」,
通常,虛擬 IP 會是 192.168 開頭,或是 10 開頭
「虛擬 IP」只存在「內網」裡面,只有連線到同一個 wi-fi(在這個內網裡面)的電腦,可以互相連接到彼此的虛擬 IP
從外面(我朋友家)是無法連到這個虛擬 IP 的(找不到這個地址)
例如:
在我家,我連到 wi-fi 時,我的 IP 是 192.168.0.20
在小明家,小明連到 wi-fi 時,小明的 IP 也可以是 192.168.0.20
可以利用一些服務,例如:ExpressVPN,就可以看到這個對外 IP 地址了
當 server 看到我連線時,只會看到我的對外 IP 地址,並不會看到內網的虛擬 IP
也就是說,對 server 來說,家裡的三台電腦都會是同一個 IP 地址
公司會鎖 IP 位置:
為了確保資安,公司會列出一個白名單:只有這些 IP 可以連到我公司的網路
如果公司內每個人對外的 IP 都不同,那就要設定一大堆白名單
因此,公司對外會有一個固定的 IP
但是在公司內網裡面,每個員工都有不同的虛擬 IP
因此,兩個同樣的 IP 並不代表一定是同一台電腦,有可能是兩台電腦在同一個內網底下,所以有同樣的「對外 IP」
以上圖的模型為例,流程如下:
假設,我現在要從我的電腦連到 google.com
從 google.com 的角度來看,只會看到數據機的 IP(20.46.77.58),因此回傳的 response 都只會到 20.46.77.58 這個對外 IP(google.com 完全不知道內網裡面有這些虛擬 IP)
數據機收到 response 後,會知道是我發出的 request,數據機就會把這個 response 再傳給我
對內網裡面的電腦來說,也只看得到數據機,所收到的 response 都是從數據機傳來的
Port 的繁體中文翻譯是「連接埠」,在中國又稱為「端口」
要發送 request,必須先有對方的地址
假設,我現在要發送一個 request 到 12.20.77.60 這個 IP 地址去
可是,一台電腦上有提供各式各樣的服務,例如:
那我要怎麼讓對方知道我需要哪個服務?
: port 代碼
,代表「我要發送 request 到哪一個 port」,一個 port 就會對應到一個程式如果有程式在監聽 80
這個 port 的話,就會收到這個 request
以下三個服務都各自有不同的 port
HTTP 80
HTTP 80
(因為 80
是 HTTP 這個服務在用的 port)HTTPS 443
FTP 21
例如:
https://github.com/Lidemy/mentor-program-4th 這個網址的 IP 地址,在最後面就會加上 :443
,也就是 HTTPS 這個服務所用的 port
因為很多的 port 都已經有其他服務在用了,我們不能拿來測試,
因此,當我們自己在測試時,常用的 port 就是 3000, 4000, 4001, 8000, 8080 這些比較冷門的 port
參考資料 網際網路協議
「TCP 與 UDP」就是在 TCP/IP 模型中的「傳輸層」的兩個協議
TCP (Transmission Control Protocol) ,是採用「三次握手」的確認機制,來確保雙方都能正常收發(有可靠的連線)
大部分在應用層的服務,例如 HTTP, FTP 都是建立在 TCP 上面,就是因為 TCP 可以保證有可靠的連線
UDP 注重的是「即時、快速」,不在乎對方有沒有收到,因為每隔幾秒就會傳回一次 response,就算有幾次沒收到也沒關係,這時就會採用 UDP 這樣的傳輸協定
例如:視訊的服務,偶爾丟了一兩個封包沒關係(因為在畫面上根本感覺不出來,可能只是零點幾毫秒的頓一下),但要求速度要快,不能 lag
TCP 是一個可靠的協定,是因為它在連接時,會有一個「三次握手」的流程,透過「三次握手(Three-way Handshake)」(每一次都會傳送一個封包)來確保連接的可靠性
圖片來源 傳輸控制協定
網路會分成四層,
從上到下就是「送出 request」的過程
從下到上就是「回傳 response」的過程
HTTP/FTP 協定,可以想成是「紙條上的內容」:有可能是訂便當、訂飲料、借籃球等等
在傳輸層,可以選擇「我要怎麼傳輸」
無論選擇哪種傳輸方式,最後都會到 IP 這層
要寫上 IP 地址
透過實體的網路電纜、海底電纜,把 request 傳送到 server 或是從 server 傳回 response
]]>最底層:最後會透過一些實體的東西(例如:我的電腦連接到數據機的網路線、中華電信的海底電纜)把資料傳到 server 去
往上一層:作業系統:如何去包裝我的 request、如何加上目的地的 IP 位置
在網路中,有一個協議叫做「OSI 模型」,又稱為「OSI 七層網路架構」:
有一個組織,把網路標準化,把網路分成七個層級,每一層都負責不同的事情
但是,因為這個 OSI 模型偏理論且太複雜,
因此當我們在實作時,都會去參考另一個模型叫做「TCP/IP」,只會有四層而已
TCP/IP 模型的四層,分別是:
下面是「OSI 模型和 TCP/IP 模型的對照圖」,可以看到:
TCP/IP 模型其實就是 OSI 模型簡化後的版本,兩個模型提出的目的都是要幫網路分層,來解決網路的一些問題
參考資料 鳥哥的 Linux 私房菜
在 TCP/IP 模型中,每一層都有相關的通訊協定
協定是有分層的,好處是:只要處理那一層的問題即可,不用去管其他層在做什麼
HTTP 是建立在 TCP 上面,
TCP 又是建立在 IP 上面 (IP 的全名是「Internet Protocol」)
對應到傳紙條故事:
「訂便當協定、訂飲料協定」都是建立在「三次連接的協定」上面,
「三次連接的協定」又是建立在「傳紙條的協定」上面
IP 的全名是「Internet Protocol」,就是「網路協定」
IP 這個協定有分為兩個:
192.0.2.235
2001:0db8:86a3:08d3:1319:8a2e:0370:7344
IPv4 和 IPv6 最主要的差異就是:
IP 位置的格式不同
打開我的電腦中的網路偏好設定,可以看到:
Wi-Fi 已連接到 192.168.0.16 這個 IP 地址
但是,其他人並無法透過 192.168.0.16 這個 IP 地址連到我的電腦
原因是:這個是在內網裡面的虛擬 IP
IP 有分不同類型,下面會介紹 IP 的不同類型以及用途
這是最理想的情況:
一台電腦就是一個 IP 地址
因此,我只要有某人的 IP 地址,我就可以連到他的電腦,每一台電腦都可以“直接的”互相連線
這樣的 IP 地址叫做「固定 IP」:
例如:
每次連接到網路的時候,我的電腦的 IP 位置都會不一樣
原因一:
一般用戶根本就不需要有固定的 IP 位置,就算有朋友要跟我連線,這次的 IP 地址跟上次不一樣也沒關係,就把現在的 IP 地址貼給他就好
因此,電信商就可以節省資源,不需要每個用戶都特別給他一個固定 IP
只需要給數據機一個固定 IP 即可
原因二:
如果被駭客知道了我的 IP 位置也沒關係,因為下次我再連線時,IP 位置又會不一樣了,因此駭客無法攻擊我
因此,網路會是像下圖這個樣子:
我家有三台電腦,每台電腦都有一個「虛擬 IP」,
通常,虛擬 IP 會是 192.168 開頭,或是 10 開頭
「虛擬 IP」只存在「內網」裡面,只有連線到同一個 wi-fi(在這個內網裡面)的電腦,可以互相連接到彼此的虛擬 IP
從外面(我朋友家)是無法連到這個虛擬 IP 的(找不到這個地址)
例如:
在我家,我連到 wi-fi 時,我的 IP 是 192.168.0.20
在小明家,小明連到 wi-fi 時,小明的 IP 也可以是 192.168.0.20
可以利用一些服務,例如:ExpressVPN,就可以看到這個對外 IP 地址了
當 server 看到我連線時,只會看到我的對外 IP 地址,並不會看到內網的虛擬 IP
也就是說,對 server 來說,家裡的三台電腦都會是同一個 IP 地址
公司會鎖 IP 位置:
為了確保資安,公司會列出一個白名單:只有這些 IP 可以連到我公司的網路
如果公司內每個人對外的 IP 都不同,那就要設定一大堆白名單
因此,公司對外會有一個固定的 IP
但是在公司內網裡面,每個員工都有不同的虛擬 IP
因此,兩個同樣的 IP 並不代表一定是同一台電腦,有可能是兩台電腦在同一個內網底下,所以有同樣的「對外 IP」
以上圖的模型為例,流程如下:
假設,我現在要從我的電腦連到 google.com
從 google.com 的角度來看,只會看到數據機的 IP(20.46.77.58),因此回傳的 response 都只會到 20.46.77.58 這個對外 IP(google.com 完全不知道內網裡面有這些虛擬 IP)
數據機收到 response 後,會知道是我發出的 request,數據機就會把這個 response 再傳給我
對內網裡面的電腦來說,也只看得到數據機,所收到的 response 都是從數據機傳來的
Port 的繁體中文翻譯是「連接埠」,在中國又稱為「端口」
要發送 request,必須先有對方的地址
假設,我現在要發送一個 request 到 12.20.77.60 這個 IP 地址去
可是,一台電腦上有提供各式各樣的服務,例如:
那我要怎麼讓對方知道我需要哪個服務?
: port 代碼
,代表「我要發送 request 到哪一個 port」,一個 port 就會對應到一個程式如果有程式在監聽 80
這個 port 的話,就會收到這個 request
以下三個服務都各自有不同的 port
HTTP 80
HTTP 80
(因為 80
是 HTTP 這個服務在用的 port)HTTPS 443
FTP 21
例如:
https://github.com/Lidemy/mentor-program-4th 這個網址的 IP 地址,在最後面就會加上 :443
,也就是 HTTPS 這個服務所用的 port
因為很多的 port 都已經有其他服務在用了,我們不能拿來測試,
因此,當我們自己在測試時,常用的 port 就是 3000, 4000, 4001, 8000, 8080 這些比較冷門的 port
參考資料 網際網路協議
「TCP 與 UDP」就是在 TCP/IP 模型中的「傳輸層」的兩個協議
TCP (Transmission Control Protocol) ,是採用「三次握手」的確認機制,來確保雙方都能正常收發(有可靠的連線)
大部分在應用層的服務,例如 HTTP, FTP 都是建立在 TCP 上面,就是因為 TCP 可以保證有可靠的連線
UDP 注重的是「即時、快速」,不在乎對方有沒有收到,因為每隔幾秒就會傳回一次 response,就算有幾次沒收到也沒關係,這時就會採用 UDP 這樣的傳輸協定
例如:視訊的服務,偶爾丟了一兩個封包沒關係(因為在畫面上根本感覺不出來,可能只是零點幾毫秒的頓一下),但要求速度要快,不能 lag
TCP 是一個可靠的協定,是因為它在連接時,會有一個「三次握手」的流程,透過「三次握手(Three-way Handshake)」(每一次都會傳送一個封包)來確保連接的可靠性
圖片來源 傳輸控制協定
網路會分成四層,
從上到下就是「送出 request」的過程
從下到上就是「回傳 response」的過程
HTTP/FTP 協定,可以想成是「紙條上的內容」:有可能是訂便當、訂飲料、借籃球等等
在傳輸層,可以選擇「我要怎麼傳輸」
無論選擇哪種傳輸方式,最後都會到 IP 這層
要寫上 IP 地址
透過實體的網路電纜、海底電纜,把 request 傳送到 server 或是從 server 傳回 response
]]>「傳紙條」跟「網路」的本質都是在“溝通”
傳紙條守則:
傳紙條守則:
傳紙條守則:
協定,其實就是一種標準(為了要讓彼此能夠溝通而建立的一個規範)。有了標準,才可以做「規模化」
例如:
HTTP 的全名是 Hypertext Transfer Protocol,就是一個協定
網頁前端在跟後端溝通時,都是透過 HTTP 協定
所以網址最前面都會有 http 或是 https(s 就是 secure,是一種更安全的連線方式)
透過 HTTP 協定溝通的兩端,分別是 Client 和 Server
Client 就是「自己的電腦、瀏覽器」
以這個 GitHub 的頁面 來說,我們為什麼可以看到這個頁面呢?
Network 頁籤這裡會印出「瀏覽器發送的每一個 request」和「收到的 response」
下圖中,在最左側的 Name 欄位,第一個「mentor-program-4th」是「整個網頁」的 request, response:
瀏覽器 --> 製造 request --> 傳給 server
server --> 處理 --> 回傳 response 給瀏覽器
DNS 的全名是 Domain Name System,負責把 domain name 轉換成「實際的 IP 位置」
下圖是 devtool 的 Network 頁籤,Remote Address 就是「經由 DNS 解析之後的 IP 位置」
nslookup github.com
使用指令 nslookup github.com
,可以查詢這個 domain name 的 IP 位置是哪裡:
可能會有多個不同的 IP 位置,對應到不同的 server(分散在世界各處的 server)
在電腦裡面有一個檔案叫做 /hosts,長這樣子:
(檔案位置是在 /private/etc/hosts)
這個 /hosts 檔案的內容是 Host Database,在這個檔案裡面,可以自己新增一些規則來「把一個 domain name 對應到一個 IP 位置」。
電腦在發送 request 時,會優先到這個 /hosts 檔案查詢這些規則來找是否有對應的 IP 位置。如果規則沒有寫,電腦才會去 DNS server 查 IP 位置。
例如:
localhost
對應到的 IP 位置就是 127.0.0.1(在任何一台電腦上,127.0.0.1 就是“自己電腦的 server”的意思,是一個特殊的 IP 位置)
當我的電腦要發送 request 到 localhost
這個 domain name 時,localhost
的 IP 位置是哪裡呢?電腦就會先去 /hosts 檔案裡面查,就會查到「localhost
這個 domain name 的 IP 位置是 127.0.0.1」,因此就會去 127.0.0.1 這個位置
假設我現在在 /hosts 檔案裡面新增了 127.0.0.1 github.com
,當我「用 nslookup github.com
查詢」或者是「在瀏覽器的網址列輸入 github.com」時,電腦就會把 github.com 的 IP 位置解析為 127.0.0.1
通常是在測試一些東西的時候會用到 /hosts 檔案
在安裝 Adobe 的盜版軟體時,會要你去改一個叫做 /hosts 的檔案:
把一個 domain name 例如 adobe.com 對應到 127.0.0.1
為什麼要這麼做呢?
在安裝軟體時,都會有一個「檢查是否為盜版軟體」的機制。
檢查的方式是:發送一個 request 到 adobe server -> adobe server 就會回傳一個 response 說是否為盜版軟體
request -> adobe server -> response
但是,因為我要下載盜版軟體時,不希望它去做這個“是否為盜版軟體”的檢查,因此我可以這麼做:
假設 adobe server 的 domain name 是 adobe.com
那我就在 /hosts 檔案內新增這行 127.0.0.1 adobe.com
或是 127.0.0.2 adobe.com
意思就是,把 adobe.com 的 IP 位置對應到 127.0.0.1 (我的電腦)或是 127.0.0.2 (其他任意一個不存在的 IP 位置)
這樣一來,
發送到 adobe.com 的每一個 request 都會被導到一個根本沒有 adobe 服務的地方(我的電腦 or 一個不存在的 IP 位置),就不會有 response(所以也不會去做“是否為盜版”的檢查),我就不會被視為是盜版軟體了:
request -> ??? -> xxx
永遠不要忘記:瀏覽器只是一個程式,這個程式就是幫我發送 request -> 接收 response -> 再把 response 的 html 程式碼渲染成頁面讓我看到
沒有瀏覽器,要怎麼拿到 response 呢?
在 Node.js 有一套 library 叫做 Request
安裝完 Request 之後,直接從 GitHub 把這段用法貼到 index.js 裡面:
const request = require('request');
request('http://www.google.com', function (error, response, body) {
console.error('error:', error); // Print the error if one occurred
console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
console.log('body:', body); // Print the HTML for the Google homepage.
});
request
小括號裡面,會傳入兩個參數:代表說:我要發送 request 到 https://github.com/Lidemy/mentor-program-4th 這個網址去
function 裡面會做一些事情(印出接收到的資訊):
console.error('error:', error)
就是:如果有出現錯誤,會把錯誤印出來console.log('statusCode:', response && response.statusCode)
就是:如果有成功收到 response,就把 status code 印出來console.log('body:', body)
就是:把 response 的 body 印出來const request = require('request');
request('https://github.com/Lidemy/mentor-program-4th', function (error, response, body) {
console.error('error:', error); // Print the error if one occurred
console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
// console.log('body:', body);
});
這裡,我們先不印出 body,先執行一下 node index.js
,結果印出如下,代表:沒有錯誤
error: null
statusCode: 200
接著,就可以把 body 印出來看看:
const request = require('request');
request(
'https://github.com/Lidemy/mentor-program-4th',
function (error, response, body) {
// console.error('error:', error); // Print the error if one occurred
// console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
console.log('body:', body);
}
);
執行一下 node index.js
,結果印出如下,就是我在 Network 頁籤看到的 response(也就是:從 https://github.com/Lidemy/mentor-program-4th 這個 server 回傳的 response)
node index.js > mtr4th.html
這個指令,把 node index.js
輸出的東西,導到一個新的檔案叫做 mtr4th.html在資料夾裡面,就可以看到 mtr4th.html 這個檔案
接著,用 open mtr4th.html
打開 mtr4th.html,就可以看到渲染完成的網頁了!(跟原本用瀏覽器接收 response 的頁面一模一樣)
request 和 response 都有分成兩個部分:header, body
Header 裡面寫的就是:看 server 有要求什麼資訊,或者是我想要帶什麼資訊給 server
打開 devtool,在 Network 頁籤的 Headers,會把 response 和 request 的 header 都寫在這裡
例如:
Request Method
這個欄位,只有在 Request Headers 會有,在 Response Headers 不會有
例如:content-type
這個 Header 欄位
content-type
是要告訴 server「我要傳送的資料內容是什麼格式」content-type
是要告訴瀏覽器「這個 response 的資料內容是什麼格式」用 request 這個 Node.js 的套件,把 response.headers
印出來看看:
const request = require('request');
request(
'https://github.com/Lidemy/mentor-program-4th',
function (error, response, body) {
console.log(response.headers)
// console.log('body:', body);
}
);
response.headers
就會是這樣:
{
server: 'GitHub.com',
date: 'Sat, 18 Jul 2020 05:54:59 GMT',
'content-type': 'text/html; charset=utf-8',
status: '200 OK',
vary: 'X-PJAX, Accept-Encoding, Accept, X-Requested-With',
...
在 HTTP 的 request 裡面,使用 GET, POST 這些動詞,來決定要執行什麼動作
例如:
當我想看這張杯子的圖片時,就會使用 GET,「GET」背後做的事情是:
瀏覽器發送了一個「GET」的 request 到 https://avatars2.githubusercontent.com/u/2755720?s=64&v=4 這個網址去,所得到的 response 就是這張圖片,瀏覽器再把這張圖片顯示出來讓我看到
以這個 GitHub 的 Sign in 頁面來說,打開 devtool > Network 頁籤,然後隨便輸入帳號密碼按下 Sign in,在 Network 頁籤點擊 session 就可以看到:
瀏覽器發送了一個 POST 的 request 到 https://github.com/session 這個網址去
往下拉,會看到一個 Form Data,這就是這個 POST request 的 Body(我要 POST 出去的東西),會有 key 和相對應的 value
例如:
我剛剛在 Sign in 表單中填寫的帳號(login: aa)、密碼(password: bb)
上圖看到的 Form Data 是 chrome 編碼之後的樣子(用一個比較方便觀看的格式顯示出來)
在 Form Data 點擊 view source,就會看到一堆很像亂碼的東西,這就是 Form Data 實際上在 request 的 Body 裡面的樣子(在 encode 之前的樣子)
參考資料 HTTP request methods
最常用的 HTTP Method 就是 GET, POST, DELETE, PUT
用來取得資料
不會有 response body
在有些情況,我不想知道這個 response 的內容,我只想知道這個 response 的一些資訊,因此我只需要取得 response 的 header 就好
(此 method 會用到的機率很低)
用來提交東西
改變內容
PUT 跟 PATCH 很類似,差別在於:
刪除資源
(很少用)
回傳給我「這個 server 支援哪些方法」
(很少用)
HTTP Status code(狀態碼)
通常,Status code 都是以「不同的開頭」來區分,代表不同的意思
1** 代表:server 已收到請求,但 client 端需要去進行一些處理
例如:
server 需要我去使用另一個不同的 protocol
例如:
server 成功處理了 request,但沒有返回任何內容
例如,我發了一個 request 是「DELETE (刪除資源)」,如果刪除成功,就會回傳 204 的狀態碼(但不會返回任何內容,因為沒有什麼資訊需要返回)
最常見的例如:
此資源已經被永久的移到新的位置(就是轉址的意思)
例如:我用 GET 訪問 a.com 這個資源,如果是回傳 301,
在 response header 會有一個 Location 的欄位是 b.com
下一次當我再發 request 到 a.com 時,不需要再問一次「要去哪裡」,瀏覽器就會直接幫我轉址到 b.com
因為從第一次的 request 中,瀏覽器已經知道:a.com 被永久的移到 b.com 了(瀏覽器把這個關係給存起來了)
// 第一次 request
GET a.com
status code: 301
Location: b.com
// 第二次 request
GET a.com => b.com(瀏覽器會直接去 b.com)
此資源暫時的被移到新的位置
例如:我用 GET 訪問 a.com 這個資源,如果是回傳 302,
代表:a.com 只是暫時的被移到 b.com
下一次當我再發 request 到 a.com 時,還是會先去 a.com 問「要去哪裡」,最後再到 b.com
// 第一次 request
GET a.com
status code: 302
Location: b.com
// 第二次 request
GET a.com
status code: 302
Location: b.com
例如:
例如:
1 Hold on
2 Here you go
3 Go away
4 You fucked up
5** I fucked up
在 Node.js 裡面,有一個內建的 library 叫做 http
作法如下:
注意!每次修改程式碼,就要把 server 關掉再重跑一次,不然不會更新
然後就可以宣告一個變數 server
,並使用 http.createServer()
建立一個 server
var http = require('http');
var server = http.createServer()
作法就是:傳入一個 function 叫做 handleRequest
在 handleRequest
function 裡面:
res.write('I love you')
代表:要回傳的 response 內容是 'I love you'res.end()
代表:讓 client 端知道「這個 response 結束了」最後,要用 server.listen(5000)
給它一個 port 名稱(也就是:服務代碼),server 就會去監聽 5000
這個 port
var http = require('http');
var server = http.createServer(handleRequest);
function handleRequest(req, res){
console.log(req.url);
res.write('I love you');
res.end();
}
server.listen(5000) // port
這時,輸入指令 node index.js
會發現:
不會出現任何東西,這很正常,因為我沒有 log 任何東西(按下 ctrl + C 可以結束 server)
但是,(要在 node index.js
執行這個 server 時)當我在瀏覽器網址列輸入 localhost:5000 按下 Enter,就看到我剛剛設定的 response 內容了
(5000 就是我剛剛指定的 port,要輸入 5000 才能連接到這個服務)
這時,再回到 CLI 去看 console.log(req.url)
,會把 request 的 url 都印出來:
/
/favicon.ico
/
就是「根目錄」/favicon.ico
就是「網頁左上角的小 icon」,瀏覽器預設都會發送一個 request 問說「有沒有這個 icon 可以拿」,有的話就拿回來並顯示出來(此範例中,並沒有拿到 favicon)打開 devtool,可以看到:
用 GET 發送 request 到 http://localhost:5000/ 這個網址去
response 的內容就是 I love you
這裡的 request URL 就會是 http://localhost:5000/hey
然後,console.log(req.url)
就會印出 /hey
這個 URL:
/hey
/favicon.ico
req.url
做不同的事情我設定兩個不同的 url 會回傳不同的 response:
var http = require('http');
var server = http.createServer(handleRequest);
function handleRequest(req, res){
if (req.url === '/') {
res.write('welcome!');
res.end();
return
}
if (req.url === '/about') {
res.write('Hi, I am Harry.');
res.end();
return
}
}
server.listen(5000) // port
server 端就會根據不同的網址,回傳不同的 response
如果想要「指定 Header」,可以用一個 function 叫做 res.writeHead()
,裡面傳入兩個參數:
'myBlog': 'good'
不能寫成 'my Blog': 'good'
var http = require('http');
var server = http.createServer(handleRequest);
function handleRequest(req, res){
if (req.url === '/') {
res.write('welcome!');
res.end();
return
}
if (req.url === '/about') {
res.write('Hi, I am Harry.');
res.end();
return
}
// 指定 Header 的內容
if (req.url === '/page3') {
res.writeHead(200, {
'myBlog': 'good'
})
res.end();
return
}
res.writeHead(404);
res.end();
}
server.listen(5000) // port
在最後面加上 res.writeHead(404)
,因此如果輸入了一個亂打的網址 http://localhost:5000/kkk ,頁面就會顯示「This localhost page can’t be found」,並且在 devtool 顯示 404
如果輸入網址 http://localhost:5000/page3 ,點進 page3 的 Response Headers,會看到「myBlog: good」,就是我剛剛指定的 Header
如果想要「重新導向」,狀態碼就是 301 或 302
這裡,可以用一個 function 叫做 res.writeHead()
,裡面傳入兩個參數:
Location
的 Header,後面的 value 就是「要重新導向到哪一個網址 /about
」var http = require('http');
var server = http.createServer(handleRequest);
function handleRequest(req, res){
if (req.url === '/') {
res.write('welcome!');
res.end();
return
}
if (req.url === '/about') {
res.write('Hi, I am Harry.');
res.end();
return
}
// 重新導向
if (req.url === '/redirect') {
res.writeHead(302, {
'Location': '/about'
})
res.end();
return
}
res.writeHead(404);
res.end();
}
server.listen(5000) // port
這時,輸入網址 http://localhost:5000/redirect ,瀏覽器就會幫我轉址到 http://localhost:5000/about 去了。
打開 devtool,可以看到:
當然,也可以轉址到其他網址去,例如:'Location': 'https://google.com'
var http = require('http');
var server = http.createServer(handleRequest);
function handleRequest(req, res){
if (req.url === '/') {
res.write('welcome!');
res.end();
return
}
if (req.url === '/about') {
res.write('Hi, I am Harry.');
res.end();
return
}
// 重新導向
if (req.url === '/redirect') {
res.writeHead(302, {
'Location': 'https://google.com'
})
res.end();
return
}
res.writeHead(404);
res.end();
}
server.listen(5000) // port
這時,輸入網址 http://localhost:5000/redirect ,瀏覽器就會幫我轉址到 https://www.google.com/ 去了。
點進去 google.com 會發現 https://google.com/ 也有一個 301 轉址,轉到 https://www.google.com/
「傳紙條」跟「網路」的本質都是在“溝通”
傳紙條守則:
傳紙條守則:
傳紙條守則:
協定,其實就是一種標準(為了要讓彼此能夠溝通而建立的一個規範)。有了標準,才可以做「規模化」
例如:
HTTP 的全名是 Hypertext Transfer Protocol,就是一個協定
網頁前端在跟後端溝通時,都是透過 HTTP 協定
所以網址最前面都會有 http 或是 https(s 就是 secure,是一種更安全的連線方式)
透過 HTTP 協定溝通的兩端,分別是 Client 和 Server
Client 就是「自己的電腦、瀏覽器」
以這個 GitHub 的頁面 來說,我們為什麼可以看到這個頁面呢?
Network 頁籤這裡會印出「瀏覽器發送的每一個 request」和「收到的 response」
下圖中,在最左側的 Name 欄位,第一個「mentor-program-4th」是「整個網頁」的 request, response:
瀏覽器 --> 製造 request --> 傳給 server
server --> 處理 --> 回傳 response 給瀏覽器
DNS 的全名是 Domain Name System,負責把 domain name 轉換成「實際的 IP 位置」
下圖是 devtool 的 Network 頁籤,Remote Address 就是「經由 DNS 解析之後的 IP 位置」
nslookup github.com
使用指令 nslookup github.com
,可以查詢這個 domain name 的 IP 位置是哪裡:
可能會有多個不同的 IP 位置,對應到不同的 server(分散在世界各處的 server)
在電腦裡面有一個檔案叫做 /hosts,長這樣子:
(檔案位置是在 /private/etc/hosts)
這個 /hosts 檔案的內容是 Host Database,在這個檔案裡面,可以自己新增一些規則來「把一個 domain name 對應到一個 IP 位置」。
電腦在發送 request 時,會優先到這個 /hosts 檔案查詢這些規則來找是否有對應的 IP 位置。如果規則沒有寫,電腦才會去 DNS server 查 IP 位置。
例如:
localhost
對應到的 IP 位置就是 127.0.0.1(在任何一台電腦上,127.0.0.1 就是“自己電腦的 server”的意思,是一個特殊的 IP 位置)
當我的電腦要發送 request 到 localhost
這個 domain name 時,localhost
的 IP 位置是哪裡呢?電腦就會先去 /hosts 檔案裡面查,就會查到「localhost
這個 domain name 的 IP 位置是 127.0.0.1」,因此就會去 127.0.0.1 這個位置
假設我現在在 /hosts 檔案裡面新增了 127.0.0.1 github.com
,當我「用 nslookup github.com
查詢」或者是「在瀏覽器的網址列輸入 github.com」時,電腦就會把 github.com 的 IP 位置解析為 127.0.0.1
通常是在測試一些東西的時候會用到 /hosts 檔案
在安裝 Adobe 的盜版軟體時,會要你去改一個叫做 /hosts 的檔案:
把一個 domain name 例如 adobe.com 對應到 127.0.0.1
為什麼要這麼做呢?
在安裝軟體時,都會有一個「檢查是否為盜版軟體」的機制。
檢查的方式是:發送一個 request 到 adobe server -> adobe server 就會回傳一個 response 說是否為盜版軟體
request -> adobe server -> response
但是,因為我要下載盜版軟體時,不希望它去做這個“是否為盜版軟體”的檢查,因此我可以這麼做:
假設 adobe server 的 domain name 是 adobe.com
那我就在 /hosts 檔案內新增這行 127.0.0.1 adobe.com
或是 127.0.0.2 adobe.com
意思就是,把 adobe.com 的 IP 位置對應到 127.0.0.1 (我的電腦)或是 127.0.0.2 (其他任意一個不存在的 IP 位置)
這樣一來,
發送到 adobe.com 的每一個 request 都會被導到一個根本沒有 adobe 服務的地方(我的電腦 or 一個不存在的 IP 位置),就不會有 response(所以也不會去做“是否為盜版”的檢查),我就不會被視為是盜版軟體了:
request -> ??? -> xxx
永遠不要忘記:瀏覽器只是一個程式,這個程式就是幫我發送 request -> 接收 response -> 再把 response 的 html 程式碼渲染成頁面讓我看到
沒有瀏覽器,要怎麼拿到 response 呢?
在 Node.js 有一套 library 叫做 Request
安裝完 Request 之後,直接從 GitHub 把這段用法貼到 index.js 裡面:
const request = require('request');
request('http://www.google.com', function (error, response, body) {
console.error('error:', error); // Print the error if one occurred
console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
console.log('body:', body); // Print the HTML for the Google homepage.
});
request
小括號裡面,會傳入兩個參數:代表說:我要發送 request 到 https://github.com/Lidemy/mentor-program-4th 這個網址去
function 裡面會做一些事情(印出接收到的資訊):
console.error('error:', error)
就是:如果有出現錯誤,會把錯誤印出來console.log('statusCode:', response && response.statusCode)
就是:如果有成功收到 response,就把 status code 印出來console.log('body:', body)
就是:把 response 的 body 印出來const request = require('request');
request('https://github.com/Lidemy/mentor-program-4th', function (error, response, body) {
console.error('error:', error); // Print the error if one occurred
console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
// console.log('body:', body);
});
這裡,我們先不印出 body,先執行一下 node index.js
,結果印出如下,代表:沒有錯誤
error: null
statusCode: 200
接著,就可以把 body 印出來看看:
const request = require('request');
request(
'https://github.com/Lidemy/mentor-program-4th',
function (error, response, body) {
// console.error('error:', error); // Print the error if one occurred
// console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
console.log('body:', body);
}
);
執行一下 node index.js
,結果印出如下,就是我在 Network 頁籤看到的 response(也就是:從 https://github.com/Lidemy/mentor-program-4th 這個 server 回傳的 response)
node index.js > mtr4th.html
這個指令,把 node index.js
輸出的東西,導到一個新的檔案叫做 mtr4th.html在資料夾裡面,就可以看到 mtr4th.html 這個檔案
接著,用 open mtr4th.html
打開 mtr4th.html,就可以看到渲染完成的網頁了!(跟原本用瀏覽器接收 response 的頁面一模一樣)
request 和 response 都有分成兩個部分:header, body
Header 裡面寫的就是:看 server 有要求什麼資訊,或者是我想要帶什麼資訊給 server
打開 devtool,在 Network 頁籤的 Headers,會把 response 和 request 的 header 都寫在這裡
例如:
Request Method
這個欄位,只有在 Request Headers 會有,在 Response Headers 不會有
例如:content-type
這個 Header 欄位
content-type
是要告訴 server「我要傳送的資料內容是什麼格式」content-type
是要告訴瀏覽器「這個 response 的資料內容是什麼格式」用 request 這個 Node.js 的套件,把 response.headers
印出來看看:
const request = require('request');
request(
'https://github.com/Lidemy/mentor-program-4th',
function (error, response, body) {
console.log(response.headers)
// console.log('body:', body);
}
);
response.headers
就會是這樣:
{
server: 'GitHub.com',
date: 'Sat, 18 Jul 2020 05:54:59 GMT',
'content-type': 'text/html; charset=utf-8',
status: '200 OK',
vary: 'X-PJAX, Accept-Encoding, Accept, X-Requested-With',
...
在 HTTP 的 request 裡面,使用 GET, POST 這些動詞,來決定要執行什麼動作
例如:
當我想看這張杯子的圖片時,就會使用 GET,「GET」背後做的事情是:
瀏覽器發送了一個「GET」的 request 到 https://avatars2.githubusercontent.com/u/2755720?s=64&v=4 這個網址去,所得到的 response 就是這張圖片,瀏覽器再把這張圖片顯示出來讓我看到
以這個 GitHub 的 Sign in 頁面來說,打開 devtool > Network 頁籤,然後隨便輸入帳號密碼按下 Sign in,在 Network 頁籤點擊 session 就可以看到:
瀏覽器發送了一個 POST 的 request 到 https://github.com/session 這個網址去
往下拉,會看到一個 Form Data,這就是這個 POST request 的 Body(我要 POST 出去的東西),會有 key 和相對應的 value
例如:
我剛剛在 Sign in 表單中填寫的帳號(login: aa)、密碼(password: bb)
上圖看到的 Form Data 是 chrome 編碼之後的樣子(用一個比較方便觀看的格式顯示出來)
在 Form Data 點擊 view source,就會看到一堆很像亂碼的東西,這就是 Form Data 實際上在 request 的 Body 裡面的樣子(在 encode 之前的樣子)
參考資料 HTTP request methods
最常用的 HTTP Method 就是 GET, POST, DELETE, PUT
用來取得資料
不會有 response body
在有些情況,我不想知道這個 response 的內容,我只想知道這個 response 的一些資訊,因此我只需要取得 response 的 header 就好
(此 method 會用到的機率很低)
用來提交東西
改變內容
PUT 跟 PATCH 很類似,差別在於:
刪除資源
(很少用)
回傳給我「這個 server 支援哪些方法」
(很少用)
HTTP Status code(狀態碼)
通常,Status code 都是以「不同的開頭」來區分,代表不同的意思
1** 代表:server 已收到請求,但 client 端需要去進行一些處理
例如:
server 需要我去使用另一個不同的 protocol
例如:
server 成功處理了 request,但沒有返回任何內容
例如,我發了一個 request 是「DELETE (刪除資源)」,如果刪除成功,就會回傳 204 的狀態碼(但不會返回任何內容,因為沒有什麼資訊需要返回)
最常見的例如:
此資源已經被永久的移到新的位置(就是轉址的意思)
例如:我用 GET 訪問 a.com 這個資源,如果是回傳 301,
在 response header 會有一個 Location 的欄位是 b.com
下一次當我再發 request 到 a.com 時,不需要再問一次「要去哪裡」,瀏覽器就會直接幫我轉址到 b.com
因為從第一次的 request 中,瀏覽器已經知道:a.com 被永久的移到 b.com 了(瀏覽器把這個關係給存起來了)
// 第一次 request
GET a.com
status code: 301
Location: b.com
// 第二次 request
GET a.com => b.com(瀏覽器會直接去 b.com)
此資源暫時的被移到新的位置
例如:我用 GET 訪問 a.com 這個資源,如果是回傳 302,
代表:a.com 只是暫時的被移到 b.com
下一次當我再發 request 到 a.com 時,還是會先去 a.com 問「要去哪裡」,最後再到 b.com
// 第一次 request
GET a.com
status code: 302
Location: b.com
// 第二次 request
GET a.com
status code: 302
Location: b.com
例如:
例如:
1 Hold on
2 Here you go
3 Go away
4 You fucked up
5** I fucked up
在 Node.js 裡面,有一個內建的 library 叫做 http
作法如下:
注意!每次修改程式碼,就要把 server 關掉再重跑一次,不然不會更新
然後就可以宣告一個變數 server
,並使用 http.createServer()
建立一個 server
var http = require('http');
var server = http.createServer()
作法就是:傳入一個 function 叫做 handleRequest
在 handleRequest
function 裡面:
res.write('I love you')
代表:要回傳的 response 內容是 'I love you'res.end()
代表:讓 client 端知道「這個 response 結束了」最後,要用 server.listen(5000)
給它一個 port 名稱(也就是:服務代碼),server 就會去監聽 5000
這個 port
var http = require('http');
var server = http.createServer(handleRequest);
function handleRequest(req, res){
console.log(req.url);
res.write('I love you');
res.end();
}
server.listen(5000) // port
這時,輸入指令 node index.js
會發現:
不會出現任何東西,這很正常,因為我沒有 log 任何東西(按下 ctrl + C 可以結束 server)
但是,(要在 node index.js
執行這個 server 時)當我在瀏覽器網址列輸入 localhost:5000 按下 Enter,就看到我剛剛設定的 response 內容了
(5000 就是我剛剛指定的 port,要輸入 5000 才能連接到這個服務)
這時,再回到 CLI 去看 console.log(req.url)
,會把 request 的 url 都印出來:
/
/favicon.ico
/
就是「根目錄」/favicon.ico
就是「網頁左上角的小 icon」,瀏覽器預設都會發送一個 request 問說「有沒有這個 icon 可以拿」,有的話就拿回來並顯示出來(此範例中,並沒有拿到 favicon)打開 devtool,可以看到:
用 GET 發送 request 到 http://localhost:5000/ 這個網址去
response 的內容就是 I love you
這裡的 request URL 就會是 http://localhost:5000/hey
然後,console.log(req.url)
就會印出 /hey
這個 URL:
/hey
/favicon.ico
req.url
做不同的事情我設定兩個不同的 url 會回傳不同的 response:
var http = require('http');
var server = http.createServer(handleRequest);
function handleRequest(req, res){
if (req.url === '/') {
res.write('welcome!');
res.end();
return
}
if (req.url === '/about') {
res.write('Hi, I am Harry.');
res.end();
return
}
}
server.listen(5000) // port
server 端就會根據不同的網址,回傳不同的 response
如果想要「指定 Header」,可以用一個 function 叫做 res.writeHead()
,裡面傳入兩個參數:
'myBlog': 'good'
不能寫成 'my Blog': 'good'
var http = require('http');
var server = http.createServer(handleRequest);
function handleRequest(req, res){
if (req.url === '/') {
res.write('welcome!');
res.end();
return
}
if (req.url === '/about') {
res.write('Hi, I am Harry.');
res.end();
return
}
// 指定 Header 的內容
if (req.url === '/page3') {
res.writeHead(200, {
'myBlog': 'good'
})
res.end();
return
}
res.writeHead(404);
res.end();
}
server.listen(5000) // port
在最後面加上 res.writeHead(404)
,因此如果輸入了一個亂打的網址 http://localhost:5000/kkk ,頁面就會顯示「This localhost page can’t be found」,並且在 devtool 顯示 404
如果輸入網址 http://localhost:5000/page3 ,點進 page3 的 Response Headers,會看到「myBlog: good」,就是我剛剛指定的 Header
如果想要「重新導向」,狀態碼就是 301 或 302
這裡,可以用一個 function 叫做 res.writeHead()
,裡面傳入兩個參數:
Location
的 Header,後面的 value 就是「要重新導向到哪一個網址 /about
」var http = require('http');
var server = http.createServer(handleRequest);
function handleRequest(req, res){
if (req.url === '/') {
res.write('welcome!');
res.end();
return
}
if (req.url === '/about') {
res.write('Hi, I am Harry.');
res.end();
return
}
// 重新導向
if (req.url === '/redirect') {
res.writeHead(302, {
'Location': '/about'
})
res.end();
return
}
res.writeHead(404);
res.end();
}
server.listen(5000) // port
這時,輸入網址 http://localhost:5000/redirect ,瀏覽器就會幫我轉址到 http://localhost:5000/about 去了。
打開 devtool,可以看到:
當然,也可以轉址到其他網址去,例如:'Location': 'https://google.com'
var http = require('http');
var server = http.createServer(handleRequest);
function handleRequest(req, res){
if (req.url === '/') {
res.write('welcome!');
res.end();
return
}
if (req.url === '/about') {
res.write('Hi, I am Harry.');
res.end();
return
}
// 重新導向
if (req.url === '/redirect') {
res.writeHead(302, {
'Location': 'https://google.com'
})
res.end();
return
}
res.writeHead(404);
res.end();
}
server.listen(5000) // port
這時,輸入網址 http://localhost:5000/redirect ,瀏覽器就會幫我轉址到 https://www.google.com/ 去了。
點進去 google.com 會發現 https://google.com/ 也有一個 301 轉址,轉到 https://www.google.com/
default parameters 就是「預設值」的意思
這裡是一個 repeat
函式
function repeat(str, times){
return str.repeat(times)
}
console.log(repeat('love', 5))
// output: lovelovelovelovelove
在呼叫 repeat
函式時,如果參數 times
沒有寫,那麼參數 times
就會是 undefined,然後印出 repeat('love')
的結果也會是什麼都沒有
function repeat(str, times){
console.log(times) // 把 times 印出來看看
return str.repeat(times)
}
console.log(repeat('love'))
output:
undefined
在 repeat
函式的參數 times
後方設定 = 5
的預設值,這樣,在呼叫 repeat
函式時如果沒有傳入 times
,times
就會是預設值(5)
function repeat(str, times = 5){
console.log(times) // 把 times 印出來看看
return str.repeat(times)
}
console.log(repeat('love'))
output:
5
lovelovelovelovelove
也可以把兩個參數 str
和 times
都設定預設值
這樣,在呼叫 repeat
函式時,如果完全沒有傳入參數,就會都使用預設值
function repeat(str = 'hey', times = 3){
return str.repeat(times)
}
console.log(repeat())
// output: heyheyhey
「預設值」也可以用在陣列、物件的解構
const obj = {
a: 8,
b: 9
}
const {a, b} = obj
console.log(a, b)
// output: 8 9
如果在宣告變數 obj
時,就沒有 a,那麼解構之後印出變數 a
就會是 undefined
const obj = {
b: 9
}
const {a, b} = obj
console.log(a, b)
// output: undefined 9
在解構時,可以幫變數 a
設定預設值 = 333
在宣告變數 obj
時,如果沒有 a,那麼印出來的變數 a
就會是 333
const obj = {
b: 9
}
const {a = 333, b} = obj // 幫變數 a 設定預設值
console.log(a, b)
// output: 333 9
但在宣告變數 obj
時,如果有 a,就不會使用預設值了
const obj = {
a: 20
}
const {a = 333, b = 'lemon'} = obj // 幫變數 a 設定預設值
console.log(a, b)
// output: 20 lemon
在 ES5,宣告 function 的方式有兩種:
function test(n){
return n * 2
}
const test = function(n){
return n * 2
}
在 ES6,有另一種宣告 function 的方式,也就是上方第二種的變化版:
(n)
後面加上一個箭頭符號 =>
const test = (n) => {
return n * 2
}
const test = n => {
return n * 2
}
const test = n => n * 2
如果想要把大括號加回去,記得也要把 return 加回去,不然 function 預設都會 return undefined可參考 箭頭函式出現 bug 的可能原因
使用箭頭函式的好處是:程式碼更簡潔、可讀性更高,尤其是在使用array 相關的 function 時更明顯
例如:
在使用 filter()
和 map()
時,裡面都要傳入一個 function
ES5 的寫法是這樣:
let arr = [4, 5, 6, 7, 8]
console.log(
arr
.filter(function(value){
return value > 4
})
.map(function(value){
return value * 2
})
)
// output: [ 10, 12, 14, 16 ]
ES6 的寫法是這樣:
let arr = [4, 5, 6, 7, 8]
console.log(
arr
.filter(value => value > 4)
.map(value => value * 2)
)
// output: [ 10, 12, 14, 16 ]
關於 module 的輸出、引入,在 ES5 和 ES6 有什麼不一樣呢?
module.exports
和 require
是「成對使用」的我現在有兩個檔案,分別是:index.js 和 utils.js
module.exports
在 utils.js 把 module 輸出
function add(a, b){
return a + b
}
module.exports = add
require
然後在 index.js 把 module 引入
let myModule = require('./utils')
console.log(myModule(8, 9))
// output: 17
在 ES6,會有不同的語法來輸出、引入 module
是「成對使用」的
export
用 export
這個關鍵字可以同時把多個東西輸出
export
在 utils.js
假設我想要輸出 add
函式,就在 add
函式前面加上 export
這個關鍵字即可
假設我也想要輸出 PI
這個變數,也一樣就在變數最前面加上 export
這個關鍵字即可
export function add(a, b){
return a + b
}
export const PI = 3.14
export{}
在 utils.js
假設我想要輸出 add
函式和 PI
變數,就在下面寫一個 export
加大括號,然後在大括號裡面寫上 add
函式和 PI
變數
注意!這裡的 export {add, PI}
並不是一個物件
function add(a, b){
return a + b
}
const PI = 3.14
export {
add,
PI
}
import { add, PI } from './utils.js'
import { add, PI } from './utils.js'
console.log(add(8, 9), PI)
// output: 17 3.14
如果直接用 node index.js
執行的話,會出現錯誤:它認不得 import 這個東西
原因是:
目前 Node.js 的版本,還沒辦法原生的支援 import 語法
因此,要用 npx babel-node index.js
來執行 index.js
as
這個關鍵字)假設,我想要用 addFunction
這個新的名稱來輸出 add
函式
export
使用 add as addFunction
用 as
這個關鍵字來幫 add
函式取別名
例如說 export { add as addFunction }
function add(a, b){
return a + b
}
const PI = 3.14
export {
add as addFunction,
PI
}
那在 index.js 引入時,就要使用新的名稱 addFunction
import { addFunction, PI } from './utils.js'
console.log(addFunction(8, 9), PI)
// output: 17 3.14
as
這個關鍵字)import
使用 addFunction as plus
在引入時,如果覺得 addFunction
這個名稱太長了,可以使用 as
來取另一個名稱
例如,我把 addFunction
重新取名叫做 plus
import { addFunction as plus, PI } from './utils.js'
console.log(plus(8, 9), PI)
// output: 17 3.14
*
把所有東西一次引入進來(此方法很常用)utils.js 的輸出:
function add(a, b){
return a + b
}
const PI = 3.14
export {
add as addFunction,
PI
}
*
把 utils.js 裡面的所有東西全部都 import 進來使用這句 import * as hello from './utils.js'
即可
這句的意思是:
把 utils.js 裡面的所有東西,用 hello
這個 module 名稱(自己隨意取)引入到 index.js 裡面
在程式中,*
通常代表「所有東西」的意思
因此,
add
函式時,就要用 hello.addFunction(8, 9)
這樣子PI
時,就要用 hello.PI
這樣子import * as hello from './utils.js'
console.log(hello.addFunction(8, 9), hello.PI)
// output: 17 3.14
export default
default
就是「預設值」的意思
在 utils.js 輸出時,
add
函式前面加上 export default
PI
前面只有寫 export
而已export default function add(a, b){
return a + b
}
export const PI = 3.14
default
這個關鍵字,引入就不需要大括號(因為:預設就會引入)因為 add
函式在輸出時,有加上 default
所以,在 index.js 用 import
引入時,add
不需要加大括號,直接寫上 add
就可以把 add
函式引入了
import add from './utils.js'
console.log(add(8, 9))
// output: 17
default
這個關鍵字,引入就需要大括號因為 PI
在輸出時,沒有加上 default
所以,在 index.js 用 import
引入時,PI
需要加大括號
import add, {PI} from './utils.js'
console.log(add(8, 9), PI)
// output: 17 3.14
用 default
來預設引入,背後的原理可以看成是這樣:
import {default as add, PI} from './utils.js'
console.log(add(8, 9), PI)
下面這兩句是一樣的意思:
import {default as add} from './utils.js'
import add from './utils.js'
Babel 是現代前端開發中,一個不可或缺的工具
如果我想用某個語法,但目前這個語法在舊的瀏覽器還沒被支援,這時該怎麼辦?
就去開發一個工具,把新語法轉換成舊的語法,讓舊的瀏覽器都可以支援
因為前端的發展速度比「瀏覽器更新的速度」要快很多,因此才會需要這麼多前端工具來解決舊瀏覽器支援度或是其他問題
而 Babel 就是一個這樣的前端工具:
輸入 ES6/ES7/ES8 的語法,經過 Babel 編譯之後,轉換成 ES5(如果是在 IE7/IE8 這種超舊的瀏覽器,甚至會轉換成 ES4)
ES6/ES7/ES8 => Babel => ES4/ES5
假設,專案資料夾的檔案有:
export const PI = 3.14
* index.js (輸入)
```javascript=
import add from './utils.js'
console.log(add(8, 9))
如果 Node.js 的版本在 13.0 以下,輸入指令 node index.js
去執行的話,就會出現錯誤:它還無法支援 import
這個語法
這時,就需要 Babel 的幫忙了
Babel 的用法有很多種,這裡要介紹的是 babel-node
babel-node 的指令跟 node 很像(把它當成 node 來用就好),跟 node 的差別在於:babel-node 可以支援更多新的語法,但 node 不行
注意!因為 babel-node 的效能不太好,所以不要用在 production 上面,babel-node 只適合用在自己個人開發上(自己玩玩的)
如果要把 Babel 用在 production 上的話,可以參考這些範例,通常都會先 compile 完之後,再直接用 node 去執行 compile 完的那些檔案
在專案資料夾底下,輸入指令 npm install --save-dev @babel/core @babel/node
來安裝 @babel/core 和 @babel/node
npx babel-node
來進入「可以輸入程式碼」的模式npx babel-node index.js
來執行 index.js,還是會出現錯誤:無法支援 import
語法原因是:
Babel 還需要設定一些東西,才能支援這些新的語法
設定方式是:
輸入指令 npm install --save @babel/preset-env
來安裝 @babel/preset-env
preset 裡面是一些設定,會告訴 Babel 關於語法轉換的事情(要把多新的語法,轉換成多舊的語法)
一定要裝 @babel/preset-env 套件,Babel 才會知道要幫我把 ES6 轉換成 ES5 的語法
npx babel-node index.js
來執行 index.js,還是一樣會出現錯誤:無法支援 import
語法原因是:
我只是安裝了 @babel/preset-env 套件,但我還沒跟 Babel 說「我要使用 @babel/preset-env 這個套件」
{
"presets": ["@babel/preset-env"]
}
意思就是:我的 preset 要使用的是 @babel/preset-env 這個套件
npx babel-node index.js
來執行 index.js,就可以成功執行了npm install --save-dev @babel/core @babel/node @babel/preset-env
{
"presets": ["@babel/preset-env"]
}
這樣,就可以用 npx babel-node index.js
來執行了!]]>想查詢更多 ES6 新增的語法,可參考 es6-cheatsheet
default parameters 就是「預設值」的意思
這裡是一個 repeat
函式
function repeat(str, times){
return str.repeat(times)
}
console.log(repeat('love', 5))
// output: lovelovelovelovelove
在呼叫 repeat
函式時,如果參數 times
沒有寫,那麼參數 times
就會是 undefined,然後印出 repeat('love')
的結果也會是什麼都沒有
function repeat(str, times){
console.log(times) // 把 times 印出來看看
return str.repeat(times)
}
console.log(repeat('love'))
output:
undefined
在 repeat
函式的參數 times
後方設定 = 5
的預設值,這樣,在呼叫 repeat
函式時如果沒有傳入 times
,times
就會是預設值(5)
function repeat(str, times = 5){
console.log(times) // 把 times 印出來看看
return str.repeat(times)
}
console.log(repeat('love'))
output:
5
lovelovelovelovelove
也可以把兩個參數 str
和 times
都設定預設值
這樣,在呼叫 repeat
函式時,如果完全沒有傳入參數,就會都使用預設值
function repeat(str = 'hey', times = 3){
return str.repeat(times)
}
console.log(repeat())
// output: heyheyhey
「預設值」也可以用在陣列、物件的解構
const obj = {
a: 8,
b: 9
}
const {a, b} = obj
console.log(a, b)
// output: 8 9
如果在宣告變數 obj
時,就沒有 a,那麼解構之後印出變數 a
就會是 undefined
const obj = {
b: 9
}
const {a, b} = obj
console.log(a, b)
// output: undefined 9
在解構時,可以幫變數 a
設定預設值 = 333
在宣告變數 obj
時,如果沒有 a,那麼印出來的變數 a
就會是 333
const obj = {
b: 9
}
const {a = 333, b} = obj // 幫變數 a 設定預設值
console.log(a, b)
// output: 333 9
但在宣告變數 obj
時,如果有 a,就不會使用預設值了
const obj = {
a: 20
}
const {a = 333, b = 'lemon'} = obj // 幫變數 a 設定預設值
console.log(a, b)
// output: 20 lemon
在 ES5,宣告 function 的方式有兩種:
function test(n){
return n * 2
}
const test = function(n){
return n * 2
}
在 ES6,有另一種宣告 function 的方式,也就是上方第二種的變化版:
(n)
後面加上一個箭頭符號 =>
const test = (n) => {
return n * 2
}
const test = n => {
return n * 2
}
const test = n => n * 2
如果想要把大括號加回去,記得也要把 return 加回去,不然 function 預設都會 return undefined可參考 箭頭函式出現 bug 的可能原因
使用箭頭函式的好處是:程式碼更簡潔、可讀性更高,尤其是在使用array 相關的 function 時更明顯
例如:
在使用 filter()
和 map()
時,裡面都要傳入一個 function
ES5 的寫法是這樣:
let arr = [4, 5, 6, 7, 8]
console.log(
arr
.filter(function(value){
return value > 4
})
.map(function(value){
return value * 2
})
)
// output: [ 10, 12, 14, 16 ]
ES6 的寫法是這樣:
let arr = [4, 5, 6, 7, 8]
console.log(
arr
.filter(value => value > 4)
.map(value => value * 2)
)
// output: [ 10, 12, 14, 16 ]
關於 module 的輸出、引入,在 ES5 和 ES6 有什麼不一樣呢?
module.exports
和 require
是「成對使用」的我現在有兩個檔案,分別是:index.js 和 utils.js
module.exports
在 utils.js 把 module 輸出
function add(a, b){
return a + b
}
module.exports = add
require
然後在 index.js 把 module 引入
let myModule = require('./utils')
console.log(myModule(8, 9))
// output: 17
在 ES6,會有不同的語法來輸出、引入 module
是「成對使用」的
export
用 export
這個關鍵字可以同時把多個東西輸出
export
在 utils.js
假設我想要輸出 add
函式,就在 add
函式前面加上 export
這個關鍵字即可
假設我也想要輸出 PI
這個變數,也一樣就在變數最前面加上 export
這個關鍵字即可
export function add(a, b){
return a + b
}
export const PI = 3.14
export{}
在 utils.js
假設我想要輸出 add
函式和 PI
變數,就在下面寫一個 export
加大括號,然後在大括號裡面寫上 add
函式和 PI
變數
注意!這裡的 export {add, PI}
並不是一個物件
function add(a, b){
return a + b
}
const PI = 3.14
export {
add,
PI
}
import { add, PI } from './utils.js'
import { add, PI } from './utils.js'
console.log(add(8, 9), PI)
// output: 17 3.14
如果直接用 node index.js
執行的話,會出現錯誤:它認不得 import 這個東西
原因是:
目前 Node.js 的版本,還沒辦法原生的支援 import 語法
因此,要用 npx babel-node index.js
來執行 index.js
as
這個關鍵字)假設,我想要用 addFunction
這個新的名稱來輸出 add
函式
export
使用 add as addFunction
用 as
這個關鍵字來幫 add
函式取別名
例如說 export { add as addFunction }
function add(a, b){
return a + b
}
const PI = 3.14
export {
add as addFunction,
PI
}
那在 index.js 引入時,就要使用新的名稱 addFunction
import { addFunction, PI } from './utils.js'
console.log(addFunction(8, 9), PI)
// output: 17 3.14
as
這個關鍵字)import
使用 addFunction as plus
在引入時,如果覺得 addFunction
這個名稱太長了,可以使用 as
來取另一個名稱
例如,我把 addFunction
重新取名叫做 plus
import { addFunction as plus, PI } from './utils.js'
console.log(plus(8, 9), PI)
// output: 17 3.14
*
把所有東西一次引入進來(此方法很常用)utils.js 的輸出:
function add(a, b){
return a + b
}
const PI = 3.14
export {
add as addFunction,
PI
}
*
把 utils.js 裡面的所有東西全部都 import 進來使用這句 import * as hello from './utils.js'
即可
這句的意思是:
把 utils.js 裡面的所有東西,用 hello
這個 module 名稱(自己隨意取)引入到 index.js 裡面
在程式中,*
通常代表「所有東西」的意思
因此,
add
函式時,就要用 hello.addFunction(8, 9)
這樣子PI
時,就要用 hello.PI
這樣子import * as hello from './utils.js'
console.log(hello.addFunction(8, 9), hello.PI)
// output: 17 3.14
export default
default
就是「預設值」的意思
在 utils.js 輸出時,
add
函式前面加上 export default
PI
前面只有寫 export
而已export default function add(a, b){
return a + b
}
export const PI = 3.14
default
這個關鍵字,引入就不需要大括號(因為:預設就會引入)因為 add
函式在輸出時,有加上 default
所以,在 index.js 用 import
引入時,add
不需要加大括號,直接寫上 add
就可以把 add
函式引入了
import add from './utils.js'
console.log(add(8, 9))
// output: 17
default
這個關鍵字,引入就需要大括號因為 PI
在輸出時,沒有加上 default
所以,在 index.js 用 import
引入時,PI
需要加大括號
import add, {PI} from './utils.js'
console.log(add(8, 9), PI)
// output: 17 3.14
用 default
來預設引入,背後的原理可以看成是這樣:
import {default as add, PI} from './utils.js'
console.log(add(8, 9), PI)
下面這兩句是一樣的意思:
import {default as add} from './utils.js'
import add from './utils.js'
Babel 是現代前端開發中,一個不可或缺的工具
如果我想用某個語法,但目前這個語法在舊的瀏覽器還沒被支援,這時該怎麼辦?
就去開發一個工具,把新語法轉換成舊的語法,讓舊的瀏覽器都可以支援
因為前端的發展速度比「瀏覽器更新的速度」要快很多,因此才會需要這麼多前端工具來解決舊瀏覽器支援度或是其他問題
而 Babel 就是一個這樣的前端工具:
輸入 ES6/ES7/ES8 的語法,經過 Babel 編譯之後,轉換成 ES5(如果是在 IE7/IE8 這種超舊的瀏覽器,甚至會轉換成 ES4)
ES6/ES7/ES8 => Babel => ES4/ES5
假設,專案資料夾的檔案有:
export const PI = 3.14
* index.js (輸入)
```javascript=
import add from './utils.js'
console.log(add(8, 9))
如果 Node.js 的版本在 13.0 以下,輸入指令 node index.js
去執行的話,就會出現錯誤:它還無法支援 import
這個語法
這時,就需要 Babel 的幫忙了
Babel 的用法有很多種,這裡要介紹的是 babel-node
babel-node 的指令跟 node 很像(把它當成 node 來用就好),跟 node 的差別在於:babel-node 可以支援更多新的語法,但 node 不行
注意!因為 babel-node 的效能不太好,所以不要用在 production 上面,babel-node 只適合用在自己個人開發上(自己玩玩的)
如果要把 Babel 用在 production 上的話,可以參考這些範例,通常都會先 compile 完之後,再直接用 node 去執行 compile 完的那些檔案
在專案資料夾底下,輸入指令 npm install --save-dev @babel/core @babel/node
來安裝 @babel/core 和 @babel/node
npx babel-node
來進入「可以輸入程式碼」的模式npx babel-node index.js
來執行 index.js,還是會出現錯誤:無法支援 import
語法原因是:
Babel 還需要設定一些東西,才能支援這些新的語法
設定方式是:
輸入指令 npm install --save @babel/preset-env
來安裝 @babel/preset-env
preset 裡面是一些設定,會告訴 Babel 關於語法轉換的事情(要把多新的語法,轉換成多舊的語法)
一定要裝 @babel/preset-env 套件,Babel 才會知道要幫我把 ES6 轉換成 ES5 的語法
npx babel-node index.js
來執行 index.js,還是一樣會出現錯誤:無法支援 import
語法原因是:
我只是安裝了 @babel/preset-env 套件,但我還沒跟 Babel 說「我要使用 @babel/preset-env 這個套件」
{
"presets": ["@babel/preset-env"]
}
意思就是:我的 preset 要使用的是 @babel/preset-env 這個套件
npx babel-node index.js
來執行 index.js,就可以成功執行了npm install --save-dev @babel/core @babel/node @babel/preset-env
{
"presets": ["@babel/preset-env"]
}
這樣,就可以用 npx babel-node index.js
來執行了!]]>想查詢更多 ES6 新增的語法,可參考 es6-cheatsheet
我現在要宣告四個變數,分別等於陣列 arr
的第 0, 1, 2, 3 個元素,在 ES5 就只能這樣寫:
要重複寫很多次「結構一樣的東西」,好麻煩
const arr = [7, 8, 9, 10]
let first = arr[0]
let second = arr[1]
let third = arr[2]
let forth = arr[3]
console.log(second, third)
// output: 8 9
這句 var [first, second, third, forth] = arr
可以看成是
var [first, second, third, forth] = [7, 8, 9, 10]
first
會對應到 7second
會對應到 8third
會對應到 9forth
會對應到 10const arr = [7, 8, 9, 10]
var [first, second, third, forth] = arr
console.log(second, third)
// output: 8 9
也可以只解構出「第 0 個元素」
const arr = [7, 8, 9, 10]
var [first] = arr
console.log(first)
// output: 7
我現在要宣告三個變數,分別等於物件 obj
的 name
, age
, address
,在 ES5 就只能這樣寫:
要重複寫很多次「結構一樣的東西」,好麻煩
const obj = {
name: 'Daniel',
age: 28,
address: 'Munich'
}
let userName = obj.name
let userAge = obj.age
let userAddress = obj.address
console.log(userAge, userAddress)
// output: 28 Munich
大括號內填入我要的 key(也就是 name
, age
, address
)
解構之後,name
, age
, address
就會各自變成「一個變數」,就可以直接用 console.log(address)
來印出 address 的 value
解構物件,就是同時做了兩件事情:
obj.name
obj.age
obj.address
const obj = {
name: 'Daniel',
age: 28,
address: 'Munich'
}
let {name, age, address} = obj
console.log(address)
// output: Munich
如果在解構的大括號內,放了一個不存在的 key,那麼 phone
的 value 就會是 undefined
const obj = {
name: 'Daniel',
age: 28,
address: 'Munich'
}
let {phone} = obj
console.log(phone)
// output: undefined
在物件 obj
裡面,有另一個物件 family
把 family 解構出來,變數 family 也會是一個物件
const obj = {
name: 'Daniel',
age: 28,
address: 'Munich',
family: {
father: 'Jack',
mother: 'Wendy'
}
}
let {family} = obj
console.log(family)
// output: { father: 'Jack', mother: 'Wendy' }
接著,我再用一次解構,就可以把 mother 拿出來
const obj = {
name: 'Daniel',
age: 28,
address: 'Munich',
family: {
father: 'Jack',
mother: 'Wendy'
}
}
let {family} = obj
let {mother} = family
console.log(mother)
// output: Wendy
解構再解構,也可以這樣寫:
先取出 family,再取出 mother(最後只會取出最後一層的 mother 而已)
const obj = {
name: 'Daniel',
age: 28,
address: 'Munich',
family: {
father: 'Jack',
mother: 'Wendy'
}
}
let {family: {mother}} = obj
console.log(mother)
// output: Wendy
可以把 obj
跟解構後的語法做對照,其實結構是一樣的,因此就可以互相對應:
const obj = {
family: {
father: 'Jack',
mother: 'Wendy'
}
}
let {
family: {
mother
}
} = obj
在 ES5 時,function 接收的參數就只能是 obj
所以,我要把 address
的 value 印出來,就要寫 console.log(obj.address)
function test(obj){
console.log(obj.address)
}
test({
name: 'Daniel',
age: 28,
address: 'Munich'
})
// output: Munich
在 function 接收參數時就做解構
就可以直接用 console.log(address)
印出 value 了
function test({name, age, address}) {
console.log(address)
}
test({
name: 'Daniel',
age: 28,
address: 'Munich'
})
// output: Munich
Spread Operator 就是「展開運算子」,可以把物件、陣列的最外層括號去掉
在 arr2
裡面放入 arr
,陣列 arr2
就會變成一個「雙層的 array」
let arr = [4, 5, 6]
let arr2 = [7, 8, 9, arr]
console.log(arr2)
// output: [ 7, 8, 9, [ 4, 5, 6 ] ]
展開運算子的用法:
在 arr
前面加上 ...
,就可以把 arr
展開:把 arr
變成「三個不同的數字」,而不是「一個陣列」
let arr = [4, 5, 6]
let arr2 = [7, 8, 9, ...arr]
console.log(arr2)
// output: [ 7, 8, 9, 4, 5, 6 ]
可以把 ...arr
加在任何地方,例如:
let arr = [4, 5, 6]
let arr2 = [7, ...arr, 8, 9]
console.log(arr2)
// output: [ 7, 4, 5, 6, 8, 9 ]
[還沒使用展開運算子]
在 add
函式傳入的參數是:三個數字 7, 8, 9
function add(a, b, c){
return a + b + c
}
console.log(add(7, 8, 9))
// output: 24
現在,我想要把 arr
陣列當作參數,傳入 add
函式
如果沒有用展開運算子的話,就會回傳錯誤的結果「7,8,9undefinedundefined」
因為參數 a 會是整個 arr
陣列,b 和 c 都是 undefined
function add(a, b, c){
return a + b + c
}
let arr = [7, 8, 9]
console.log(add(arr))
// output: 7,8,9undefinedundefined
[使用展開運算子]
當我用展開運算子把 ...arr
傳入 add
函式時,展開運算子就會把陣列 [7, 8, 9]
展開變成三個數字(7, 8, 9),也就會分別對應到三個參數(a, b, c)
因此,就會回傳正確的結果(24)
function add(a, b, c){
return a + b + c
}
let arr = [7, 8, 9]
console.log(add(...arr))
// output: 24
可以把 add(...arr)
想成是 add(7, 8, 9)
物件也可以使用展開運算子
在變數 obj3
裡面:
把 obj1
的物件展開變成「獨立的個體」(把大括號拿掉),再放進 obj3
裡面
let obj1 = {
a: 5,
b: 6
}
let obj2 = {
z: 30
}
let obj3 = {
...obj1,
c: 7
}
console.log(obj3)
// output: { a: 5, b: 6, c: 7 }
如果我在 obj3
的最後面有一個 b: 80
,那麼「後面的值會覆蓋掉前面的值」,所以 obj3
的 b
就會是 80
let obj1 = {
a: 5,
b: 6
}
let obj2 = {
z: 30
}
let obj3 = {
...obj1,
b: 80
}
console.log(obj3)
// output: { a: 5, b: 80 }
但如果我在 obj3
的最前面有一個 b: 80
,那麼「後面的值會覆蓋掉前面的值」,所以 obj3
的 b
就會是 6
let obj1 = {
a: 5,
b: 6
}
let obj2 = {
z: 30
}
let obj3 = {
b: 80,
...obj1
}
console.log(obj3)
// output: { b: 6, a: 5 }
我有一個陣列 arr
我想要有另一個新的陣列 arr2
,內容要跟陣列 arr
一樣(也就是要“複製”另一個新的陣列出來,內容也要是 [7, 8, 9]
)
let arr2 = arr
這樣不叫做「複製」另一個新的陣列出來
因為:
所謂的複製人,就是長得一樣但是要是「兩個不同」的人,這樣才叫做「複製」
如果是寫 let arr2 = arr
的話,arr
和 arr2
會指向「同一個記憶體位置」,它們根本就是「同一個陣列」,所以 arr === arr2
會是 true
let arr = [7, 8, 9]
let arr2 = arr
console.log(arr === arr2)
// output: true
let arr2 = [...arr]
使用展開運算子,把 arr
的內容先展開成三個獨立的數字(7, 8, 9)後,再放到新的空陣列 arr2
裡面。這樣,arr
和 arr2
就會指向「不同的記憶體位置」,是兩個不同的陣列,因此 arr === arr2
會是 false,這樣才叫做「複製 arr
陣列」
let arr = [7, 8, 9]
let arr2 = [...arr]
console.log(arr === arr2)
// output: false
因為是用展開運算子複製「值」,所以 arr
跟 arr2
的值一樣,但兩個是不同的陣列(arr === arr2
是 false)
let nestedArray = [10]
let arr = [7, 8, 9, nestedArray]
console.log('arr: ', arr)
let arr2 = [...arr]
console.log('arr2: ', arr2)
console.log(arr === arr2)
output:
arr: [ 7, 8, 9, [ 10 ] ]
arr2: [ 7, 8, 9, [ 10 ] ]
false
nestedArray
是一個「陣列」let arr2 = [...arr]
可以看成是:
let arr2 = [7, 8, 9, nestedArray]
也就是:
arr2[3] = nestedArray
也就是說:
arr2[3] 和 nestedArray 會指向「同一個記憶體位置」,是「同一個陣列」
因此:
單獨把 index = 3 這個元素拉出來看的話,會是兩個同樣的陣列
arr[3] === arr2[3]
會是 true
let nestedArray = [10]
let arr = [7, 8, 9, nestedArray]
console.log('arr: ', arr)
let arr2 = [...arr]
console.log('arr2: ', arr2)
console.log(arr[3] === arr2[3])
output:
arr: [ 7, 8, 9, [ 10 ] ]
arr2: [ 7, 8, 9, [ 10 ] ]
true
有一個物件 obj
我希望有另一個新的物件 obj2
,跟 obj
有一樣的值(是兩個不同的物件)
let obj2 = obj
obj
和 obj2
會指向同一個記憶體位置(根本就是同一個物件)
let obj = {
a: 8,
b: 9
}
let obj2 = obj
console.log(obj, obj2, obj === obj2)
// output: { a: 8, b: 9 } { a: 8, b: 9 } true
obj
和 obj2
的值會一樣,但是兩個會指向不同的記憶體位置(不同的物件)
let obj = {
a: 8,
b: 9
}
let obj2 = {
...obj
}
console.log(obj, obj2, obj === obj2)
// output: { a: 8, b: 9 } { a: 8, b: 9 } false
前面講到的「展開運算子」,是把 [7, 8, 9]
展開,也就是把 7, 8, 9 直接放到 arr2
裡面
let arr = [7, 8, 9]
let arr2 = [...arr, 10]
console.log(arr2)
// output: [ 7, 8, 9, 10 ]
Rest Parameters 的語法是 ...rest
Rest Parameters 通常會跟「解構」一起使用
例如:
在下方的例子中
變數 first
會配對到 7
rest 就是「剩下的東西」的意思,...rest
會把陣列 arr
剩下的東西(還沒配對到的),也就是 8, 9, 10 這三個數字,變成一個 array 後放到變數 rest
裡面去
因此,變數 rest
就會是 [ 8, 9, 10 ]
的一個 array
let arr = [7, 8, 9, 10]
let [first, ...rest] = arr
console.log(rest)
// output: [ 8, 9, 10 ]
...rest
只能放在最後面下面這樣寫是錯的:
因為 ...rest
只能放在最後面,所以如果把 ...rest
放在中間就會出現錯誤:Rest element must be last element
let arr = [7, 8, 9, 10, 11]
let [first, ...rest, last] = arr
console.log(rest)
...rest
的變數 rest
也可以改取其他的名稱,例如叫做 obj2
變數 obj2
就會是一個 { b: 8, c: 9 }
的 object
let obj = {
a: 7,
b: 8,
c: 9
}
let {a, ...obj2} = obj
console.log(obj2)
// output: { b: 8, c: 9 }
...obj2
也可以是「整個物件」let obj = {
a: 7,
b: 8,
c: 9
}
let {...obj2} = obj
console.log(obj2)
// output: { a: 7, b: 8, c: 9 }
...
就只有這兩種用法:「把東西展開」或是「把東西集合起來」下面把「展開運算子」和「Rest Parameters」都使用到了
let obj = {
a: 7,
b: 8,
}
let obj2 = {
...obj,
c: 10
}
let {a, ...rest} = obj2
console.log(rest)
// output: { b: 8, c: 10 }
沒有使用 Rest Parameters,原本的寫法是這樣:
function add(a, b){
return a + b
}
console.log(add(8, 9))
// output: 17
如果我在 add
函式的參數使用 Rest Parameters
...args
就會把 8, 9 這兩個數字變成一個陣列 [ 8, 9 ]
所以,就可以用 args[0]
取得第一個參數 8,用 args[1]
取得第二個參數 9
這裡的
args
很像 arguments,但它們兩個的差異是:args
是一個陣列,arguments 是一個「類陣列」的物件
function add(...args){
console.log(args)
return args[0] + args[1]
}
console.log(add(8, 9))
output:
[ 8, 9 ]
17
]]>我現在要宣告四個變數,分別等於陣列 arr
的第 0, 1, 2, 3 個元素,在 ES5 就只能這樣寫:
要重複寫很多次「結構一樣的東西」,好麻煩
const arr = [7, 8, 9, 10]
let first = arr[0]
let second = arr[1]
let third = arr[2]
let forth = arr[3]
console.log(second, third)
// output: 8 9
這句 var [first, second, third, forth] = arr
可以看成是
var [first, second, third, forth] = [7, 8, 9, 10]
first
會對應到 7second
會對應到 8third
會對應到 9forth
會對應到 10const arr = [7, 8, 9, 10]
var [first, second, third, forth] = arr
console.log(second, third)
// output: 8 9
也可以只解構出「第 0 個元素」
const arr = [7, 8, 9, 10]
var [first] = arr
console.log(first)
// output: 7
我現在要宣告三個變數,分別等於物件 obj
的 name
, age
, address
,在 ES5 就只能這樣寫:
要重複寫很多次「結構一樣的東西」,好麻煩
const obj = {
name: 'Daniel',
age: 28,
address: 'Munich'
}
let userName = obj.name
let userAge = obj.age
let userAddress = obj.address
console.log(userAge, userAddress)
// output: 28 Munich
大括號內填入我要的 key(也就是 name
, age
, address
)
解構之後,name
, age
, address
就會各自變成「一個變數」,就可以直接用 console.log(address)
來印出 address 的 value
解構物件,就是同時做了兩件事情:
obj.name
obj.age
obj.address
const obj = {
name: 'Daniel',
age: 28,
address: 'Munich'
}
let {name, age, address} = obj
console.log(address)
// output: Munich
如果在解構的大括號內,放了一個不存在的 key,那麼 phone
的 value 就會是 undefined
const obj = {
name: 'Daniel',
age: 28,
address: 'Munich'
}
let {phone} = obj
console.log(phone)
// output: undefined
在物件 obj
裡面,有另一個物件 family
把 family 解構出來,變數 family 也會是一個物件
const obj = {
name: 'Daniel',
age: 28,
address: 'Munich',
family: {
father: 'Jack',
mother: 'Wendy'
}
}
let {family} = obj
console.log(family)
// output: { father: 'Jack', mother: 'Wendy' }
接著,我再用一次解構,就可以把 mother 拿出來
const obj = {
name: 'Daniel',
age: 28,
address: 'Munich',
family: {
father: 'Jack',
mother: 'Wendy'
}
}
let {family} = obj
let {mother} = family
console.log(mother)
// output: Wendy
解構再解構,也可以這樣寫:
先取出 family,再取出 mother(最後只會取出最後一層的 mother 而已)
const obj = {
name: 'Daniel',
age: 28,
address: 'Munich',
family: {
father: 'Jack',
mother: 'Wendy'
}
}
let {family: {mother}} = obj
console.log(mother)
// output: Wendy
可以把 obj
跟解構後的語法做對照,其實結構是一樣的,因此就可以互相對應:
const obj = {
family: {
father: 'Jack',
mother: 'Wendy'
}
}
let {
family: {
mother
}
} = obj
在 ES5 時,function 接收的參數就只能是 obj
所以,我要把 address
的 value 印出來,就要寫 console.log(obj.address)
function test(obj){
console.log(obj.address)
}
test({
name: 'Daniel',
age: 28,
address: 'Munich'
})
// output: Munich
在 function 接收參數時就做解構
就可以直接用 console.log(address)
印出 value 了
function test({name, age, address}) {
console.log(address)
}
test({
name: 'Daniel',
age: 28,
address: 'Munich'
})
// output: Munich
Spread Operator 就是「展開運算子」,可以把物件、陣列的最外層括號去掉
在 arr2
裡面放入 arr
,陣列 arr2
就會變成一個「雙層的 array」
let arr = [4, 5, 6]
let arr2 = [7, 8, 9, arr]
console.log(arr2)
// output: [ 7, 8, 9, [ 4, 5, 6 ] ]
展開運算子的用法:
在 arr
前面加上 ...
,就可以把 arr
展開:把 arr
變成「三個不同的數字」,而不是「一個陣列」
let arr = [4, 5, 6]
let arr2 = [7, 8, 9, ...arr]
console.log(arr2)
// output: [ 7, 8, 9, 4, 5, 6 ]
可以把 ...arr
加在任何地方,例如:
let arr = [4, 5, 6]
let arr2 = [7, ...arr, 8, 9]
console.log(arr2)
// output: [ 7, 4, 5, 6, 8, 9 ]
[還沒使用展開運算子]
在 add
函式傳入的參數是:三個數字 7, 8, 9
function add(a, b, c){
return a + b + c
}
console.log(add(7, 8, 9))
// output: 24
現在,我想要把 arr
陣列當作參數,傳入 add
函式
如果沒有用展開運算子的話,就會回傳錯誤的結果「7,8,9undefinedundefined」
因為參數 a 會是整個 arr
陣列,b 和 c 都是 undefined
function add(a, b, c){
return a + b + c
}
let arr = [7, 8, 9]
console.log(add(arr))
// output: 7,8,9undefinedundefined
[使用展開運算子]
當我用展開運算子把 ...arr
傳入 add
函式時,展開運算子就會把陣列 [7, 8, 9]
展開變成三個數字(7, 8, 9),也就會分別對應到三個參數(a, b, c)
因此,就會回傳正確的結果(24)
function add(a, b, c){
return a + b + c
}
let arr = [7, 8, 9]
console.log(add(...arr))
// output: 24
可以把 add(...arr)
想成是 add(7, 8, 9)
物件也可以使用展開運算子
在變數 obj3
裡面:
把 obj1
的物件展開變成「獨立的個體」(把大括號拿掉),再放進 obj3
裡面
let obj1 = {
a: 5,
b: 6
}
let obj2 = {
z: 30
}
let obj3 = {
...obj1,
c: 7
}
console.log(obj3)
// output: { a: 5, b: 6, c: 7 }
如果我在 obj3
的最後面有一個 b: 80
,那麼「後面的值會覆蓋掉前面的值」,所以 obj3
的 b
就會是 80
let obj1 = {
a: 5,
b: 6
}
let obj2 = {
z: 30
}
let obj3 = {
...obj1,
b: 80
}
console.log(obj3)
// output: { a: 5, b: 80 }
但如果我在 obj3
的最前面有一個 b: 80
,那麼「後面的值會覆蓋掉前面的值」,所以 obj3
的 b
就會是 6
let obj1 = {
a: 5,
b: 6
}
let obj2 = {
z: 30
}
let obj3 = {
b: 80,
...obj1
}
console.log(obj3)
// output: { b: 6, a: 5 }
我有一個陣列 arr
我想要有另一個新的陣列 arr2
,內容要跟陣列 arr
一樣(也就是要“複製”另一個新的陣列出來,內容也要是 [7, 8, 9]
)
let arr2 = arr
這樣不叫做「複製」另一個新的陣列出來
因為:
所謂的複製人,就是長得一樣但是要是「兩個不同」的人,這樣才叫做「複製」
如果是寫 let arr2 = arr
的話,arr
和 arr2
會指向「同一個記憶體位置」,它們根本就是「同一個陣列」,所以 arr === arr2
會是 true
let arr = [7, 8, 9]
let arr2 = arr
console.log(arr === arr2)
// output: true
let arr2 = [...arr]
使用展開運算子,把 arr
的內容先展開成三個獨立的數字(7, 8, 9)後,再放到新的空陣列 arr2
裡面。這樣,arr
和 arr2
就會指向「不同的記憶體位置」,是兩個不同的陣列,因此 arr === arr2
會是 false,這樣才叫做「複製 arr
陣列」
let arr = [7, 8, 9]
let arr2 = [...arr]
console.log(arr === arr2)
// output: false
因為是用展開運算子複製「值」,所以 arr
跟 arr2
的值一樣,但兩個是不同的陣列(arr === arr2
是 false)
let nestedArray = [10]
let arr = [7, 8, 9, nestedArray]
console.log('arr: ', arr)
let arr2 = [...arr]
console.log('arr2: ', arr2)
console.log(arr === arr2)
output:
arr: [ 7, 8, 9, [ 10 ] ]
arr2: [ 7, 8, 9, [ 10 ] ]
false
nestedArray
是一個「陣列」let arr2 = [...arr]
可以看成是:
let arr2 = [7, 8, 9, nestedArray]
也就是:
arr2[3] = nestedArray
也就是說:
arr2[3] 和 nestedArray 會指向「同一個記憶體位置」,是「同一個陣列」
因此:
單獨把 index = 3 這個元素拉出來看的話,會是兩個同樣的陣列
arr[3] === arr2[3]
會是 true
let nestedArray = [10]
let arr = [7, 8, 9, nestedArray]
console.log('arr: ', arr)
let arr2 = [...arr]
console.log('arr2: ', arr2)
console.log(arr[3] === arr2[3])
output:
arr: [ 7, 8, 9, [ 10 ] ]
arr2: [ 7, 8, 9, [ 10 ] ]
true
有一個物件 obj
我希望有另一個新的物件 obj2
,跟 obj
有一樣的值(是兩個不同的物件)
let obj2 = obj
obj
和 obj2
會指向同一個記憶體位置(根本就是同一個物件)
let obj = {
a: 8,
b: 9
}
let obj2 = obj
console.log(obj, obj2, obj === obj2)
// output: { a: 8, b: 9 } { a: 8, b: 9 } true
obj
和 obj2
的值會一樣,但是兩個會指向不同的記憶體位置(不同的物件)
let obj = {
a: 8,
b: 9
}
let obj2 = {
...obj
}
console.log(obj, obj2, obj === obj2)
// output: { a: 8, b: 9 } { a: 8, b: 9 } false
前面講到的「展開運算子」,是把 [7, 8, 9]
展開,也就是把 7, 8, 9 直接放到 arr2
裡面
let arr = [7, 8, 9]
let arr2 = [...arr, 10]
console.log(arr2)
// output: [ 7, 8, 9, 10 ]
Rest Parameters 的語法是 ...rest
Rest Parameters 通常會跟「解構」一起使用
例如:
在下方的例子中
變數 first
會配對到 7
rest 就是「剩下的東西」的意思,...rest
會把陣列 arr
剩下的東西(還沒配對到的),也就是 8, 9, 10 這三個數字,變成一個 array 後放到變數 rest
裡面去
因此,變數 rest
就會是 [ 8, 9, 10 ]
的一個 array
let arr = [7, 8, 9, 10]
let [first, ...rest] = arr
console.log(rest)
// output: [ 8, 9, 10 ]
...rest
只能放在最後面下面這樣寫是錯的:
因為 ...rest
只能放在最後面,所以如果把 ...rest
放在中間就會出現錯誤:Rest element must be last element
let arr = [7, 8, 9, 10, 11]
let [first, ...rest, last] = arr
console.log(rest)
...rest
的變數 rest
也可以改取其他的名稱,例如叫做 obj2
變數 obj2
就會是一個 { b: 8, c: 9 }
的 object
let obj = {
a: 7,
b: 8,
c: 9
}
let {a, ...obj2} = obj
console.log(obj2)
// output: { b: 8, c: 9 }
...obj2
也可以是「整個物件」let obj = {
a: 7,
b: 8,
c: 9
}
let {...obj2} = obj
console.log(obj2)
// output: { a: 7, b: 8, c: 9 }
...
就只有這兩種用法:「把東西展開」或是「把東西集合起來」下面把「展開運算子」和「Rest Parameters」都使用到了
let obj = {
a: 7,
b: 8,
}
let obj2 = {
...obj,
c: 10
}
let {a, ...rest} = obj2
console.log(rest)
// output: { b: 8, c: 10 }
沒有使用 Rest Parameters,原本的寫法是這樣:
function add(a, b){
return a + b
}
console.log(add(8, 9))
// output: 17
如果我在 add
函式的參數使用 Rest Parameters
...args
就會把 8, 9 這兩個數字變成一個陣列 [ 8, 9 ]
所以,就可以用 args[0]
取得第一個參數 8,用 args[1]
取得第二個參數 9
這裡的
args
很像 arguments,但它們兩個的差異是:args
是一個陣列,arguments 是一個「類陣列」的物件
function add(...args){
console.log(args)
return args[0] + args[1]
}
console.log(add(8, 9))
output:
[ 8, 9 ]
17
]]>
ECMAScript 就是一個「標準、規範」,開發者可以根據這個規範去開發程式語言
而 JavaScript 這個程式語言就是根據 ECMAScript 去實作的
const 的全名是 constant(常數),是「不能改變」的
使用時機:
有些變數,我已經知道它是不會變的,就可以用 const 來宣告
例如:
我用 const 宣告變數 b
(是一個 number,屬於 primitive type),變數裡面是直接存「值」
因此,變數 b
的「值」是不能改變的
如果我把變數 b
的值改成 20
console.log(b)
就會出現錯誤:Assignment to constant variable.(你不可以改變一個 constant variable)
const b = 9
b = 20
console.log(b)
(object 可以是物件或陣列)
例如:
我用 const 宣告變數 b
(是一個物件,屬於 object),變數裡面存的是「記憶體位置」
因此,我用 b.number = 30
修改 number 的 value
結果發現是可以更改的!
const b = {
number: 10
}
b.number = 30
console.log(b)
// output: { number: 30 }
原因是:
「用 const 宣告的變數,不能改變」,對於 object 來說是指:「物件/陣列裡面存的 “記憶體位置” 不能改變」,如果只是更改 b.number = 30
並沒有改動到記憶體位置,b.number = 30
只是去改「那個記憶體位置上的物件的 value」而已,因此是可以更改的!(用這種 .
的方式去修改物件裡面的值)
b
,那就不行了,因為新的物件會是「新的記憶體位置」例如:
下面這樣寫就會出現錯誤:Assignment to constant variable(你不可以改變一個 constant variable)
const b = {
number: 10
}
b = {
number: 99
}
console.log(b)
要了解 var 跟 let, const 的差別,首先要了解「作用域」的概念:
作用域,就是「變數的生存範圍」
例如:
範例一:在 function 外面宣告變數 a
變數 a
是「全域變數」
因為我在外層宣告的變數 a
是在「檔案的這層」,所以,如果 test
function 裡面沒有宣告 a
,它就會往上層找,直到找到 a
為止,因此就會找到 var a = 20
var a = 20
function test(){
console.log(a)
}
test()
// output: 20
範例二:function 裡面有宣告變數 a
因為在 test
function 裡面就有宣告 a
,所以它在 function 裡面就找到 a
了
var a = 20
function test(){
var a = 99
console.log(a)
}
test()
// output: 99
因為我是在 test
function 裡面宣告變數 a
,所以變數 a
的作用域就只存在 test
function 裡面
變數 a
是「區域變數」
在 test
function 外面是無法使用變數 a 的
因此,console.log(a)
就會出現錯誤:a is not defined
function test(){
var a = 20
}
test()
console.log(a)
例如:
雖然我是在 if 的區塊(block)宣告變數 a
,但只要是在 test
function 裡面,就都是變數 a
的作用域
因此,console.log(a)
可以存取到 a
的值
function test(){
if(10 > 5){
var a = 10
}
console.log(a)
}
test()
// output: 10
例如:
用 let 宣告變數
我是在 if 的區塊(block)內,用 let 宣告變數 a
,因此變數 a
的作用域就只會在 if 這個 block 內而已:
if(10 > 5){
let a = 10
}
所以,在 if 區塊外面的 console.log(a)
就會出現錯誤:a is not defined
function test(){
if(10 > 5){
let a = 10
}
console.log(a)
}
test()
用 const 宣告變數
我是在 if 的區塊(block)內,用 const 宣告變數 a
,因此變數 a
的作用域就只會在 if 這個 block 內而已
所以,在 if 區塊外面的 console.log(a)
就會出現錯誤:a is not defined
function test(){
if(10 > 5){
const a = 10
}
console.log(a)
}
test()
const PI = 3.14
在 ES5 時,使用 string 會遇到幾個問題:
有多行字串時,無法直接換行(下面這樣寫,是會出錯的)
let str = '
hello
I
love
you
'
字串要換行就要用這種「字串拼接」的方式,很麻煩
let str =
'hello' + '\n' +
'I' + '\n' +
'love' + '\n' +
'you'
console.log(str)
output:
hello
I
love
you
單引號、加號如果放錯位置,就很容易造成語法錯誤,這樣很麻煩
function sayHi(name){
console.log('Hello, ' + name + '! It is ' + new Date() + ' now.')
}
sayHi('Dan')
// output: Hello, Dan! It is Thu Jul 09 2020 15:28:20 GMT+0800 (Taipei Standard Time) now.
``
來寫字串用 ``
這個反引號來包住字串,字串就可以直接換行了
let str = `
hello
I
love
you
`
console.log(str)
output:
hello
I
love
you
${ }
插入 JS 程式碼,好方便在 ${ }
裡面,可以放 JavaScript 的程式碼,最常放在裡面的就是「JavaScript 的變數」
function sayHi(name){
console.log(`Hello, ${name}! It is ${new Date()} now.`)
}
sayHi('Dan')
// output: Hello, Dan! It is Thu Jul 09 2020 15:39:22 GMT+0800 (Taipei Standard Time) now.
也可以直接在 ${ }
裡面執行一些 function,例如: ${name.toUpperCase()}
就會把 name.toUpperCase()
的回傳值,放到字串裡面
function sayHi(name){
console.log(`Hello, ${name.toUpperCase()}! It is ${new Date()} now.`)
}
sayHi('Daniel')
// output: Hello, DANIEL! It is Thu Jul 09 2020 15:43:35 GMT+0800 (Taipei Standard Time) now.
]]>ECMAScript 就是一個「標準、規範」,開發者可以根據這個規範去開發程式語言
而 JavaScript 這個程式語言就是根據 ECMAScript 去實作的
const 的全名是 constant(常數),是「不能改變」的
使用時機:
有些變數,我已經知道它是不會變的,就可以用 const 來宣告
例如:
我用 const 宣告變數 b
(是一個 number,屬於 primitive type),變數裡面是直接存「值」
因此,變數 b
的「值」是不能改變的
如果我把變數 b
的值改成 20
console.log(b)
就會出現錯誤:Assignment to constant variable.(你不可以改變一個 constant variable)
const b = 9
b = 20
console.log(b)
(object 可以是物件或陣列)
例如:
我用 const 宣告變數 b
(是一個物件,屬於 object),變數裡面存的是「記憶體位置」
因此,我用 b.number = 30
修改 number 的 value
結果發現是可以更改的!
const b = {
number: 10
}
b.number = 30
console.log(b)
// output: { number: 30 }
原因是:
「用 const 宣告的變數,不能改變」,對於 object 來說是指:「物件/陣列裡面存的 “記憶體位置” 不能改變」,如果只是更改 b.number = 30
並沒有改動到記憶體位置,b.number = 30
只是去改「那個記憶體位置上的物件的 value」而已,因此是可以更改的!(用這種 .
的方式去修改物件裡面的值)
b
,那就不行了,因為新的物件會是「新的記憶體位置」例如:
下面這樣寫就會出現錯誤:Assignment to constant variable(你不可以改變一個 constant variable)
const b = {
number: 10
}
b = {
number: 99
}
console.log(b)
要了解 var 跟 let, const 的差別,首先要了解「作用域」的概念:
作用域,就是「變數的生存範圍」
例如:
範例一:在 function 外面宣告變數 a
變數 a
是「全域變數」
因為我在外層宣告的變數 a
是在「檔案的這層」,所以,如果 test
function 裡面沒有宣告 a
,它就會往上層找,直到找到 a
為止,因此就會找到 var a = 20
var a = 20
function test(){
console.log(a)
}
test()
// output: 20
範例二:function 裡面有宣告變數 a
因為在 test
function 裡面就有宣告 a
,所以它在 function 裡面就找到 a
了
var a = 20
function test(){
var a = 99
console.log(a)
}
test()
// output: 99
因為我是在 test
function 裡面宣告變數 a
,所以變數 a
的作用域就只存在 test
function 裡面
變數 a
是「區域變數」
在 test
function 外面是無法使用變數 a 的
因此,console.log(a)
就會出現錯誤:a is not defined
function test(){
var a = 20
}
test()
console.log(a)
例如:
雖然我是在 if 的區塊(block)宣告變數 a
,但只要是在 test
function 裡面,就都是變數 a
的作用域
因此,console.log(a)
可以存取到 a
的值
function test(){
if(10 > 5){
var a = 10
}
console.log(a)
}
test()
// output: 10
例如:
用 let 宣告變數
我是在 if 的區塊(block)內,用 let 宣告變數 a
,因此變數 a
的作用域就只會在 if 這個 block 內而已:
if(10 > 5){
let a = 10
}
所以,在 if 區塊外面的 console.log(a)
就會出現錯誤:a is not defined
function test(){
if(10 > 5){
let a = 10
}
console.log(a)
}
test()
用 const 宣告變數
我是在 if 的區塊(block)內,用 const 宣告變數 a
,因此變數 a
的作用域就只會在 if 這個 block 內而已
所以,在 if 區塊外面的 console.log(a)
就會出現錯誤:a is not defined
function test(){
if(10 > 5){
const a = 10
}
console.log(a)
}
test()
const PI = 3.14
在 ES5 時,使用 string 會遇到幾個問題:
有多行字串時,無法直接換行(下面這樣寫,是會出錯的)
let str = '
hello
I
love
you
'
字串要換行就要用這種「字串拼接」的方式,很麻煩
let str =
'hello' + '\n' +
'I' + '\n' +
'love' + '\n' +
'you'
console.log(str)
output:
hello
I
love
you
單引號、加號如果放錯位置,就很容易造成語法錯誤,這樣很麻煩
function sayHi(name){
console.log('Hello, ' + name + '! It is ' + new Date() + ' now.')
}
sayHi('Dan')
// output: Hello, Dan! It is Thu Jul 09 2020 15:28:20 GMT+0800 (Taipei Standard Time) now.
``
來寫字串用 ``
這個反引號來包住字串,字串就可以直接換行了
let str = `
hello
I
love
you
`
console.log(str)
output:
hello
I
love
you
${ }
插入 JS 程式碼,好方便在 ${ }
裡面,可以放 JavaScript 的程式碼,最常放在裡面的就是「JavaScript 的變數」
function sayHi(name){
console.log(`Hello, ${name}! It is ${new Date()} now.`)
}
sayHi('Dan')
// output: Hello, Dan! It is Thu Jul 09 2020 15:39:22 GMT+0800 (Taipei Standard Time) now.
也可以直接在 ${ }
裡面執行一些 function,例如: ${name.toUpperCase()}
就會把 name.toUpperCase()
的回傳值,放到字串裡面
function sayHi(name){
console.log(`Hello, ${name.toUpperCase()}! It is ${new Date()} now.`)
}
sayHi('Daniel')
// output: Hello, DANIEL! It is Thu Jul 09 2020 15:43:35 GMT+0800 (Taipei Standard Time) now.
]]>
console.log()
來做測試(最陽春的測試方式)自己想一些測試資料,也要記得測試一些 edge case
function repeat(str, times){
let result = ''
for(let i=0; i<times; i++){
result += str
}
return result
}
console.log(repeat('love', 3))
console.log(repeat('c!!', 5))
console.log(repeat('hey', 0))
console.log(repeat('', 8))
output:
lovelovelove
c!!c!!c!!c!!c!!
但是,用這樣 console.log(repeat('love', 3))
的方式來寫的話,比較麻煩的是:印出結果後,我還要去 CLI 對照結果是否正確
因此,優化的方式是:
直接在 console.log()
裡面寫上答案,如果輸出的結果是 true,就代表是正確的
function repeat(str, times){
let result = ''
for(let i=0; i<times; i++){
result += str
}
return result
}
console.log(repeat('love', 3) === 'lovelovelove')
console.log(repeat('c!!', 5) === 'c!!c!!c!!c!!c!!')
console.log(repeat('hey', 0) === '')
console.log(repeat('', 8) === '')
output:
true
true
true
true
console.log()
做測試的缺點Jest 是一個由 Facebook 開發的前端 framework(是 open source 的),可以幫助我更方便、更有架構的完成「測試程式」的目標
用 Jest 做的測試又稱為 unit test(單元測試),較常用在測試一個一個的 function,確認每一個小的 unit(每個 function)都是正確的
(針對單一一個 function 做測試,來確認這個 function 的 input, output 是正確的)
安裝方式一:
按照官網上的指令 npm install --save-dev jest
就只會把 jest 安裝在「專案資料夾底下」
安裝方式二(較建議):
如果是輸入指令 npm install -g jest
,就可以把 jest 安裝在系統裡(參數 -g
代表 install jest globally)
「程式」跟「測試」會是分開的(用模組分開)
因此,在我的程式(也就是 repeat
函式)的下方,就會用 module.exports = repeat
做輸出
index.js:
function repeat(str, times){
let result = ''
for(let i=0; i<times; i++){
result += str
}
return result
}
module.exports = repeat
再新增一個檔案叫做 index.test.js
打開 index.test.js 之後,先把我的 index.js 引入進來,順便測試一下是否引入成功
var repeat = require('./index')
console.log(repeat('love', 3))
// output: lovelovelove
把 Jest 提供的測試模板貼過來 index.test.js 這裡
var repeat = require('./index')
// Jest 提供的模板
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
[模板解說]
Jest 會提供我一個叫做 test
的 funciton,裡面傳入兩個參數(把參數內容改寫成自己要的):
'adds 1 + 2 to equal 3'
要填入的是「描述」,也可以寫中文,例如:'love 重複 3 次應該要是 lovelovelove'
var repeat = require('./index')
test('adds 1 + 2 to equal 3', func);
把兩個參數都改寫成自己要的:
expect()
和 toBe()
也都是 Jest 提供的 function
repeat('love', 3)
放在 expect()
這個 function 裡面repeat('love', 3)
的回傳值(return),放在 toBe()
這個 function 裡面整句話的意思就是:我預期 repeat('love', 3)
的回傳值,應該要是 'lovelovelove'
var repeat = require('./index')
test('love 重複 3 次應該要是 lovelovelove', () => {
expect(repeat('love', 3)).toBe('lovelovelove');
});
到這裡,就寫完了我的測試!
回到 CLI 去執行 index.test.js 的測試,並不能輸入指令 node index.test.js
喔,因為這個不是 Node.js,它是 Jest,要輸入的指令是 jest
可以在 package.json 裡面的 scripts
預先寫好我想執行的指令 jest
:
"scripts": {
"start": "node index.js",
"test": "jest"
},
npm run test
,就會執行 test
(也就是 jest
)指令 jest
會自動去找「副檔名是 .test.js 的檔案」並且去跑測試
結果出現 PASS 就代表測試成功了!
假設,我只想要測試 index.test.js 這個檔案而已,
那就去改 package.json 裡面的 scripts
,在 jest
後面加上我想要測試的檔案名稱 index.test.js
"scripts": {
"start": "node index.js",
"test": "jest index.test.js"
},
接著在 CLI 執行指令 npm run test
時,就只會針對 index.test.js 這個檔案跑測試
jest index.test.js
直接在 CLI 執行?如果在安裝 jest 時,是按照官網上的指令 npm install --save-dev jest
,那就只會把 jest 安裝在「專案資料夾底下」
這時,為什麼如果把 jest index.test.js
直接在我的 CLI 執行,就會說「找不到 jest
指令」呢?
jest 並沒有用「globally」的方式安裝在我的電腦裡,jest 目前只有安裝在這個專案資料夾底下,因此電腦沒有設定一些東西去找到 jest
這個指令
如果是把 jest index.test.js
寫在這個專案的 package.json 的 scripts
裡面,再用 npm run test
去跑時,npm 會先去找這個專案資料夾底下的東西,發現有找到 jest
指令,因此就可以用 jest
去執行
但是,如果把 jest index.test.js
直接在系統(CLI)上面跑,就會從「系統」裡面找「是否有安裝 jest」,結果是找不到的,因為我就只有把 jest 安裝在專案底下而已
npx jest index.test.js
直接在 CLI 跑測試如果 npm 的版本比較新的話,會有附 npx
這個指令
就可以直接在 CLI 使用指令 npx jest index.test.js
來跑測試
如果前面有加 npx
的話,就會在「專案」裡面找「是否有安裝 jest」,找到後就會用 jest
指令去執行,就跟我「把 jest index.test.js
寫在 package.json 裡面」是一樣的意思
但如果我直接使用指令 jest index.test.js
,那就會從「系統」裡面找「我是否有安裝 jest」,但因為系統沒有裝 jest,所以就會找不到
可以寫很多個測試:
var repeat = require('./index')
test('love 重複 3 次應該要是 lovelovelove', () => {
expect(repeat('love', 3)).toBe('lovelovelove');
});
test('hey 重複 1 次應該要是 hey', () => {
expect(repeat('hey', 1)).toBe('hey');
});
test(' "" 重複 5 次應該要是 "" ', () => {
expect(repeat('', 5)).toBe('');
});
describe()
函式把 test cases 組合起來但是,目前這幾個 test cases 滿分散的,可以改成這樣更有結構的寫法:
在 index.test.js 用一個 describe()
函式把 test cases 組合起來,describe()
函式一樣是傳入兩個參數:
'測試 repeat'
是一個字串,可以填入「測試的名稱」var repeat = require('./index')
describe('測試 repeat', function(){
test('love 重複 3 次應該要是 lovelovelove', () => {
expect(repeat('love', 3)).toBe('lovelovelove');
});
test('hey 重複 1 次應該要是 hey', () => {
expect(repeat('hey', 1)).toBe('hey');
});
test(' "" 重複 5 次應該要是 "" ', () => {
expect(repeat('', 5)).toBe('');
});
})
接著,一樣在 CLI 執行 npx jest index.test.js
,就可以跑出測試結果了
TDD 的全名是 Test-driven Development(測試驅動開發)
一般的開發流程是:
先寫好 function > 再寫測試
TDD 的開發流程是:
先把測試寫好 > 再來寫 function
假設,我現在需要一個 function 可以幫我把字串倒過來(把函式取名叫做 reverse
)
在 index.js 先把 reverse
函式的框架寫好
function reverse(str){
}
module.exports = reverse
index.test.js:
var reverse = require('./index')
describe('測試 reverse', function(){
test('abc reverse should be cba', () => {
expect(reverse('abc')).toBe('cba');
});
test('!!! reverse should be !!!', () => {
expect(reverse('!!!')).toBe('!!!');
});
test(' "" reverse should be "" ', () => {
expect(reverse('')).toBe('');
});
})
reverse
函式實作出來邊開發邊測試,直到測試 pass 為止
function reverse(str){
let result = ''
for(let i=str.length-1; i>=0; i--){
result += str[i]
}
return result
}
module.exports = reverse
]]>console.log()
來做測試(最陽春的測試方式)自己想一些測試資料,也要記得測試一些 edge case
function repeat(str, times){
let result = ''
for(let i=0; i<times; i++){
result += str
}
return result
}
console.log(repeat('love', 3))
console.log(repeat('c!!', 5))
console.log(repeat('hey', 0))
console.log(repeat('', 8))
output:
lovelovelove
c!!c!!c!!c!!c!!
但是,用這樣 console.log(repeat('love', 3))
的方式來寫的話,比較麻煩的是:印出結果後,我還要去 CLI 對照結果是否正確
因此,優化的方式是:
直接在 console.log()
裡面寫上答案,如果輸出的結果是 true,就代表是正確的
function repeat(str, times){
let result = ''
for(let i=0; i<times; i++){
result += str
}
return result
}
console.log(repeat('love', 3) === 'lovelovelove')
console.log(repeat('c!!', 5) === 'c!!c!!c!!c!!c!!')
console.log(repeat('hey', 0) === '')
console.log(repeat('', 8) === '')
output:
true
true
true
true
console.log()
做測試的缺點Jest 是一個由 Facebook 開發的前端 framework(是 open source 的),可以幫助我更方便、更有架構的完成「測試程式」的目標
用 Jest 做的測試又稱為 unit test(單元測試),較常用在測試一個一個的 function,確認每一個小的 unit(每個 function)都是正確的
(針對單一一個 function 做測試,來確認這個 function 的 input, output 是正確的)
安裝方式一:
按照官網上的指令 npm install --save-dev jest
就只會把 jest 安裝在「專案資料夾底下」
安裝方式二(較建議):
如果是輸入指令 npm install -g jest
,就可以把 jest 安裝在系統裡(參數 -g
代表 install jest globally)
「程式」跟「測試」會是分開的(用模組分開)
因此,在我的程式(也就是 repeat
函式)的下方,就會用 module.exports = repeat
做輸出
index.js:
function repeat(str, times){
let result = ''
for(let i=0; i<times; i++){
result += str
}
return result
}
module.exports = repeat
再新增一個檔案叫做 index.test.js
打開 index.test.js 之後,先把我的 index.js 引入進來,順便測試一下是否引入成功
var repeat = require('./index')
console.log(repeat('love', 3))
// output: lovelovelove
把 Jest 提供的測試模板貼過來 index.test.js 這裡
var repeat = require('./index')
// Jest 提供的模板
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
[模板解說]
Jest 會提供我一個叫做 test
的 funciton,裡面傳入兩個參數(把參數內容改寫成自己要的):
'adds 1 + 2 to equal 3'
要填入的是「描述」,也可以寫中文,例如:'love 重複 3 次應該要是 lovelovelove'
var repeat = require('./index')
test('adds 1 + 2 to equal 3', func);
把兩個參數都改寫成自己要的:
expect()
和 toBe()
也都是 Jest 提供的 function
repeat('love', 3)
放在 expect()
這個 function 裡面repeat('love', 3)
的回傳值(return),放在 toBe()
這個 function 裡面整句話的意思就是:我預期 repeat('love', 3)
的回傳值,應該要是 'lovelovelove'
var repeat = require('./index')
test('love 重複 3 次應該要是 lovelovelove', () => {
expect(repeat('love', 3)).toBe('lovelovelove');
});
到這裡,就寫完了我的測試!
回到 CLI 去執行 index.test.js 的測試,並不能輸入指令 node index.test.js
喔,因為這個不是 Node.js,它是 Jest,要輸入的指令是 jest
可以在 package.json 裡面的 scripts
預先寫好我想執行的指令 jest
:
"scripts": {
"start": "node index.js",
"test": "jest"
},
npm run test
,就會執行 test
(也就是 jest
)指令 jest
會自動去找「副檔名是 .test.js 的檔案」並且去跑測試
結果出現 PASS 就代表測試成功了!
假設,我只想要測試 index.test.js 這個檔案而已,
那就去改 package.json 裡面的 scripts
,在 jest
後面加上我想要測試的檔案名稱 index.test.js
"scripts": {
"start": "node index.js",
"test": "jest index.test.js"
},
接著在 CLI 執行指令 npm run test
時,就只會針對 index.test.js 這個檔案跑測試
jest index.test.js
直接在 CLI 執行?如果在安裝 jest 時,是按照官網上的指令 npm install --save-dev jest
,那就只會把 jest 安裝在「專案資料夾底下」
這時,為什麼如果把 jest index.test.js
直接在我的 CLI 執行,就會說「找不到 jest
指令」呢?
jest 並沒有用「globally」的方式安裝在我的電腦裡,jest 目前只有安裝在這個專案資料夾底下,因此電腦沒有設定一些東西去找到 jest
這個指令
如果是把 jest index.test.js
寫在這個專案的 package.json 的 scripts
裡面,再用 npm run test
去跑時,npm 會先去找這個專案資料夾底下的東西,發現有找到 jest
指令,因此就可以用 jest
去執行
但是,如果把 jest index.test.js
直接在系統(CLI)上面跑,就會從「系統」裡面找「是否有安裝 jest」,結果是找不到的,因為我就只有把 jest 安裝在專案底下而已
npx jest index.test.js
直接在 CLI 跑測試如果 npm 的版本比較新的話,會有附 npx
這個指令
就可以直接在 CLI 使用指令 npx jest index.test.js
來跑測試
如果前面有加 npx
的話,就會在「專案」裡面找「是否有安裝 jest」,找到後就會用 jest
指令去執行,就跟我「把 jest index.test.js
寫在 package.json 裡面」是一樣的意思
但如果我直接使用指令 jest index.test.js
,那就會從「系統」裡面找「我是否有安裝 jest」,但因為系統沒有裝 jest,所以就會找不到
可以寫很多個測試:
var repeat = require('./index')
test('love 重複 3 次應該要是 lovelovelove', () => {
expect(repeat('love', 3)).toBe('lovelovelove');
});
test('hey 重複 1 次應該要是 hey', () => {
expect(repeat('hey', 1)).toBe('hey');
});
test(' "" 重複 5 次應該要是 "" ', () => {
expect(repeat('', 5)).toBe('');
});
describe()
函式把 test cases 組合起來但是,目前這幾個 test cases 滿分散的,可以改成這樣更有結構的寫法:
在 index.test.js 用一個 describe()
函式把 test cases 組合起來,describe()
函式一樣是傳入兩個參數:
'測試 repeat'
是一個字串,可以填入「測試的名稱」var repeat = require('./index')
describe('測試 repeat', function(){
test('love 重複 3 次應該要是 lovelovelove', () => {
expect(repeat('love', 3)).toBe('lovelovelove');
});
test('hey 重複 1 次應該要是 hey', () => {
expect(repeat('hey', 1)).toBe('hey');
});
test(' "" 重複 5 次應該要是 "" ', () => {
expect(repeat('', 5)).toBe('');
});
})
接著,一樣在 CLI 執行 npx jest index.test.js
,就可以跑出測試結果了
TDD 的全名是 Test-driven Development(測試驅動開發)
一般的開發流程是:
先寫好 function > 再寫測試
TDD 的開發流程是:
先把測試寫好 > 再來寫 function
假設,我現在需要一個 function 可以幫我把字串倒過來(把函式取名叫做 reverse
)
在 index.js 先把 reverse
函式的框架寫好
function reverse(str){
}
module.exports = reverse
index.test.js:
var reverse = require('./index')
describe('測試 reverse', function(){
test('abc reverse should be cba', () => {
expect(reverse('abc')).toBe('cba');
});
test('!!! reverse should be !!!', () => {
expect(reverse('!!!')).toBe('!!!');
});
test(' "" reverse should be "" ', () => {
expect(reverse('')).toBe('');
});
})
reverse
函式實作出來邊開發邊測試,直到測試 pass 為止
function reverse(str){
let result = ''
for(let i=str.length-1; i>=0; i--){
result += str[i]
}
return result
}
module.exports = reverse
]]>
在開發時,有時候會想要找一些別人已經寫好的 open source 功能來使用,就可以透過 npm 來安裝這些套件、library
在裝 node.js 的時候,預設就會直接把 npm 一起安裝好
輸入指令 npm -v
,如果有出現版本號,就代表有成功安裝 npm
library 跟 module 跟 package 其實都是差不多的東西
其實 left-pad 這個 module 發生過一些很有名的事情,因為很多有名的 library 都有用到 left-pad,之前作者把這個 module 從 npm 上面拿掉,造成了其他使用它的 module 都沒辦法安裝,詳情可參考:如何看待 Azer Koçulu 刪除了自己的所有 npm 庫? 或是 抽掉 11 行程式就讓網路大崩塌!一場撞名事件,看開源的威力與權力衝突。
假設,我想在我的專案資料夾中,使用 pad-left 這個套件
npm init
來產生 package.json 檔案package.json 這個檔案是用來「描述我這個專案」的
json 格式跟物件差不多,只是 json 的 key 一定要用「雙引號」包起來:
"main"
是「入口檔案」的意思"dependencies"
是「依賴」的意思:我有裝了哪些套件{
"name": "test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"pad-left": "^2.1.0"
}
}
npm install pad-left
或是 npm i pad-left
都可以i
就是 install
的簡寫而已)補充資訊:
從 npm 5 以後,
--save
就已經是預設的選項了,所以npm install pad-left
不加--save
也是可以的,一樣會把 dependencies 的資訊寫到 package.json 裡面去
這時,專案資料夾裡面就多了兩個東西:
var padLeft = require('pad-left')
來引入 module這裡的 module 名稱只需要寫 'pad-left' 就可以了
原因是:
require 很智慧,會去各個地方找 pad-left 這個 module
./
,就代表要在「同一個資料夾底下找」./
,那就會先去「系統(Node.js)內找」,如果系統沒有提供 pad-left 這個 module,那就會去 node_modules 資料夾找因此,就可以在 node_modules 資料夾找到 pad-left 這個 module 了
var padLeft = require('pad-left')
console.log(padLeft(789, 10, '0'))
// output: 0000000789
在 package.json 裡面,有一個區塊是 "scripts"
:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
在 "scripts"
可以先寫好一些指令,當我用 npm 執行 test
指令時,就會幫我執行 echo \"Error: no test specified\" && exit 1
這行指令
例如:
當我要執行一個檔案時,我就會輸入指令 node index.js
但是當專案變大時,可能會有很多個 js 檔案(index1.js, index2.js, home.js, main.js 等等)
每個 js 檔案看起來都很像入口點(很像是我應該要執行的檔案),我不知道是要執行哪一個,這時該怎麼辦?
方法一:
在 package.json 的 "main"
先寫好「入口檔案」
"main": "index.js",
方法二:
在 "scripts"
寫上這行 "start": "node index.js"
意思就是:
當我執行 start
指令時,npm 就會幫我執行 node index.js
這個指令
start
就像是一個「別名」
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
所以,我就可以輸入指令 npm run start
意思就是:我要用 npm 來執行 start
這個指令
就會幫我執行 node index.js
yarn 是由 Facebook 開發的,可以把它想成是一個更新、更快速的 npm,因此,通常比較新的專案都會使用 yarn
安裝好 yarn 之後,使用指令 yarn -v
就可以看到版本(就代表我有安裝成功)
接著,要如何使用 yarn 呢?
假設,我現在從 GitHub 上面 clone 下來一個專案(裡面不會有 node_modules,只會有 package.json 檔案)
第一步:我就必須先執行指令 yarn
或是 yarn install
(就等同於 npm install
的作用),yarn 就會把 package.json 的 dependencies 有寫到的所有套件(node_modules)給安裝進來
在 npm 要安裝新的套件,要使用指令 npm install pad-left
如果是用 yarn 安裝新的套件,就要使用指令 yarn add pad-left
,同時也會把 pad-left 套件寫進 package.json 的 dependencies 裡面
scripts
用 npm 執行 scripts
,是使用指令 npm run start
如果是用 yarn 執行 scripts
的話:
使用指令 yarn run start
就可以執行 package.json 裡面 scripts
的 start
指令,也就是 node index.js
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
]]>在開發時,有時候會想要找一些別人已經寫好的 open source 功能來使用,就可以透過 npm 來安裝這些套件、library
在裝 node.js 的時候,預設就會直接把 npm 一起安裝好
輸入指令 npm -v
,如果有出現版本號,就代表有成功安裝 npm
library 跟 module 跟 package 其實都是差不多的東西
其實 left-pad 這個 module 發生過一些很有名的事情,因為很多有名的 library 都有用到 left-pad,之前作者把這個 module 從 npm 上面拿掉,造成了其他使用它的 module 都沒辦法安裝,詳情可參考:如何看待 Azer Koçulu 刪除了自己的所有 npm 庫? 或是 抽掉 11 行程式就讓網路大崩塌!一場撞名事件,看開源的威力與權力衝突。
假設,我想在我的專案資料夾中,使用 pad-left 這個套件
npm init
來產生 package.json 檔案package.json 這個檔案是用來「描述我這個專案」的
json 格式跟物件差不多,只是 json 的 key 一定要用「雙引號」包起來:
"main"
是「入口檔案」的意思"dependencies"
是「依賴」的意思:我有裝了哪些套件{
"name": "test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"pad-left": "^2.1.0"
}
}
npm install pad-left
或是 npm i pad-left
都可以i
就是 install
的簡寫而已)補充資訊:
從 npm 5 以後,
--save
就已經是預設的選項了,所以npm install pad-left
不加--save
也是可以的,一樣會把 dependencies 的資訊寫到 package.json 裡面去
這時,專案資料夾裡面就多了兩個東西:
var padLeft = require('pad-left')
來引入 module這裡的 module 名稱只需要寫 'pad-left' 就可以了
原因是:
require 很智慧,會去各個地方找 pad-left 這個 module
./
,就代表要在「同一個資料夾底下找」./
,那就會先去「系統(Node.js)內找」,如果系統沒有提供 pad-left 這個 module,那就會去 node_modules 資料夾找因此,就可以在 node_modules 資料夾找到 pad-left 這個 module 了
var padLeft = require('pad-left')
console.log(padLeft(789, 10, '0'))
// output: 0000000789
在 package.json 裡面,有一個區塊是 "scripts"
:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
在 "scripts"
可以先寫好一些指令,當我用 npm 執行 test
指令時,就會幫我執行 echo \"Error: no test specified\" && exit 1
這行指令
例如:
當我要執行一個檔案時,我就會輸入指令 node index.js
但是當專案變大時,可能會有很多個 js 檔案(index1.js, index2.js, home.js, main.js 等等)
每個 js 檔案看起來都很像入口點(很像是我應該要執行的檔案),我不知道是要執行哪一個,這時該怎麼辦?
方法一:
在 package.json 的 "main"
先寫好「入口檔案」
"main": "index.js",
方法二:
在 "scripts"
寫上這行 "start": "node index.js"
意思就是:
當我執行 start
指令時,npm 就會幫我執行 node index.js
這個指令
start
就像是一個「別名」
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
所以,我就可以輸入指令 npm run start
意思就是:我要用 npm 來執行 start
這個指令
就會幫我執行 node index.js
yarn 是由 Facebook 開發的,可以把它想成是一個更新、更快速的 npm,因此,通常比較新的專案都會使用 yarn
安裝好 yarn 之後,使用指令 yarn -v
就可以看到版本(就代表我有安裝成功)
接著,要如何使用 yarn 呢?
假設,我現在從 GitHub 上面 clone 下來一個專案(裡面不會有 node_modules,只會有 package.json 檔案)
第一步:我就必須先執行指令 yarn
或是 yarn install
(就等同於 npm install
的作用),yarn 就會把 package.json 的 dependencies 有寫到的所有套件(node_modules)給安裝進來
在 npm 要安裝新的套件,要使用指令 npm install pad-left
如果是用 yarn 安裝新的套件,就要使用指令 yarn add pad-left
,同時也會把 pad-left 套件寫進 package.json 的 dependencies 裡面
scripts
用 npm 執行 scripts
,是使用指令 npm run start
如果是用 yarn 執行 scripts
的話:
使用指令 yarn run start
就可以執行 package.json 裡面 scripts
的 start
指令,也就是 node index.js
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
]]>
(Module 中國翻譯叫做“模塊”)
Module 就是「一個功能」
如果沒有 Module 的概念,那就會像這樣:把所有功能都放在一起
壞處是:每個功能之間依賴性太高,變得很難維護(改一個就動到另一個)
因此比較好的做法是「模組化」把功能切分開來:
最後用一個主程式,把每個 module 連接起來
在實務上,會把一些共同會用到的 function 抽出來放到一個叫做 utils.js 的檔案裡面,在其他各個地方要用到這些 function 時,再 import 進來
好處是:
因為 function 都只會放在 utils.js 這個檔案裡面
一個 function 的可重複使用性很高,且當我想要修改 function 時,就只要去 utils.js 裡面改就可以了
Node.js 有提供一些 module 可以使用:
像是其中一個叫做 OS
透過這些 module 可以拿到一些跟作業系統有關的資訊
module 使用方式如下:
os
(變數名稱可以自己取),用 require
來引入 OS 這個 moduleos
變數就代表「OS 這個 module」var os = require('os')
現在,就可以開始使用 OS 模組了
例如:
使用其中一個 function 叫做 os.platform() 會回傳我的作業系統名稱
var os = require('os')
console.log(os.platform())
// output: darwin
darwin 是 Mac 核心的名稱
接下來要講的是:
自己要如何做出一個 module,然後讓別人或自己使用
作法如下:
function double(n){
return n * 2
}
module.exports = double
module.exports = double
先把這個 module 「輸出」:function double(n){
return n * 2
}
module.exports = double // 輸出 module
在 require 小括號裡面,在「module 檔案名稱」前面要加上 ./
(也就是檔案路徑:./
代表是在同一個資料夾底下),因為這是我自己寫的 module,而不是系統(Node.js)提供的,所以要加上檔案路徑
var myModule = require('./myModule.js')
module 檔案名稱的副檔名(.js)可以省略
var myModule = require('./myModule')
require 很聰明,如果沒寫副檔名的話,它就會自己去找有沒有 .js 的檔案
module.exports =
」後面接的東西,就會是 index.js「require」進來的東西(在此範例中,就是指 double 函式)
因此,如果我在 index.js 把 myModule 印出來,結果就會是「double 函式」
var myModule = require('./myModule.js')
console.log(myModule)
// output: [Function: double]
因為現在的 myModule
變數就代表「double 函式」
因此,myModule(6)
就等於是 double(6)
var myModule = require('./myModule')
console.log(myModule(6))
// output: 12
例如:我改成叫做 calc
var calc = require('./myModule')
console.log(calc(6))
// output: 12
例如:我在 myModule.js export 一個陣列 [7, 8, 9]
function double(n){
return n * 2
}
module.exports = [7, 8, 9] // 輸出 module
那在 index.js 就會引入這個 [ 7, 8, 9 ]
陣列
var myModule = require('./myModule')
console.log(myModule)
// output: [ 7, 8, 9 ]
通常,一個 module 裡面都會提供很多個 function,那要如何同時輸出這些 function 呢?
作法是:
obj
,裡面放入所有我要輸出的 function在物件 obj
裡面:
double
(也就是 double 函式)function double(n){
return n * 2
}
var obj = {
double: double,
triple: function(n){
return n * 3
}
}
module.exports = obj
先把 module 印出來看看,結果當然也會是一個物件:
var myModule = require('./myModule')
console.log(myModule)
// output: { double: [Function: double], triple: [Function: triple] }
用物件的 key 來取用 function:
var myModule = require('./myModule')
console.log(myModule.double(4), myModule.triple(7))
// output: 8 21
背後運作的原理是:
因為這時的變數 myModule
就等同於是 obj
物件,像是這樣
var myModule = {
double: double,
triple: function (n) {
return n * 3
}
}
console.log(myModule.double(4), myModule.triple(7))
exports.double = double
在 myModule.js 除了使用 module.exports = double
來輸出之外,Node.js 提供了第二種寫法是 exports.double = double
myModule.js:
exports
本身當作一個空物件 {}
,輸出的東西就是一個物件exports.
後面接的會是「物件的 key 名稱」(自己隨意取名)double
(就會是 hello 這個 key 的 value)exports.hello = double
index.js:
把 myModule 印出來一樣會是一個物件
* key 是 hello
* value 是 double 函式
```javascript=
var myModule = require('./myModule')
console.log(myModule)
// output: { hello: [Function: double] }
也可以再 export 另一個函式
myModule.js:
function double(n){
return n * 2
}
exports.hello = double
exports.triple = function(n){
return n * 3
}
把 exports
本身當作一個空物件 {}
所以在執行完 exports.hello = double
和 exports.triple = function(n){
return n * 3
}
之後,exports
物件就會像是這樣:
{
hello: double,
triple: function(n){
return n * 3
}
}
index.js:
把 myModule 印出來一樣會是一個物件,裡面有兩個 function
var myModule = require('./myModule')
console.log(myModule)
// output: { hello: [Function: double], triple: [Function] }
使用 module.exports = 789
的話,=
後面可以接任何東西(數字、字串、陣列、物件...都可以),也就是說:可以輸出任何東西(數字、字串、陣列、物件…都可以)
function double(n){
return n * 2
}
module.exports = 789
但如果是使用 exports.hello = double
的話,輸出的就一定會是一個「物件」
function double(n){
return n * 2
}
exports.hello = double
exports.triple = function(n){
return n * 3
}
]]>(Module 中國翻譯叫做“模塊”)
Module 就是「一個功能」
如果沒有 Module 的概念,那就會像這樣:把所有功能都放在一起
壞處是:每個功能之間依賴性太高,變得很難維護(改一個就動到另一個)
因此比較好的做法是「模組化」把功能切分開來:
最後用一個主程式,把每個 module 連接起來
在實務上,會把一些共同會用到的 function 抽出來放到一個叫做 utils.js 的檔案裡面,在其他各個地方要用到這些 function 時,再 import 進來
好處是:
因為 function 都只會放在 utils.js 這個檔案裡面
一個 function 的可重複使用性很高,且當我想要修改 function 時,就只要去 utils.js 裡面改就可以了
Node.js 有提供一些 module 可以使用:
像是其中一個叫做 OS
透過這些 module 可以拿到一些跟作業系統有關的資訊
module 使用方式如下:
os
(變數名稱可以自己取),用 require
來引入 OS 這個 moduleos
變數就代表「OS 這個 module」var os = require('os')
現在,就可以開始使用 OS 模組了
例如:
使用其中一個 function 叫做 os.platform() 會回傳我的作業系統名稱
var os = require('os')
console.log(os.platform())
// output: darwin
darwin 是 Mac 核心的名稱
接下來要講的是:
自己要如何做出一個 module,然後讓別人或自己使用
作法如下:
function double(n){
return n * 2
}
module.exports = double
module.exports = double
先把這個 module 「輸出」:function double(n){
return n * 2
}
module.exports = double // 輸出 module
在 require 小括號裡面,在「module 檔案名稱」前面要加上 ./
(也就是檔案路徑:./
代表是在同一個資料夾底下),因為這是我自己寫的 module,而不是系統(Node.js)提供的,所以要加上檔案路徑
var myModule = require('./myModule.js')
module 檔案名稱的副檔名(.js)可以省略
var myModule = require('./myModule')
require 很聰明,如果沒寫副檔名的話,它就會自己去找有沒有 .js 的檔案
module.exports =
」後面接的東西,就會是 index.js「require」進來的東西(在此範例中,就是指 double 函式)
因此,如果我在 index.js 把 myModule 印出來,結果就會是「double 函式」
var myModule = require('./myModule.js')
console.log(myModule)
// output: [Function: double]
因為現在的 myModule
變數就代表「double 函式」
因此,myModule(6)
就等於是 double(6)
var myModule = require('./myModule')
console.log(myModule(6))
// output: 12
例如:我改成叫做 calc
var calc = require('./myModule')
console.log(calc(6))
// output: 12
例如:我在 myModule.js export 一個陣列 [7, 8, 9]
function double(n){
return n * 2
}
module.exports = [7, 8, 9] // 輸出 module
那在 index.js 就會引入這個 [ 7, 8, 9 ]
陣列
var myModule = require('./myModule')
console.log(myModule)
// output: [ 7, 8, 9 ]
通常,一個 module 裡面都會提供很多個 function,那要如何同時輸出這些 function 呢?
作法是:
obj
,裡面放入所有我要輸出的 function在物件 obj
裡面:
double
(也就是 double 函式)function double(n){
return n * 2
}
var obj = {
double: double,
triple: function(n){
return n * 3
}
}
module.exports = obj
先把 module 印出來看看,結果當然也會是一個物件:
var myModule = require('./myModule')
console.log(myModule)
// output: { double: [Function: double], triple: [Function: triple] }
用物件的 key 來取用 function:
var myModule = require('./myModule')
console.log(myModule.double(4), myModule.triple(7))
// output: 8 21
背後運作的原理是:
因為這時的變數 myModule
就等同於是 obj
物件,像是這樣
var myModule = {
double: double,
triple: function (n) {
return n * 3
}
}
console.log(myModule.double(4), myModule.triple(7))
exports.double = double
在 myModule.js 除了使用 module.exports = double
來輸出之外,Node.js 提供了第二種寫法是 exports.double = double
myModule.js:
exports
本身當作一個空物件 {}
,輸出的東西就是一個物件exports.
後面接的會是「物件的 key 名稱」(自己隨意取名)double
(就會是 hello 這個 key 的 value)exports.hello = double
index.js:
把 myModule 印出來一樣會是一個物件
* key 是 hello
* value 是 double 函式
```javascript=
var myModule = require('./myModule')
console.log(myModule)
// output: { hello: [Function: double] }
也可以再 export 另一個函式
myModule.js:
function double(n){
return n * 2
}
exports.hello = double
exports.triple = function(n){
return n * 3
}
把 exports
本身當作一個空物件 {}
所以在執行完 exports.hello = double
和 exports.triple = function(n){
return n * 3
}
之後,exports
物件就會像是這樣:
{
hello: double,
triple: function(n){
return n * 3
}
}
index.js:
把 myModule 印出來一樣會是一個物件,裡面有兩個 function
var myModule = require('./myModule')
console.log(myModule)
// output: { hello: [Function: double], triple: [Function] }
使用 module.exports = 789
的話,=
後面可以接任何東西(數字、字串、陣列、物件...都可以),也就是說:可以輸出任何東西(數字、字串、陣列、物件…都可以)
function double(n){
return n * 2
}
module.exports = 789
但如果是使用 exports.hello = double
的話,輸出的就一定會是一個「物件」
function double(n){
return n * 2
}
exports.hello = double
exports.triple = function(n){
return n * 3
}
]]>
result
就會是「a
函式執行完的結果」
把「呼叫 a
函式的回傳值」,用一個變數 result
去接收
因為 a
函式的回傳值是 'A',因此變數 result
就會是 ‘A’
function a(){
console.log('呼叫 A')
return 'A'
}
var result = a()
console.log('result: ', result)
output:
因為 var result = a()
這行,會先執行 a()
:
console.log('呼叫 A')
就會印出「呼叫 A」return 'A'
,就會讓變數 result
等於 'A'然後再把變數 result
印出來:console.log('result: ', result)
就會是「result: A」
呼叫 A
result: A
雖然我在呼叫 a
函式時,傳了兩個參數進去(50, 890),但是因為 a
函式只接收「一個參數」,因此就只會傳入「第一個參數(50)」
function a(number){
console.log('呼叫 A', number)
return 'A'
}
a(50, 890)
// output: 呼叫 A 50
function a(number){
console.log('呼叫 A', number)
return 'A'
}
function main(){
var result = a(789)
console.log(result)
}
main()
執行順序是
執行 main()
:
main
函式的第一行 var result = a(789)
:a(789)
,參數 number
就會是 789:先執行 console.log('呼叫 A', number)
印出「呼叫 A 789」,再執行 return 'A'
回傳 'A'a(789)
執行完畢。
這時,變數 result
就接收到了 a(789)
回傳的 'A'
main
函式的第二行 console.log(result)
就會印出 ‘A’ 了output:
呼叫 A 789
A
上面的程式碼也可以這樣寫:
把變數 result
拿掉,直接寫 console.log(a(789))
,執行順序是:
a(789)
console.log('呼叫 A', number)
會印出「呼叫 A 789」return 'A'
回傳 'A'console.log(a(789))
,就會把 a(789)
的回傳值 'A' 給 log 出來function a(number){
console.log('呼叫 A', number)
return 'A'
}
function main(){
console.log(a(789))
}
main()
output:
執行結果跟剛剛是一樣的
呼叫 A 789
A
a
這個 function 印出來而已如果只有寫 console.log(a)
,因為我並沒有「呼叫 a
function」,所以就只會把 a
這個 function 印出來而已
function a(number){
console.log('呼叫 A', number)
return 'A'
}
function main(){
console.log(a)
}
main()
// output: [Function: a]
把 a
function 當作參數傳入 main
function
console.log(arg)
出來的結果就會是 a
這個 function
function a(number){
console.log('呼叫 A', number)
return 'A'
}
function main(arg){
console.log(arg)
}
main(a)
// output: [Function: a]
這裡的參數 arg
就是 a
function
因此 console.log(arg === a)
會是 true
function a(number){
console.log('呼叫 A', number)
return 'A'
}
function main(arg){
console.log(arg === a)
}
main(a)
// output: true
所以,就可以用 arg()
來呼叫 a
function
function a(number){
console.log('呼叫 A', number)
return 'A'
}
function main(arg){
arg() // arg() 就等同於是 a()
}
main(a)
// output: 呼叫 A undefined
function isPositive(n){
return n > 0
}
function isValid(number){
console.log('isValid')
console.log(isPositive(number))
}
isValid(100)
output:
isValid
true
改成下面這樣寫,跟上面做的事情是一模一樣的:
把 isPositive
function 當作參數傳入 isValid
function
:heavy_check_mark: 注意!isPositive
後面不加小括號,才是「把 isPositive
function 當作參數傳入」的意思
因此,
number
就會是 100fn
就會是 isPositive
function,所以就可以用 fn(number)
來呼叫 isPositive(number)
function isPositive(n){
return n > 0
}
function isValid(number, fn){
console.log('isValid')
console.log(fn(number))
}
isValid(100, isPositive)
output:
isValid
true
isPositive
函式當作參數傳入時,isPositive
函式不可以加上小括號執行下面的程式碼,會出現錯誤「fn is not a function」
錯誤的原因為:
isPositive()
就是「呼叫 isPositive
函式」的意思,因此 isValid(100, isPositive())
會先去呼叫 isPositive
函式,而因為 isPositive()
沒有傳入參數,所以 n 是 undefined,因此 return n > 0
就會回傳 false--> isPositive()
執行完會回傳 false
因此,isValid(100, isPositive())
就等於是 isValid(100, false)
,參數 fn
會被帶入 false,因此就會出現那句錯誤「fn is not a function」
isValid(100, isPositive())
是錯誤的寫法
function isPositive(n){
return n > 0
}
function isValid(number, fn){
console.log(fn(number))
}
isValid(100, isPositive())
要當作參數的 function,可以不用先寫好
isValid
函式傳參數時,才宣告 function,這個 function 要不要加上名稱都可以function isPositive(n){
return n > 0
}
function isValid(number, fn){
console.log('isValid')
console.log(fn(number))
}
// 先把 function 宣告好,再做為參數傳入
isValid(100, isPositive)
// 在傳參數時,才宣告 function
isValid(100, function(n){
return n > 0
})
output:
isValid
true
isValid
true
map()
內建函式的用法map()
的小括號裡面可以傳入一個 function,map()
會去執行這個 function,並且把陣列裡面的每個元素都當作參數依序傳入
function isNegative(n){
console.log('呼叫 isNegative', n)
return n < 0
}
let arr = [-3, 7, 8, 9, -20]
let arr2 = arr.map(isNegative)
console.log(arr2)
output:
呼叫 isNegative -3
呼叫 isNegative 7
呼叫 isNegative 8
呼叫 isNegative 9
呼叫 isNegative -20
[ true, false, false, false, true ]
map()
小括號裡面的 function,可以傳入三個參數:currentValue, index, array
為什麼要傳入這三個參數?
沒有原因,這就是規定好的
currentValue
,也就是「陣列裡的每個元素」let arr = [-3, 7, 8, 9, -20]
arr.map(function(currentValue){
console.log(currentValue)
})
output:
-3
7
8
9
-20
index
,也就是「index 值」array
,也就是「整個陣列」let arr = [-3, 7, 8, 9, -20]
arr.map(function(currentValue, index, array){
console.log(currentValue, index, array)
})
output:
-3 0 [ -3, 7, 8, 9, -20 ]
7 1 [ -3, 7, 8, 9, -20 ]
8 2 [ -3, 7, 8, 9, -20 ]
9 3 [ -3, 7, 8, 9, -20 ]
-20 4 [ -3, 7, 8, 9, -20 ]
map()
就是長這樣:從實作可以看到:
map()
就是會幫我呼叫「我傳進去的 function」
function 裡面就是會帶入三個參數
function map(arr, fn){
let result = []
for(let i=0; i<arr.length; i++){
result[i] = fn(arr[i], i, arr)
}
return result
}
用 console.log(getData())
把 getData()
的回傳值印出來
function getData(){
return {
data: 'I love you'
}
}
console.log(getData())
// output: { data: 'I love you' }
上面的程式碼,可以用另一種寫法:
執行 getData()
就會呼叫 handleResult
函式
結果是一模一樣的
function handleResult(result){
console.log(result)
}
function getData(){
handleResult({
data: 'I love you'
})
}
getData()
// output: { data: 'I love you' }
上面的程式碼,又可以用另一種寫法:
此時 getData(fn)
的 fn
就會是 handleResult
,因此,執行 fn()
就等於是執行 handleResult()
function handleResult(result){
console.log(result)
}
function getData(fn){
fn({
data: 'I love you'
})
}
getData(handleResult)
// output: { data: 'I love you' }
上面的程式碼,又可以用另一種寫法:
在傳入 getData
函式時,才宣告 function(匿名函式)
function getData(fn){
fn({
data: 'I love you'
})
}
getData(function (result) {
console.log(result)
})
// output: { data: 'I love you' }
上面的程式碼,又可以用另一種寫法:
function getData(fn){
fn({
data: 'I love you'
})
}
getData(result => {
console.log(result)
})
// output: { data: 'I love you' }
function 傳參數只看順序(不管名稱),因此,參數要取什麼名字都可以
function getData(fn){
const data = {
data: 'I love you'
}
fn(data)
}
getData(abc => {
console.log(abc)
})
// output: { data: 'I love you' }
]]>result
就會是「a
函式執行完的結果」
把「呼叫 a
函式的回傳值」,用一個變數 result
去接收
因為 a
函式的回傳值是 'A',因此變數 result
就會是 ‘A’
function a(){
console.log('呼叫 A')
return 'A'
}
var result = a()
console.log('result: ', result)
output:
因為 var result = a()
這行,會先執行 a()
:
console.log('呼叫 A')
就會印出「呼叫 A」return 'A'
,就會讓變數 result
等於 'A'然後再把變數 result
印出來:console.log('result: ', result)
就會是「result: A」
呼叫 A
result: A
雖然我在呼叫 a
函式時,傳了兩個參數進去(50, 890),但是因為 a
函式只接收「一個參數」,因此就只會傳入「第一個參數(50)」
function a(number){
console.log('呼叫 A', number)
return 'A'
}
a(50, 890)
// output: 呼叫 A 50
function a(number){
console.log('呼叫 A', number)
return 'A'
}
function main(){
var result = a(789)
console.log(result)
}
main()
執行順序是
執行 main()
:
main
函式的第一行 var result = a(789)
:a(789)
,參數 number
就會是 789:先執行 console.log('呼叫 A', number)
印出「呼叫 A 789」,再執行 return 'A'
回傳 'A'a(789)
執行完畢。
這時,變數 result
就接收到了 a(789)
回傳的 'A'
main
函式的第二行 console.log(result)
就會印出 ‘A’ 了output:
呼叫 A 789
A
上面的程式碼也可以這樣寫:
把變數 result
拿掉,直接寫 console.log(a(789))
,執行順序是:
a(789)
console.log('呼叫 A', number)
會印出「呼叫 A 789」return 'A'
回傳 'A'console.log(a(789))
,就會把 a(789)
的回傳值 'A' 給 log 出來function a(number){
console.log('呼叫 A', number)
return 'A'
}
function main(){
console.log(a(789))
}
main()
output:
執行結果跟剛剛是一樣的
呼叫 A 789
A
a
這個 function 印出來而已如果只有寫 console.log(a)
,因為我並沒有「呼叫 a
function」,所以就只會把 a
這個 function 印出來而已
function a(number){
console.log('呼叫 A', number)
return 'A'
}
function main(){
console.log(a)
}
main()
// output: [Function: a]
把 a
function 當作參數傳入 main
function
console.log(arg)
出來的結果就會是 a
這個 function
function a(number){
console.log('呼叫 A', number)
return 'A'
}
function main(arg){
console.log(arg)
}
main(a)
// output: [Function: a]
這裡的參數 arg
就是 a
function
因此 console.log(arg === a)
會是 true
function a(number){
console.log('呼叫 A', number)
return 'A'
}
function main(arg){
console.log(arg === a)
}
main(a)
// output: true
所以,就可以用 arg()
來呼叫 a
function
function a(number){
console.log('呼叫 A', number)
return 'A'
}
function main(arg){
arg() // arg() 就等同於是 a()
}
main(a)
// output: 呼叫 A undefined
function isPositive(n){
return n > 0
}
function isValid(number){
console.log('isValid')
console.log(isPositive(number))
}
isValid(100)
output:
isValid
true
改成下面這樣寫,跟上面做的事情是一模一樣的:
把 isPositive
function 當作參數傳入 isValid
function
:heavy_check_mark: 注意!isPositive
後面不加小括號,才是「把 isPositive
function 當作參數傳入」的意思
因此,
number
就會是 100fn
就會是 isPositive
function,所以就可以用 fn(number)
來呼叫 isPositive(number)
function isPositive(n){
return n > 0
}
function isValid(number, fn){
console.log('isValid')
console.log(fn(number))
}
isValid(100, isPositive)
output:
isValid
true
isPositive
函式當作參數傳入時,isPositive
函式不可以加上小括號執行下面的程式碼,會出現錯誤「fn is not a function」
錯誤的原因為:
isPositive()
就是「呼叫 isPositive
函式」的意思,因此 isValid(100, isPositive())
會先去呼叫 isPositive
函式,而因為 isPositive()
沒有傳入參數,所以 n 是 undefined,因此 return n > 0
就會回傳 false--> isPositive()
執行完會回傳 false
因此,isValid(100, isPositive())
就等於是 isValid(100, false)
,參數 fn
會被帶入 false,因此就會出現那句錯誤「fn is not a function」
isValid(100, isPositive())
是錯誤的寫法
function isPositive(n){
return n > 0
}
function isValid(number, fn){
console.log(fn(number))
}
isValid(100, isPositive())
要當作參數的 function,可以不用先寫好
isValid
函式傳參數時,才宣告 function,這個 function 要不要加上名稱都可以function isPositive(n){
return n > 0
}
function isValid(number, fn){
console.log('isValid')
console.log(fn(number))
}
// 先把 function 宣告好,再做為參數傳入
isValid(100, isPositive)
// 在傳參數時,才宣告 function
isValid(100, function(n){
return n > 0
})
output:
isValid
true
isValid
true
map()
內建函式的用法map()
的小括號裡面可以傳入一個 function,map()
會去執行這個 function,並且把陣列裡面的每個元素都當作參數依序傳入
function isNegative(n){
console.log('呼叫 isNegative', n)
return n < 0
}
let arr = [-3, 7, 8, 9, -20]
let arr2 = arr.map(isNegative)
console.log(arr2)
output:
呼叫 isNegative -3
呼叫 isNegative 7
呼叫 isNegative 8
呼叫 isNegative 9
呼叫 isNegative -20
[ true, false, false, false, true ]
map()
小括號裡面的 function,可以傳入三個參數:currentValue, index, array
為什麼要傳入這三個參數?
沒有原因,這就是規定好的
currentValue
,也就是「陣列裡的每個元素」let arr = [-3, 7, 8, 9, -20]
arr.map(function(currentValue){
console.log(currentValue)
})
output:
-3
7
8
9
-20
index
,也就是「index 值」array
,也就是「整個陣列」let arr = [-3, 7, 8, 9, -20]
arr.map(function(currentValue, index, array){
console.log(currentValue, index, array)
})
output:
-3 0 [ -3, 7, 8, 9, -20 ]
7 1 [ -3, 7, 8, 9, -20 ]
8 2 [ -3, 7, 8, 9, -20 ]
9 3 [ -3, 7, 8, 9, -20 ]
-20 4 [ -3, 7, 8, 9, -20 ]
map()
就是長這樣:從實作可以看到:
map()
就是會幫我呼叫「我傳進去的 function」
function 裡面就是會帶入三個參數
function map(arr, fn){
let result = []
for(let i=0; i<arr.length; i++){
result[i] = fn(arr[i], i, arr)
}
return result
}
用 console.log(getData())
把 getData()
的回傳值印出來
function getData(){
return {
data: 'I love you'
}
}
console.log(getData())
// output: { data: 'I love you' }
上面的程式碼,可以用另一種寫法:
執行 getData()
就會呼叫 handleResult
函式
結果是一模一樣的
function handleResult(result){
console.log(result)
}
function getData(){
handleResult({
data: 'I love you'
})
}
getData()
// output: { data: 'I love you' }
上面的程式碼,又可以用另一種寫法:
此時 getData(fn)
的 fn
就會是 handleResult
,因此,執行 fn()
就等於是執行 handleResult()
function handleResult(result){
console.log(result)
}
function getData(fn){
fn({
data: 'I love you'
})
}
getData(handleResult)
// output: { data: 'I love you' }
上面的程式碼,又可以用另一種寫法:
在傳入 getData
函式時,才宣告 function(匿名函式)
function getData(fn){
fn({
data: 'I love you'
})
}
getData(function (result) {
console.log(result)
})
// output: { data: 'I love you' }
上面的程式碼,又可以用另一種寫法:
function getData(fn){
fn({
data: 'I love you'
})
}
getData(result => {
console.log(result)
})
// output: { data: 'I love you' }
function 傳參數只看順序(不管名稱),因此,參數要取什麼名字都可以
function getData(fn){
const data = {
data: 'I love you'
}
fn(data)
}
getData(abc => {
console.log(abc)
})
// output: { data: 'I love you' }
]]>
console.log()
來 debug 是一個快速又有效的方法
作法:
如果不確定 bug 發生在哪邊,
就在「每一行」或是「有疑惑的地方」,都加上 console.log()
,印出自己覺得有意義的資訊
然後,就可以看看 log 出來的結果,是不是跟自己預期的一樣
console.log()
來 debugconsole.log()
要加在哪些地方呢?
num
是多少,比較方便 debugconsole.log()
要加在 return 的前面,如果加在 return 後面是沒有用的,因為只要一執行到 return 就會跳出 functionfunction isPrime(num){
console.log('num: ', num)
if(num === 1) return false
if(num === 2) return true
for(let i=2; i<num; i++){
console.log('i: ', i)
if(num % i === 0){
console.log('num % i === 0', num, i) // 記得要把 console.log 加在 return 的前面
return false
} else{
console.log('else', num, i) // 記得要把 console.log 加在 return 的前面
return true
}
}
}
console.log(isPrime(15))
output:
num: 15
i: 2
else 15 2
true
看看 output,就會發現上方程式碼的問題出在哪裡了
function isPrime(num){
if(num === 1) return false
if(num === 2) return true
for(let i=2; i<num; i++){
if(num % i === 0) return false
}
return true
}
console.log(isPrime(15))
// output: false
]]>console.log()
來 debug 是一個快速又有效的方法
作法:
如果不確定 bug 發生在哪邊,
就在「每一行」或是「有疑惑的地方」,都加上 console.log()
,印出自己覺得有意義的資訊
然後,就可以看看 log 出來的結果,是不是跟自己預期的一樣
console.log()
來 debugconsole.log()
要加在哪些地方呢?
num
是多少,比較方便 debugconsole.log()
要加在 return 的前面,如果加在 return 後面是沒有用的,因為只要一執行到 return 就會跳出 functionfunction isPrime(num){
console.log('num: ', num)
if(num === 1) return false
if(num === 2) return true
for(let i=2; i<num; i++){
console.log('i: ', i)
if(num % i === 0){
console.log('num % i === 0', num, i) // 記得要把 console.log 加在 return 的前面
return false
} else{
console.log('else', num, i) // 記得要把 console.log 加在 return 的前面
return true
}
}
}
console.log(isPrime(15))
output:
num: 15
i: 2
else 15 2
true
看看 output,就會發現上方程式碼的問題出在哪裡了
function isPrime(num){
if(num === 1) return false
if(num === 2) return true
for(let i=2; i<num; i++){
if(num % i === 0) return false
}
return true
}
console.log(isPrime(15))
// output: false
]]>
除了 object (也就是陣列、物件)之外的東西,基本上都是 primitive types (原始型態)
屬於 primitive types(例如 number, string, boolean)的值就是 immutable values(不可變的值),也被稱為「primitive values」
str.toUpperCase()
,因為:如果只是呼叫 function,是絕對沒辦法改變 str
的值str.toUpperCase()
只會回傳新的字串,所以要再把新的字串重新賦予給另一個變數: var newStr = str.toUpperCase()
,newStr
才會是「轉成大寫後的字串」接下來,會用一些範例來說明 immutable values 的觀念
a 這個變數是「可以改變的」
但是 'hello' 這個字串是「不會改變的」
var a = 'hello'
a = 'tree'
意思是:
var a = 'hello'
一開始給 a 的值是 ‘hello’
'hello' 這個字串存放的「記憶體位置」假設是 0x01
var a = 'hello'
// a: 'hello' 0x01
a = 'tree'
當 a = 'tree'
時,並不是「把 0x01 上面的 ‘hello’」直接改成 'tree'
而是會把「新的字串 'tree'」用 =
重新賦予給變數 a (新的字串 ‘tree’ 會放在一個新的記憶體位置 0x02 上)
所以,變數 a 的值最後會是 'tree'
var a = 'hello'
// a: 'hello' 0x01
a = 'tree'
// a: 'tree' 0x02
a = a + 'tree'
var a = 'hello'
// a: 'hello' 0x01
a = a + 'tree'
// a: 'hellotree' 0x02
同樣地:
var a = 'hello'
:a = a + 'tree'
:a + 'tree'
,回傳一個新的字串 'hellotree',再把 'hellotree' 用 =
重新賦予給 a (新的字串 'hellotree' 會放在一個新的記憶體位置 0x02 上)a.toUpperCase()
錯誤的想法:會認為「只要呼叫 a.toUpperCase()
之後」,a 就會轉成大寫
但其實,只呼叫 a.toUpperCase()
並不會把 a 轉成大寫(不會改變 a 原本的值)
因為:
'hello' 這個字串本身是「不可變」的
所以,只要「變數 a 是屬於 primitive type(也就是 number, string, boolean)」,就絕對沒辦法用「呼叫 function 的方式」去改變「變數 a 原本的值」
a.toUpperCase()
的確會「return(回傳)」一個新的字串 'HELLO'但是!如果沒有把這個回傳的新字串 ‘HELLO’ 賦予給任何變數,是不會有任何事情發生的
var a = 'hello'
a.toUpperCase()
console.log(a)
// output: hello
a = a.toUpperCase()
讓 a.toUpperCase()
回傳一個新的字串 'HELLO',再用 =
重新賦予給變數 a (新的字串 ‘HELLO’,會在一個新的記憶體位置上)
var a = 'hello'
a = a.toUpperCase()
console.log(a)
// output: HELLO
var a = 'hello'
'hello' 這個字串所存放的記憶體位置,假設是 0x01
var a = 'hello'
// a: 'hello' 0x01
a = a.toUpperCase()
步驟一:
a.toUpperCase()
會「return(回傳)」一個新的字串 'HELLO'
但是!如果沒有把這個回傳的新字串 ‘HELLO’ 賦予給任何變數,是不會有任何事情發生的
步驟二: a = a.toUpperCase()
再把「新的字串 ‘HELLO’」,用 =
重新賦予給變數 a (新的字串 ‘HELLO’ 會在新的記憶體位置 0x02 上)
var a = 'hello'
// a: 'hello' 0x01
a = a.toUpperCase()
// a.toUpperCase(): 'HELLO'
// a: 'HELLO' 0x02
console.log(a)
var temp = a.toUpperCase()
延續上面的範例,也可以宣告一個新的變數 temp
,去接收 a.toUpperCase()
回傳的「新的字串 'HELLO'」
var a = 'hello'
// a: 'hello' 0x01
var temp = a.toUpperCase()
// a.toUpperCase(): 'HELLO'
// temp: 'HELLO' 0X02
console.log(temp)
// output: HELLO
在 change
函式可以傳一個字串進去
change
函式: change(a)
console.log(a)
var a = 'hello'
function change(str){
str = 'monday'
}
change(a)
console.log(a)
// output: hello
可以看到,最後印出來變數 a 的值依然還是 'hello',並不會被 change
函式變成 'monday'
原因也是一樣:只使用「呼叫 function」的方式,並沒辦法改變「變數 a 原本的值」
除非是「重新賦予給變數 a」一個新的值,變數 a 才會有新的值
參考資料 Array.prototype
就直接呼叫 function 即可,例如: arr.push(50)
不需要再用一個變數去接收 return 的值,例如這個錯誤寫法: arr = arr.push(50)
如果我本來就是想要改變「原本的 array」,所使用的函式就會直接改動「原本的 array」,基本上不會再回傳一個新的 array 了(會回傳別的東西)
push()
用 push()
會直接改變原本的陣列,並不會產生另一個新的陣列
var arr = [7 ,8, 9]
arr.push(50)
console.log(arr)
// output: [ 7, 8, 9, 50 ]
原因是:
push()
確實是真的去改變了在「原本記憶體位置」上面存的 [7 ,8, 9]
var arr = [7 ,8, 9]
存放 [7 ,8, 9]
的記憶體位置,假設是 0x01
var arr = [7 ,8, 9]
// arr: [7, 8, 9] 0x01
arr.push(50)
console.log(arr)
arr.push(50)
會直接在「0x01 這個記憶體位置」上,改變它存的 [7 ,8, 9]
,變成 [7, 8, 9, 50]
var arr = [7 ,8, 9]
// arr: [7, 8, 9, 50] 0x01
arr.push(50)
console.log(arr)
arr = arr.push(50)
是錯的!根本不需要再把 arr.push(50)
重新賦予給 arr
用這個錯誤寫法,印出來的值會不同--> 會印出 4
var arr = [7 ,8, 9]
arr = arr.push(50) // 錯誤寫法!!
console.log(arr)
// output: 4
因為 Array.prototype.push()
這個 method 會「在原本陣列的最後面新增一個或多個元素」,並且「return 原本陣列的新長度」
所以,回傳的「4」就是 arr
陣列 [7, 8, 9, 50]
的長度
arr = arr.push(50)
會做兩件事情:arr.push(50)
,把 arr
陣列變成 [7, 8, 9, 50]
arr.push(50)
這個 function 會回傳 [7, 8, 9, 50]
的長度,也就是 4,這個 4 會被重新賦予給變數 arr
所以,變數 arr
最後的值就變成 4 了
reverse()
var arr = [7 ,8, 9]
arr.reverse()
console.log(arr)
// output: [ 9, 8, 7 ]
arr = arr.reverse()
是錯誤寫法
但是,為什麼印出來的結果也會是 [ 9, 8, 7 ]
呢?
因為 arr.reverse()
除了直接改變原本的 array 之外,
也會 return 一個新的「順序倒過來的 array」
var arr = [7 ,8, 9]
arr = arr.reverse()
console.log(arr)
// output: [ 9, 8, 7 ]
通常來說,這些 function 回傳的值都不會是一個 array
因此,需要一個「變數」去接收這個 return 的值,例如: var result = arr.join('!')
join()
join()
不會改變原本的 arr
陣列
join()
回傳的值,會是一個 string
string 跟 array 是完全不同的東西,所以不可能去改動「原本的 array」變成 string,因此就用「回傳一個新的字串」的方式來回傳結果
var arr = [7 ,8, 9]
arr = arr.join('!')
console.log(arr)
// output: 7!8!9
indexOf()
用 indexOf(8)
來找「8」的 index 位置,也就是 1
arr.indexOf(8)
var arr = [7 ,8, 9]
arr.indexOf(8)
console.log(arr)
// output: [ 7, 8, 9 ]
可以看到:arr
陣列的值,並沒有被 arr.indexOf(8)
改變
因為 arr.indexOf(8)
會回傳一個新的值,也就是 1
但是並不會影響到原本的 arr
陣列,
所以印出的還是 [ 7, 8, 9 ]
var index = arr.indexOf(8)
因為 arr.indexOf(8)
會回傳一個新的值,也就是 1
再把 1 這個值重新賦予給變數 index
因此,就會印出 1 了
var arr = [7 ,8, 9]
var index = arr.indexOf(8)
console.log(index)
// output: 1
splice()
跟 slice()
要分清楚splice()
會改變原本的陣列:在原本的陣列「新增、移除元素」
slice()
不會改變原本的陣列:會回傳「擷取後的新陣列」
在使用 function 時,要非常清楚「可變、不可變」的特性,否則會出現很多 bug:
例如:
var str = 'FOREST'
var newStr = str.toLowerCase()
console.log(newStr)
// output: forest
例如:
var arr = ['Jan', 'Feb', 'March', 'April', 'May']
arr.splice(2, 1, 'hello')
console.log(arr)
// output: [ 'Jan', 'Feb', 'hello', 'April', 'May' ]
例如:
var arr = ['Jan', 'Feb', 'March', 'April', 'May']
var newArr = arr.slice(2, 4)
console.log(newArr)
// output: [ 'March', 'April' ]
]]>如果想了解的更深入,可參考下面資料:
https://ithelp.ithome.com.tw/articles/10206190
http://antrash.pixnet.net/blog/post/70456505-stack-vs-heap
https://medium.com/@yauhsienhuang/stack-acdcc11263a0
除了 object (也就是陣列、物件)之外的東西,基本上都是 primitive types (原始型態)
屬於 primitive types(例如 number, string, boolean)的值就是 immutable values(不可變的值),也被稱為「primitive values」
str.toUpperCase()
,因為:如果只是呼叫 function,是絕對沒辦法改變 str
的值str.toUpperCase()
只會回傳新的字串,所以要再把新的字串重新賦予給另一個變數: var newStr = str.toUpperCase()
,newStr
才會是「轉成大寫後的字串」接下來,會用一些範例來說明 immutable values 的觀念
a 這個變數是「可以改變的」
但是 'hello' 這個字串是「不會改變的」
var a = 'hello'
a = 'tree'
意思是:
var a = 'hello'
一開始給 a 的值是 ‘hello’
'hello' 這個字串存放的「記憶體位置」假設是 0x01
var a = 'hello'
// a: 'hello' 0x01
a = 'tree'
當 a = 'tree'
時,並不是「把 0x01 上面的 ‘hello’」直接改成 'tree'
而是會把「新的字串 'tree'」用 =
重新賦予給變數 a (新的字串 ‘tree’ 會放在一個新的記憶體位置 0x02 上)
所以,變數 a 的值最後會是 'tree'
var a = 'hello'
// a: 'hello' 0x01
a = 'tree'
// a: 'tree' 0x02
a = a + 'tree'
var a = 'hello'
// a: 'hello' 0x01
a = a + 'tree'
// a: 'hellotree' 0x02
同樣地:
var a = 'hello'
:a = a + 'tree'
:a + 'tree'
,回傳一個新的字串 'hellotree',再把 'hellotree' 用 =
重新賦予給 a (新的字串 'hellotree' 會放在一個新的記憶體位置 0x02 上)a.toUpperCase()
錯誤的想法:會認為「只要呼叫 a.toUpperCase()
之後」,a 就會轉成大寫
但其實,只呼叫 a.toUpperCase()
並不會把 a 轉成大寫(不會改變 a 原本的值)
因為:
'hello' 這個字串本身是「不可變」的
所以,只要「變數 a 是屬於 primitive type(也就是 number, string, boolean)」,就絕對沒辦法用「呼叫 function 的方式」去改變「變數 a 原本的值」
a.toUpperCase()
的確會「return(回傳)」一個新的字串 'HELLO'但是!如果沒有把這個回傳的新字串 ‘HELLO’ 賦予給任何變數,是不會有任何事情發生的
var a = 'hello'
a.toUpperCase()
console.log(a)
// output: hello
a = a.toUpperCase()
讓 a.toUpperCase()
回傳一個新的字串 'HELLO',再用 =
重新賦予給變數 a (新的字串 ‘HELLO’,會在一個新的記憶體位置上)
var a = 'hello'
a = a.toUpperCase()
console.log(a)
// output: HELLO
var a = 'hello'
'hello' 這個字串所存放的記憶體位置,假設是 0x01
var a = 'hello'
// a: 'hello' 0x01
a = a.toUpperCase()
步驟一:
a.toUpperCase()
會「return(回傳)」一個新的字串 'HELLO'
但是!如果沒有把這個回傳的新字串 ‘HELLO’ 賦予給任何變數,是不會有任何事情發生的
步驟二: a = a.toUpperCase()
再把「新的字串 ‘HELLO’」,用 =
重新賦予給變數 a (新的字串 ‘HELLO’ 會在新的記憶體位置 0x02 上)
var a = 'hello'
// a: 'hello' 0x01
a = a.toUpperCase()
// a.toUpperCase(): 'HELLO'
// a: 'HELLO' 0x02
console.log(a)
var temp = a.toUpperCase()
延續上面的範例,也可以宣告一個新的變數 temp
,去接收 a.toUpperCase()
回傳的「新的字串 'HELLO'」
var a = 'hello'
// a: 'hello' 0x01
var temp = a.toUpperCase()
// a.toUpperCase(): 'HELLO'
// temp: 'HELLO' 0X02
console.log(temp)
// output: HELLO
在 change
函式可以傳一個字串進去
change
函式: change(a)
console.log(a)
var a = 'hello'
function change(str){
str = 'monday'
}
change(a)
console.log(a)
// output: hello
可以看到,最後印出來變數 a 的值依然還是 'hello',並不會被 change
函式變成 'monday'
原因也是一樣:只使用「呼叫 function」的方式,並沒辦法改變「變數 a 原本的值」
除非是「重新賦予給變數 a」一個新的值,變數 a 才會有新的值
參考資料 Array.prototype
就直接呼叫 function 即可,例如: arr.push(50)
不需要再用一個變數去接收 return 的值,例如這個錯誤寫法: arr = arr.push(50)
如果我本來就是想要改變「原本的 array」,所使用的函式就會直接改動「原本的 array」,基本上不會再回傳一個新的 array 了(會回傳別的東西)
push()
用 push()
會直接改變原本的陣列,並不會產生另一個新的陣列
var arr = [7 ,8, 9]
arr.push(50)
console.log(arr)
// output: [ 7, 8, 9, 50 ]
原因是:
push()
確實是真的去改變了在「原本記憶體位置」上面存的 [7 ,8, 9]
var arr = [7 ,8, 9]
存放 [7 ,8, 9]
的記憶體位置,假設是 0x01
var arr = [7 ,8, 9]
// arr: [7, 8, 9] 0x01
arr.push(50)
console.log(arr)
arr.push(50)
會直接在「0x01 這個記憶體位置」上,改變它存的 [7 ,8, 9]
,變成 [7, 8, 9, 50]
var arr = [7 ,8, 9]
// arr: [7, 8, 9, 50] 0x01
arr.push(50)
console.log(arr)
arr = arr.push(50)
是錯的!根本不需要再把 arr.push(50)
重新賦予給 arr
用這個錯誤寫法,印出來的值會不同--> 會印出 4
var arr = [7 ,8, 9]
arr = arr.push(50) // 錯誤寫法!!
console.log(arr)
// output: 4
因為 Array.prototype.push()
這個 method 會「在原本陣列的最後面新增一個或多個元素」,並且「return 原本陣列的新長度」
所以,回傳的「4」就是 arr
陣列 [7, 8, 9, 50]
的長度
arr = arr.push(50)
會做兩件事情:arr.push(50)
,把 arr
陣列變成 [7, 8, 9, 50]
arr.push(50)
這個 function 會回傳 [7, 8, 9, 50]
的長度,也就是 4,這個 4 會被重新賦予給變數 arr
所以,變數 arr
最後的值就變成 4 了
reverse()
var arr = [7 ,8, 9]
arr.reverse()
console.log(arr)
// output: [ 9, 8, 7 ]
arr = arr.reverse()
是錯誤寫法
但是,為什麼印出來的結果也會是 [ 9, 8, 7 ]
呢?
因為 arr.reverse()
除了直接改變原本的 array 之外,
也會 return 一個新的「順序倒過來的 array」
var arr = [7 ,8, 9]
arr = arr.reverse()
console.log(arr)
// output: [ 9, 8, 7 ]
通常來說,這些 function 回傳的值都不會是一個 array
因此,需要一個「變數」去接收這個 return 的值,例如: var result = arr.join('!')
join()
join()
不會改變原本的 arr
陣列
join()
回傳的值,會是一個 string
string 跟 array 是完全不同的東西,所以不可能去改動「原本的 array」變成 string,因此就用「回傳一個新的字串」的方式來回傳結果
var arr = [7 ,8, 9]
arr = arr.join('!')
console.log(arr)
// output: 7!8!9
indexOf()
用 indexOf(8)
來找「8」的 index 位置,也就是 1
arr.indexOf(8)
var arr = [7 ,8, 9]
arr.indexOf(8)
console.log(arr)
// output: [ 7, 8, 9 ]
可以看到:arr
陣列的值,並沒有被 arr.indexOf(8)
改變
因為 arr.indexOf(8)
會回傳一個新的值,也就是 1
但是並不會影響到原本的 arr
陣列,
所以印出的還是 [ 7, 8, 9 ]
var index = arr.indexOf(8)
因為 arr.indexOf(8)
會回傳一個新的值,也就是 1
再把 1 這個值重新賦予給變數 index
因此,就會印出 1 了
var arr = [7 ,8, 9]
var index = arr.indexOf(8)
console.log(index)
// output: 1
splice()
跟 slice()
要分清楚splice()
會改變原本的陣列:在原本的陣列「新增、移除元素」
slice()
不會改變原本的陣列:會回傳「擷取後的新陣列」
在使用 function 時,要非常清楚「可變、不可變」的特性,否則會出現很多 bug:
例如:
var str = 'FOREST'
var newStr = str.toLowerCase()
console.log(newStr)
// output: forest
例如:
var arr = ['Jan', 'Feb', 'March', 'April', 'May']
arr.splice(2, 1, 'hello')
console.log(arr)
// output: [ 'Jan', 'Feb', 'hello', 'April', 'May' ]
例如:
var arr = ['Jan', 'Feb', 'March', 'April', 'May']
var newArr = arr.slice(2, 4)
console.log(newArr)
// output: [ 'March', 'April' ]
]]>如果想了解的更深入,可參考下面資料:
https://ithelp.ithome.com.tw/articles/10206190
http://antrash.pixnet.net/blog/post/70456505-stack-vs-heap
https://medium.com/@yauhsienhuang/stack-acdcc11263a0
return
是 function 回傳的值console.log()
如果是執行 add(8, 9)
,就會印出 8 9
function add(a, b){
console.log(a, b)
}
add(8, 9)
// output: 8 9
如果是 console.log(add(8, 9))
,就會印出 8 9 和 undefined
原因是:
在 function 裡面,如果沒有寫 return 的話,預設就會是 return undefined
console.log(add(8, 9))
這句的執行順序是:
add(8, 9)
這個函式:console.log(a, b)
就會印出 8 9return undefined
,就會是 add
函式的回傳值console.log(add(8, 9))
,就會印出 add
函式的回傳值(也就是 undefined)function add(a, b){
console.log(a, b)
// 預設會是 return undefined
}
console.log(add(8, 9))
output:
8 9
undefined
console.log()
,在 console 就不會印出任何東西function add(a, b){
return a + b
}
add(8, 9)
console.log()
印出 return 的值function add(a, b){
return a + b
}
console.log(add(8, 9))
// output: 17
這樣,我就沒辦法得到 function 的回傳值了(會回傳 undefined)
function add(a, b){
console.log(a + b)
}
console.log(add(8, 9))
output:
17
undefined
當我在使用 chrome devtool 的 Console 時,會是另一種不同的運作機制
我每執行一個指令,Console 就會自動幫我把「這個指令本身會產生的回傳值」給 log 出來
例如:
我執行完 var a = 1
之後,var a = 1
這個指令本身會產生的回傳值是 undefined,Console 就會把 undefined 給 log 出來
因為 a + 3
這個語句的回傳值是 4,所以就會印出 4
如果是 console.log(a + 3)
,會印出:
console.log(a + 3)
出來的值」console.log(a + 3)
這個語句本身的回傳值執行完 add
函式這個指令之後,這個指令本身的回傳值就是 undefined,所以會印出 undefined
執行 add(5, 7)
之後,會印出:
console.log(a, b)
印出來的東西」add
函式的回傳值」,也就是 return a + b
的值return
是 function 回傳的值console.log()
如果是執行 add(8, 9)
,就會印出 8 9
function add(a, b){
console.log(a, b)
}
add(8, 9)
// output: 8 9
如果是 console.log(add(8, 9))
,就會印出 8 9 和 undefined
原因是:
在 function 裡面,如果沒有寫 return 的話,預設就會是 return undefined
console.log(add(8, 9))
這句的執行順序是:
add(8, 9)
這個函式:console.log(a, b)
就會印出 8 9return undefined
,就會是 add
函式的回傳值console.log(add(8, 9))
,就會印出 add
函式的回傳值(也就是 undefined)function add(a, b){
console.log(a, b)
// 預設會是 return undefined
}
console.log(add(8, 9))
output:
8 9
undefined
console.log()
,在 console 就不會印出任何東西function add(a, b){
return a + b
}
add(8, 9)
console.log()
印出 return 的值function add(a, b){
return a + b
}
console.log(add(8, 9))
// output: 17
這樣,我就沒辦法得到 function 的回傳值了(會回傳 undefined)
function add(a, b){
console.log(a + b)
}
console.log(add(8, 9))
output:
17
undefined
當我在使用 chrome devtool 的 Console 時,會是另一種不同的運作機制
我每執行一個指令,Console 就會自動幫我把「這個指令本身會產生的回傳值」給 log 出來
例如:
我執行完 var a = 1
之後,var a = 1
這個指令本身會產生的回傳值是 undefined,Console 就會把 undefined 給 log 出來
因為 a + 3
這個語句的回傳值是 4,所以就會印出 4
如果是 console.log(a + 3)
,會印出:
console.log(a + 3)
出來的值」console.log(a + 3)
這個語句本身的回傳值執行完 add
函式這個指令之後,這個指令本身的回傳值就是 undefined,所以會印出 undefined
執行 add(5, 7)
之後,會印出:
console.log(a, b)
印出來的東西」add
函式的回傳值」,也就是 return a + b
的值參考資料 Array
join()
用字元連接陣列join()
會把「陣列的每一個元素」用我想要的「字元」連接在一起之後,產生另一個新的「string」回傳
join()
最後會回傳一個新的 stringvar arr = [7, 8, 9]
console.log(arr.join('!'))
// output: 7!8!9
如果沒有指定字元的話,預設就會是「每個元素直接連接在一起」var arr = [7, 8, 9]
console.log(arr.join(''))
// output: 789
map()
map()
傳入函式第一種寫法:
在 map()
小括號裡面傳入一個函式
然後 map()
就會把「原本陣列的每一個元素」當作參數,按照順序傳進這個 double
函式裡面,用「double
函式每一次的回傳值」產生另一個新的陣列
map()
最後會回傳另一個新的 arrayvar arr = [7, 8, 9]
function double(x){
return x * 2
}
console.log(arr.map(double))
// output: [ 14, 16, 18 ]
map()
傳入匿名函式這是第二種寫法:
也可以在 map()
的小括號裡面,直接傳入一個匿名函式
var arr = [7, 8, 9]
console.log(arr.map(function (x) {
return x * 2
}))
// output: [ 14, 16, 18 ]
map()
再接 map()
map()
後面可以接無限多個 map()
map()
:對 arr
陣列的每個元素執行 x * 2
map()
:用第一次 map()
回傳的新陣列,再針對新陣列的每個元素執行 x * -1
var arr = [7, 8, 9]
console.log(
arr
.map(function (x) {
return x * 2
})
.map(function(x){
return x * -1
})
)
// output: [ -14, -16, -18 ]
filter()
過濾東西filter()
可以把東西過濾掉:回傳 true 的東西會留下,回傳 false 的東西會不見filter()
最後會回傳另一個新的 array把 arr
陣列的每個元素當作參數,按照順序傳進 filter()
的函式裡面
x > 0
回傳是 true 的元素就會留下x > 0
回傳是 false 的元素就會不見var arr = [7, -8, 9, 2, -5, 3]
console.log(
arr
.map(function(x) {
return x * 2
})
.filter(function(x) {
return x > 0
})
)
// output: [ 14, 18, 4, 6 ]
map()
和 filter()
最後都是回傳一個 array,所以 map()
和 filter()
都可以互相接連使用例如:
var arr = [7, -8, 9, 2, -5, 3]
console.log(
arr
.map(function (x) {
return x * 2
})
.filter(function (x) {
return x > 0
})
.map(function(x){
return x - 1
})
)
// output: [ 13, 17, 3, 5 ]
join()
最後會回傳的是一個「string」,所以後面就不能再接 map()
或 filter()
原因:string 並沒有 map()
或 filter()
這些函式可以用,所以就會出錯
slice()
擷取陣列的某個部份slice()
會回傳另一個新的 arrayarr.slice(2)
代表:從 arr
陣列的「第 2 個 index 的元素」開始擷取,到最後一個元素我都要
var arr = ['milk', 'biscuit', 'salmon', 'mango', 'noodle']
console.log(arr.slice(2))
// output: [ 'salmon', 'mango', 'noodle' ]
[範例一]
arr.slice(2, 4)
的意思是:
所以,就只會擷取 index 是 2, 3 的元素
var arr = ['milk', 'biscuit', 'salmon', 'mango', 'noodle']
console.log(arr.slice(2, 4))
// output: [ 'salmon', 'mango' ]
[範例二]
arr.slice(1, 5)
只會擷取 index 是 1, 2, 3, 4 的元素
var arr = ['milk', 'biscuit', 'salmon', 'mango', 'noodle']
console.log(arr.slice(1, 5))
// output: [ 'biscuit', 'salmon', 'mango', 'noodle' ]
splice()
插入、替換元素splice 是「拼接」的意思
splice()
可以做到兩件事情:
splice()
會直接改變原本的 array例如:
原本的 months
陣列是 ['Jan', 'March', 'April', 'June']
months.splice(1, 0, 'Feb')
:會在 index = 1 的位置用 'Feb' 來「取代 0 個元素」這時,months
陣列就會變成 ['Jan', 'Feb', 'March', 'April', 'June']
months.splice(4, 1, 'May')
:會在 index = 4 的位置用 'May'「取代 1 個元素」最後,months
陣列就會變成 ['Jan', 'Feb', 'March', 'April', 'May']
var months = ['Jan', 'March', 'April', 'June']
months.splice(1, 0, 'Feb')
// replaces 0 element at index 1
console.log(months)
// output: [ 'Jan', 'Feb', 'March', 'April', 'June' ]
months.splice(4, 1, 'May')
// replaces 1 element at index 4
console.log(months)
// output: [ 'Jan', 'Feb', 'March', 'April', 'May' ]
sort()
排序sort()
會直接改變原本的 array如果是字母的話,就會根據「每個元素的第一個字母」,按照字母的排序(a, b, c...),去重新排序 array 裡的元素
如果第一個字母相同,就比第二個字母
如果是數字的話,不是按照「數字大小」排列,而是會把數字當作「字串」排列,所以是按照「每個元素的第一個數字」的字典序來排序
如果第一個數字相同,就比第二個數字
var animals = ['elephant', 'dog', 'ant', 'cat', 'bird', 'alpaca']
animals.sort()
console.log(animals)
// output: [ 'alpaca', 'ant', 'bird', 'cat', 'dog', 'elephant' ]
var arr = [5, 100000, 1, 25, 9, 213]
arr.sort()
console.log(arr)
// output: [ 1, 100000, 213, 25, 5, 9 ]
如果想要讓數字由小到大排列,可以這麼做:
在 sort()
小括號傳入一個 compareFunction
,用這個 function 來告訴 sort()
要怎麼排序
這個 function 會傳入 a, b 這兩個數字當作參數,「由小到大排列」的規則如下:
a === b
,就 return 0a < b
,也就是「a, b 不用互換位置」,就 return 一個負數a > b
,也就是「a, b 要互換位置」,就 return 一個正數最後,把 arr
陣列印出來,就會是我想要的結果了:由小到大排列
var arr = [5, 100000, 1, 25, 9, 213]
// 由小到大排列
arr.sort(function(a, b){
if(a === b) return 0
if(a < b) return -1
return 1
})
console.log(arr)
// output: [ 1, 5, 9, 25, 213, 100000 ]
這個 function 會傳入 a, b 這兩個數字當作參數,「由大到小排列」的規則如下:
a === b
,就 return 0a > b
,也就是「a, b 不用互換位置」,就 return 一個負數a < b
,也就是「a, b 要互換位置」,就 return 一個正數最後,把 arr
陣列印出來,就會是我想要的結果了:由大到小排列
var arr = [5, 100000, 1, 25, 9, 213]
// 由大到小排列
arr.sort(function(a, b){
if(a === b) return 0
if(a > b) return -1
return 1
})
console.log(arr)
// output: [ 100000, 213, 25, 9, 5, 1 ]
var arr = [5, 100000, 1, 25, 9, 213]
// 由大到小排列
arr.sort(function(a, b){
if(a === b) return 0
return (a > b) ? -1 : 1
})
console.log(arr)
// output: [ 100000, 213, 25, 9, 5, 1 ]
前面講解完原理之後,之後寫題目就直接用a - b
或 b - a
這樣寫即可
return b - a
a = 5, b = 1
就是「由大到小」排列假設 a = 5, b = 1
因為是要「由大到小」排列,所以 a, b 不用換位置--> 就要回傳一個負數,所以要讓 b - a
var arr = [5, 100000, 1, 25, 9, 213]
// 由大到小排列
arr.sort(function(a, b){
return b - a
})
console.log(arr)
// output: [ 100000, 213, 25, 9, 5, 1 ]
return a - b
a = 1, b = 5
就是「由小到大」排列假設 a = 1, b = 5
因為是要「由小到大」排列,所以 a, b 不用換位置--> 就要回傳一個負數,所以要讓 a - b
var arr = [5, 100000, 1, 25, 9, 213]
// 由小到大排列
arr.sort(function(a, b){
return a - b
})
console.log(arr)
// output: [ 1, 5, 9, 25, 213, 100000 ]
reduce()
reduce()
的小括號裡面,可以傳入兩個參數
[6, 7, 8, 9].reduce(function (accumulator, currentValue) {
return currentValue + accumulator
}, 35)
reduce()
第一個參數:是一個 functionreduce()
一定要傳入一個 functionreduce()
所傳入的 function 會有兩個參數:
accumulator
:就是「累積器」的意思accumulator
currentValue
:就是「陣列裡面的每一個元素」reduce()
最後 return 的值,就是「accumulator
最終的值」reduce()
第二個參數 initialValue
: 會是 accumulator
的初始值initialValue
可加可不加如果沒有設定 initialValue
:
accumulator
的初始值」currentValue
」reduce()
是怎麼運作的?initialValue
var sum = [6, 7, 8, 9].reduce(function (accumulator, currentValue) {
return currentValue + accumulator
})
console.log(sum)
// output: 30
陣列 [6, 7, 8, 9]
有 4 個元素,所以 reduce()
小括號裡的 function 總共會執行 3 次
accumulator
的初始值會是 6 (陣列的第一個元素)currentValue
會是 7 (陣列的第二個元素)accumulator
會是 13,因為上一次 return 的值 = 13currentValue
會是 8accumulator
會是 21,因為上一次 return 的值 = 21currentValue
會是 9最後,accumulator
會是 30,所以最後 return 的值就是 30
initialValue
(會成為 accumulator
的初始值)var sum = [6, 7, 8, 9].reduce(function (accumulator, currentValue) {
return currentValue + accumulator
}, 35)
console.log(sum)
// output: 65
陣列 [6, 7, 8, 9]
有 4 個元素,但是因為這次有設定初始值,所以 reduce()
小括號裡的 function 總共會執行 4 次
accumulator
的初始值會是 35 (也就是我設定的 initialValue
)currentValue
會是 6 (陣列的第一個元素)accumulator
會是 41,因為上一次 return 的值 = 41currentValue
會是 7accumulator
會是 48,因為上一次 return 的值 = 48currentValue
會是 8accumulator
會是 56,因為上一次 return 的值 = 56currentValue
會是 9最後,accumulator
會是 65,所以最後 return 的值就是 65
reduce()
的好處是什麼?如果是用 map()
或 forEach()
會有什麼壞處呢?]]>請參考 這篇
參考資料 Array
join()
用字元連接陣列join()
會把「陣列的每一個元素」用我想要的「字元」連接在一起之後,產生另一個新的「string」回傳
join()
最後會回傳一個新的 stringvar arr = [7, 8, 9]
console.log(arr.join('!'))
// output: 7!8!9
如果沒有指定字元的話,預設就會是「每個元素直接連接在一起」var arr = [7, 8, 9]
console.log(arr.join(''))
// output: 789
map()
map()
傳入函式第一種寫法:
在 map()
小括號裡面傳入一個函式
然後 map()
就會把「原本陣列的每一個元素」當作參數,按照順序傳進這個 double
函式裡面,用「double
函式每一次的回傳值」產生另一個新的陣列
map()
最後會回傳另一個新的 arrayvar arr = [7, 8, 9]
function double(x){
return x * 2
}
console.log(arr.map(double))
// output: [ 14, 16, 18 ]
map()
傳入匿名函式這是第二種寫法:
也可以在 map()
的小括號裡面,直接傳入一個匿名函式
var arr = [7, 8, 9]
console.log(arr.map(function (x) {
return x * 2
}))
// output: [ 14, 16, 18 ]
map()
再接 map()
map()
後面可以接無限多個 map()
map()
:對 arr
陣列的每個元素執行 x * 2
map()
:用第一次 map()
回傳的新陣列,再針對新陣列的每個元素執行 x * -1
var arr = [7, 8, 9]
console.log(
arr
.map(function (x) {
return x * 2
})
.map(function(x){
return x * -1
})
)
// output: [ -14, -16, -18 ]
filter()
過濾東西filter()
可以把東西過濾掉:回傳 true 的東西會留下,回傳 false 的東西會不見filter()
最後會回傳另一個新的 array把 arr
陣列的每個元素當作參數,按照順序傳進 filter()
的函式裡面
x > 0
回傳是 true 的元素就會留下x > 0
回傳是 false 的元素就會不見var arr = [7, -8, 9, 2, -5, 3]
console.log(
arr
.map(function(x) {
return x * 2
})
.filter(function(x) {
return x > 0
})
)
// output: [ 14, 18, 4, 6 ]
map()
和 filter()
最後都是回傳一個 array,所以 map()
和 filter()
都可以互相接連使用例如:
var arr = [7, -8, 9, 2, -5, 3]
console.log(
arr
.map(function (x) {
return x * 2
})
.filter(function (x) {
return x > 0
})
.map(function(x){
return x - 1
})
)
// output: [ 13, 17, 3, 5 ]
join()
最後會回傳的是一個「string」,所以後面就不能再接 map()
或 filter()
原因:string 並沒有 map()
或 filter()
這些函式可以用,所以就會出錯
slice()
擷取陣列的某個部份slice()
會回傳另一個新的 arrayarr.slice(2)
代表:從 arr
陣列的「第 2 個 index 的元素」開始擷取,到最後一個元素我都要
var arr = ['milk', 'biscuit', 'salmon', 'mango', 'noodle']
console.log(arr.slice(2))
// output: [ 'salmon', 'mango', 'noodle' ]
[範例一]
arr.slice(2, 4)
的意思是:
所以,就只會擷取 index 是 2, 3 的元素
var arr = ['milk', 'biscuit', 'salmon', 'mango', 'noodle']
console.log(arr.slice(2, 4))
// output: [ 'salmon', 'mango' ]
[範例二]
arr.slice(1, 5)
只會擷取 index 是 1, 2, 3, 4 的元素
var arr = ['milk', 'biscuit', 'salmon', 'mango', 'noodle']
console.log(arr.slice(1, 5))
// output: [ 'biscuit', 'salmon', 'mango', 'noodle' ]
splice()
插入、替換元素splice 是「拼接」的意思
splice()
可以做到兩件事情:
splice()
會直接改變原本的 array例如:
原本的 months
陣列是 ['Jan', 'March', 'April', 'June']
months.splice(1, 0, 'Feb')
:會在 index = 1 的位置用 'Feb' 來「取代 0 個元素」這時,months
陣列就會變成 ['Jan', 'Feb', 'March', 'April', 'June']
months.splice(4, 1, 'May')
:會在 index = 4 的位置用 'May'「取代 1 個元素」最後,months
陣列就會變成 ['Jan', 'Feb', 'March', 'April', 'May']
var months = ['Jan', 'March', 'April', 'June']
months.splice(1, 0, 'Feb')
// replaces 0 element at index 1
console.log(months)
// output: [ 'Jan', 'Feb', 'March', 'April', 'June' ]
months.splice(4, 1, 'May')
// replaces 1 element at index 4
console.log(months)
// output: [ 'Jan', 'Feb', 'March', 'April', 'May' ]
sort()
排序sort()
會直接改變原本的 array如果是字母的話,就會根據「每個元素的第一個字母」,按照字母的排序(a, b, c...),去重新排序 array 裡的元素
如果第一個字母相同,就比第二個字母
如果是數字的話,不是按照「數字大小」排列,而是會把數字當作「字串」排列,所以是按照「每個元素的第一個數字」的字典序來排序
如果第一個數字相同,就比第二個數字
var animals = ['elephant', 'dog', 'ant', 'cat', 'bird', 'alpaca']
animals.sort()
console.log(animals)
// output: [ 'alpaca', 'ant', 'bird', 'cat', 'dog', 'elephant' ]
var arr = [5, 100000, 1, 25, 9, 213]
arr.sort()
console.log(arr)
// output: [ 1, 100000, 213, 25, 5, 9 ]
如果想要讓數字由小到大排列,可以這麼做:
在 sort()
小括號傳入一個 compareFunction
,用這個 function 來告訴 sort()
要怎麼排序
這個 function 會傳入 a, b 這兩個數字當作參數,「由小到大排列」的規則如下:
a === b
,就 return 0a < b
,也就是「a, b 不用互換位置」,就 return 一個負數a > b
,也就是「a, b 要互換位置」,就 return 一個正數最後,把 arr
陣列印出來,就會是我想要的結果了:由小到大排列
var arr = [5, 100000, 1, 25, 9, 213]
// 由小到大排列
arr.sort(function(a, b){
if(a === b) return 0
if(a < b) return -1
return 1
})
console.log(arr)
// output: [ 1, 5, 9, 25, 213, 100000 ]
這個 function 會傳入 a, b 這兩個數字當作參數,「由大到小排列」的規則如下:
a === b
,就 return 0a > b
,也就是「a, b 不用互換位置」,就 return 一個負數a < b
,也就是「a, b 要互換位置」,就 return 一個正數最後,把 arr
陣列印出來,就會是我想要的結果了:由大到小排列
var arr = [5, 100000, 1, 25, 9, 213]
// 由大到小排列
arr.sort(function(a, b){
if(a === b) return 0
if(a > b) return -1
return 1
})
console.log(arr)
// output: [ 100000, 213, 25, 9, 5, 1 ]
var arr = [5, 100000, 1, 25, 9, 213]
// 由大到小排列
arr.sort(function(a, b){
if(a === b) return 0
return (a > b) ? -1 : 1
})
console.log(arr)
// output: [ 100000, 213, 25, 9, 5, 1 ]
前面講解完原理之後,之後寫題目就直接用a - b
或 b - a
這樣寫即可
return b - a
a = 5, b = 1
就是「由大到小」排列假設 a = 5, b = 1
因為是要「由大到小」排列,所以 a, b 不用換位置--> 就要回傳一個負數,所以要讓 b - a
var arr = [5, 100000, 1, 25, 9, 213]
// 由大到小排列
arr.sort(function(a, b){
return b - a
})
console.log(arr)
// output: [ 100000, 213, 25, 9, 5, 1 ]
return a - b
a = 1, b = 5
就是「由小到大」排列假設 a = 1, b = 5
因為是要「由小到大」排列,所以 a, b 不用換位置--> 就要回傳一個負數,所以要讓 a - b
var arr = [5, 100000, 1, 25, 9, 213]
// 由小到大排列
arr.sort(function(a, b){
return a - b
})
console.log(arr)
// output: [ 1, 5, 9, 25, 213, 100000 ]
reduce()
reduce()
的小括號裡面,可以傳入兩個參數
[6, 7, 8, 9].reduce(function (accumulator, currentValue) {
return currentValue + accumulator
}, 35)
reduce()
第一個參數:是一個 functionreduce()
一定要傳入一個 functionreduce()
所傳入的 function 會有兩個參數:
accumulator
:就是「累積器」的意思accumulator
currentValue
:就是「陣列裡面的每一個元素」reduce()
最後 return 的值,就是「accumulator
最終的值」reduce()
第二個參數 initialValue
: 會是 accumulator
的初始值initialValue
可加可不加如果沒有設定 initialValue
:
accumulator
的初始值」currentValue
」reduce()
是怎麼運作的?initialValue
var sum = [6, 7, 8, 9].reduce(function (accumulator, currentValue) {
return currentValue + accumulator
})
console.log(sum)
// output: 30
陣列 [6, 7, 8, 9]
有 4 個元素,所以 reduce()
小括號裡的 function 總共會執行 3 次
accumulator
的初始值會是 6 (陣列的第一個元素)currentValue
會是 7 (陣列的第二個元素)accumulator
會是 13,因為上一次 return 的值 = 13currentValue
會是 8accumulator
會是 21,因為上一次 return 的值 = 21currentValue
會是 9最後,accumulator
會是 30,所以最後 return 的值就是 30
initialValue
(會成為 accumulator
的初始值)var sum = [6, 7, 8, 9].reduce(function (accumulator, currentValue) {
return currentValue + accumulator
}, 35)
console.log(sum)
// output: 65
陣列 [6, 7, 8, 9]
有 4 個元素,但是因為這次有設定初始值,所以 reduce()
小括號裡的 function 總共會執行 4 次
accumulator
的初始值會是 35 (也就是我設定的 initialValue
)currentValue
會是 6 (陣列的第一個元素)accumulator
會是 41,因為上一次 return 的值 = 41currentValue
會是 7accumulator
會是 48,因為上一次 return 的值 = 48currentValue
會是 8accumulator
會是 56,因為上一次 return 的值 = 56currentValue
會是 9最後,accumulator
會是 65,所以最後 return 的值就是 65
reduce()
的好處是什麼?如果是用 map()
或 forEach()
會有什麼壞處呢?]]>請參考 這篇
參考資料 String
toUpperCase()
小寫變大寫只有「小寫字母」才會被 toUpperCase()
轉成大寫
因此,在做題目時,根本不需要去檢查「是否為小寫字母」
toUpperCase()
不會改變它toUpperCase()
不會改變它var a = 'Fancy!!'.toUpperCase()
console.log(a)
// output: FANCY!!
toLowerCase()
大寫變小寫只有「大寫字母」才會被 toLowerCase()
轉成小寫
因此,在做題目時,根本不需要去檢查「是否為大寫字母」
toLowerCase()
不會改變它toLowerCase()
不會改變它var a = 'ALMOND!!'.toLowerCase()
console.log(a)
// output: almond!!
charCodeAt()
每個字母、符號存在電腦內,都會有一個對應的數字,就叫做它的 ASCII Code
charCodeAt(0)
來得知「大寫 A」的 ASCII Code 是 65charCodeAt(0)
的 0 是代表「字串 'A' 的第一個位置的字」,也就是 Aconsole.log(code)
// output: 65
* 再用 `charCodeAt(0)` 來得知「小寫 a」的 ASCII Code 是 97
```javascript=
var str = 'a'
var code = str.charCodeAt(0)
console.log(code)
// output: 97
97(小寫 a)- 65(大寫 a)= 32
String.fromCharCode()
String.fromCharCode()
可以把 ASCII Code 轉成「它所代表的字母 or 符號」
因此,如果我想把「小寫 h」轉成「大寫 H」
String.fromCharCode()
把「大寫 H 的 ASCII Code」轉成字母即可var str = 'h'
var code = str.charCodeAt(0)
var upperStr = String.fromCharCode(code - 32)
console.log(upperStr)
// output: H
範例一:
回傳 true,代表「字串'h'」會介於「字串'a'」和「字串'z'」之間,因此可以判斷:變數 char
是小寫字母
var char = 'h'
console.log(char >= 'a' && char <= 'z')
// output: true
範例二:
回傳 false,代表「字串'h'」沒有介於「字串'a'」和「字串'z'」之間,因此可以判斷:變數 char
不是小寫字母
var char = 'H'
console.log(char >= 'a' && char <= 'z')
// output: false
indexOf()
判斷單字是否存在用 indexOf()
可以判斷:在一個字串裡面,have 這個單字是否存在?
如果有 have 這個單字的話,就會回傳 have 這個單字的第一個字母:h 的 index 值
var str = 'May I have a coffee'
var index = str.indexOf('have')
console.log(index)
// output: 6
indexOf()
回傳的值如果 < 0,就代表「have!!」並不存在
var str = 'May I have a coffee'
var index = str.indexOf('have!!')
console.log(index)
// output: -1
replace()
取代字串例如:我想要把字串中的 'May' 換成 '!!!'
var str = 'May I have a coffee'.replace('May', '!!!')
console.log(str)
// output: !!! I have a coffee
只會 match 到「第一個」
var str = 'May I have a coffee'.replace('e', '!!!')
console.log(str)
// output: May I hav!!! a coffee
如果要把全部的 'e' 都換成 ‘!!!’,就要使用 RegExp
(Regular Expression 正規表達式)
這個 RegExp
是「另一種尋找字串的方法」:
/e/g
的 g 就是 global 的意思--> 會 globally 地去 match 到「每一個」有對到的字元
var str = 'May I have a coffee'.replace(/e/g, '!!!')
console.log(str)
// output: May I hav!!! a coff!!!!!!
split()
把字串切割成陣列split()
會回傳一個陣列
separator
參數如果「沒有寫 separator
參數」或是「字串裡面不存在 separator
參數」,那麼回傳的陣列就會是:只有一個元素,這個元素就是「原本的字串」
var str = 'May I have a coffee'
console.log(str.split())
// output: [ 'May I have a coffee' ]
separator
參數可以傳一個 separator
參數,代表:要用什麼來切割
例如:
用一個空格 ' '
來切割,就會回傳一個「有 5 個元素的陣列」
var str = 'May I have a coffee'
console.log(str.split(' '))
// output: [ 'May', 'I', 'have', 'a', 'coffee' ]
用 'a'
來切割,就會回傳一個「有 4 個元素的陣列」(a 就不見了)
var str = 'May I have a coffee'
console.log(str.split('a'))
// output: [ 'M', 'y I h', 've ', ' coffee' ]
trim()
去除最前面、最後面的空格用 trim()
可以去除掉「字串最前面、最後面的所有空格」
var str = ' May I have a coffee '
console.log(str.trim())
// output: May I have a coffee
slice()
擷取字串的某個部份slice()
會回傳另一個新的 stringlet str = 'penguin'
let newStr = str.slice(2, 5)
console.log(newStr)
// output: ngu
參考資料 String
toUpperCase()
小寫變大寫只有「小寫字母」才會被 toUpperCase()
轉成大寫
因此,在做題目時,根本不需要去檢查「是否為小寫字母」
toUpperCase()
不會改變它toUpperCase()
不會改變它var a = 'Fancy!!'.toUpperCase()
console.log(a)
// output: FANCY!!
toLowerCase()
大寫變小寫只有「大寫字母」才會被 toLowerCase()
轉成小寫
因此,在做題目時,根本不需要去檢查「是否為大寫字母」
toLowerCase()
不會改變它toLowerCase()
不會改變它var a = 'ALMOND!!'.toLowerCase()
console.log(a)
// output: almond!!
charCodeAt()
每個字母、符號存在電腦內,都會有一個對應的數字,就叫做它的 ASCII Code
charCodeAt(0)
來得知「大寫 A」的 ASCII Code 是 65charCodeAt(0)
的 0 是代表「字串 'A' 的第一個位置的字」,也就是 Aconsole.log(code)
// output: 65
* 再用 `charCodeAt(0)` 來得知「小寫 a」的 ASCII Code 是 97
```javascript=
var str = 'a'
var code = str.charCodeAt(0)
console.log(code)
// output: 97
97(小寫 a)- 65(大寫 a)= 32
String.fromCharCode()
String.fromCharCode()
可以把 ASCII Code 轉成「它所代表的字母 or 符號」
因此,如果我想把「小寫 h」轉成「大寫 H」
String.fromCharCode()
把「大寫 H 的 ASCII Code」轉成字母即可var str = 'h'
var code = str.charCodeAt(0)
var upperStr = String.fromCharCode(code - 32)
console.log(upperStr)
// output: H
範例一:
回傳 true,代表「字串'h'」會介於「字串'a'」和「字串'z'」之間,因此可以判斷:變數 char
是小寫字母
var char = 'h'
console.log(char >= 'a' && char <= 'z')
// output: true
範例二:
回傳 false,代表「字串'h'」沒有介於「字串'a'」和「字串'z'」之間,因此可以判斷:變數 char
不是小寫字母
var char = 'H'
console.log(char >= 'a' && char <= 'z')
// output: false
indexOf()
判斷單字是否存在用 indexOf()
可以判斷:在一個字串裡面,have 這個單字是否存在?
如果有 have 這個單字的話,就會回傳 have 這個單字的第一個字母:h 的 index 值
var str = 'May I have a coffee'
var index = str.indexOf('have')
console.log(index)
// output: 6
indexOf()
回傳的值如果 < 0,就代表「have!!」並不存在
var str = 'May I have a coffee'
var index = str.indexOf('have!!')
console.log(index)
// output: -1
replace()
取代字串例如:我想要把字串中的 'May' 換成 '!!!'
var str = 'May I have a coffee'.replace('May', '!!!')
console.log(str)
// output: !!! I have a coffee
只會 match 到「第一個」
var str = 'May I have a coffee'.replace('e', '!!!')
console.log(str)
// output: May I hav!!! a coffee
如果要把全部的 'e' 都換成 ‘!!!’,就要使用 RegExp
(Regular Expression 正規表達式)
這個 RegExp
是「另一種尋找字串的方法」:
/e/g
的 g 就是 global 的意思--> 會 globally 地去 match 到「每一個」有對到的字元
var str = 'May I have a coffee'.replace(/e/g, '!!!')
console.log(str)
// output: May I hav!!! a coff!!!!!!
split()
把字串切割成陣列split()
會回傳一個陣列
separator
參數如果「沒有寫 separator
參數」或是「字串裡面不存在 separator
參數」,那麼回傳的陣列就會是:只有一個元素,這個元素就是「原本的字串」
var str = 'May I have a coffee'
console.log(str.split())
// output: [ 'May I have a coffee' ]
separator
參數可以傳一個 separator
參數,代表:要用什麼來切割
例如:
用一個空格 ' '
來切割,就會回傳一個「有 5 個元素的陣列」
var str = 'May I have a coffee'
console.log(str.split(' '))
// output: [ 'May', 'I', 'have', 'a', 'coffee' ]
用 'a'
來切割,就會回傳一個「有 4 個元素的陣列」(a 就不見了)
var str = 'May I have a coffee'
console.log(str.split('a'))
// output: [ 'M', 'y I h', 've ', ' coffee' ]
trim()
去除最前面、最後面的空格用 trim()
可以去除掉「字串最前面、最後面的所有空格」
var str = ' May I have a coffee '
console.log(str.trim())
// output: May I have a coffee
slice()
擷取字串的某個部份slice()
會回傳另一個新的 stringlet str = 'penguin'
let newStr = str.slice(2, 5)
console.log(newStr)
// output: ngu
使用內建函式的好處:
0.1 + 0.2 === 0.3
會 return false接下來,會介紹幾個常用的「number 類型的內建函式」
toString()
var a = 5
var str = a.toString()
console.log(typeof str)
// output: string
number + ''
因為「數字 + 字串」會變成「字串」
所以把 a 加上一個「空字串」,就可以把 a 轉成字串
var a = 5
var str = a + ''
console.log(typeof str)
// output: string
有兩種方式:
Number()
Number()
會保留小數點以後的數字var a = 10
var b = '20.75'
console.log(a + Number(b))
// output: 30.75
parseFloat()
Float 就是「浮點數」,也就是「小數」
parseFloat()
會保留小數點以後的數字var a = 10
var b = '20.75'
console.log(a + parseFloat(b, 10))
// output: 30.75
parseInt()
Int
就是 Integer(整數)的意思,所以
parseInt()
不會保留小數點以後的數字var a = 10
var b = '20.75'
console.log(a + parseInt(b, 10))
// output: 30
toFixed()
toFixed()
的回傳值,會是一個 string例如:我用 toFixed()
就可以四捨五入到「整數」
var b = '20.738953'
console.log(parseFloat(b, 10).toFixed())
// output: 21
例如:我用 toFixed(2)
就可以四捨五入到「小數點第二位」
var b = '20.738953'
console.log(parseFloat(b, 10).toFixed(2))
// output: 20.74
Number.MAX_VALUE
如果直接把 Number.MAX_VALUE
印出來,就代表:在 JavaScript,可以存的最大數字是 1.7976931348623157e+308
Number.MAX_VALUE
的數字,存的就會不精準console.log(Number.MAX_VALUE)
// output: 1.7976931348623157e+308
Math.PI
像是圓周率這種不會變的數,我們會稱為「常數」,常數會用大寫來表示,例如: PI
console.log(Math.PI)
// output: 3.141592653589793
Math.round()
console.log(Math.round(10.7))
// output: 11
Math.ceil()
ceil
是「天花板(ceiling)」的意思,所以就是「往上取」--> 無條件進位
console.log(Math.ceil(10.3))
// output: 11
Math.floor()
floor
是「地板」的意思,所以就是「往下取」--> 無條件捨去
console.log(Math.floor(10.7))
// output: 10
Math.sqrt()
9 開根號就是 3
console.log(Math.sqrt(9))
// output: 3
Math.pow()
2 的「5 次方」= 32
console.log(Math.pow(2, 5))
// output: 32
這個隨機數的範圍會是:0 到 <1(包括 0,但小於 1)
console.log(Math.random())
// output: 0.7736234935788224
Math.random()
可以用在哪裡呢?因為 Math.random()
可以產生 0 ~ 0.99999 之間的隨機數(把 <1 想成是 0.99999,會比較好想)
那我把 Math.random()
乘以 10,就可以產生一個「0 ~ 9.9999」之間的隨機數
console.log(Math.random() * 10)
那我把 Math.random()
乘以 10 再 +1,就可以產生一個「1 ~ 10.9999」之間的隨機數
接著,再用「無條件捨去」,就可以產生一個「1 ~ 10」之間的隨機數了!
console.log(Math.floor(Math.random() * 10 + 1))
// output: 10
]]>使用內建函式的好處:
0.1 + 0.2 === 0.3
會 return false接下來,會介紹幾個常用的「number 類型的內建函式」
toString()
var a = 5
var str = a.toString()
console.log(typeof str)
// output: string
number + ''
因為「數字 + 字串」會變成「字串」
所以把 a 加上一個「空字串」,就可以把 a 轉成字串
var a = 5
var str = a + ''
console.log(typeof str)
// output: string
有兩種方式:
Number()
Number()
會保留小數點以後的數字var a = 10
var b = '20.75'
console.log(a + Number(b))
// output: 30.75
parseFloat()
Float 就是「浮點數」,也就是「小數」
parseFloat()
會保留小數點以後的數字var a = 10
var b = '20.75'
console.log(a + parseFloat(b, 10))
// output: 30.75
parseInt()
Int
就是 Integer(整數)的意思,所以
parseInt()
不會保留小數點以後的數字var a = 10
var b = '20.75'
console.log(a + parseInt(b, 10))
// output: 30
toFixed()
toFixed()
的回傳值,會是一個 string例如:我用 toFixed()
就可以四捨五入到「整數」
var b = '20.738953'
console.log(parseFloat(b, 10).toFixed())
// output: 21
例如:我用 toFixed(2)
就可以四捨五入到「小數點第二位」
var b = '20.738953'
console.log(parseFloat(b, 10).toFixed(2))
// output: 20.74
Number.MAX_VALUE
如果直接把 Number.MAX_VALUE
印出來,就代表:在 JavaScript,可以存的最大數字是 1.7976931348623157e+308
Number.MAX_VALUE
的數字,存的就會不精準console.log(Number.MAX_VALUE)
// output: 1.7976931348623157e+308
Math.PI
像是圓周率這種不會變的數,我們會稱為「常數」,常數會用大寫來表示,例如: PI
console.log(Math.PI)
// output: 3.141592653589793
Math.round()
console.log(Math.round(10.7))
// output: 11
Math.ceil()
ceil
是「天花板(ceiling)」的意思,所以就是「往上取」--> 無條件進位
console.log(Math.ceil(10.3))
// output: 11
Math.floor()
floor
是「地板」的意思,所以就是「往下取」--> 無條件捨去
console.log(Math.floor(10.7))
// output: 10
Math.sqrt()
9 開根號就是 3
console.log(Math.sqrt(9))
// output: 3
Math.pow()
2 的「5 次方」= 32
console.log(Math.pow(2, 5))
// output: 32
這個隨機數的範圍會是:0 到 <1(包括 0,但小於 1)
console.log(Math.random())
// output: 0.7736234935788224
Math.random()
可以用在哪裡呢?因為 Math.random()
可以產生 0 ~ 0.99999 之間的隨機數(把 <1 想成是 0.99999,會比較好想)
那我把 Math.random()
乘以 10,就可以產生一個「0 ~ 9.9999」之間的隨機數
console.log(Math.random() * 10)
那我把 Math.random()
乘以 10 再 +1,就可以產生一個「1 ~ 10.9999」之間的隨機數
接著,再用「無條件捨去」,就可以產生一個「1 ~ 10」之間的隨機數了!
console.log(Math.floor(Math.random() * 10 + 1))
// output: 10
]]>
y = f(x) 是數學上的「函數」,它跟程式裡面的「函式」是一樣的東西,都叫做「function」
f(x)
就是「函數」x
就是「參數」y
就是「回傳值」return
也可以回傳一個 objectfunction twice(a){
return {
answer: a * 2
}
}
console.log(twice(5))
// output: { answer: 10 }
有些人為了排版,會把要回傳的東西按 Enter 到下一行去。
但這樣寫是錯的!
因為:
如果 return
後面沒有接東西的話,JavaScript 會把 return
和 {answer: a * 2}
當成「兩句不相干的東西」,所以 return
就只會回傳 undefined
function twice(a){
return
{
answer: a * 2
}
}
console.log(twice(5))
// output: undefined
好的做法:取個有意義的參數名稱,像是 min
、max
不好的做法:無意義的參數名稱,像是 a
、b
function generateArray(min, max){
var arr = []
for(let i=min; i<=max; i++){
arr.push(i)
}
return arr
}
console.log(generateArray(3, 10))
function generateArray(min, max){
var arr = []
for(let i=min; i<=max; i++){
arr.push(i)
}
return arr
}
var a = 3
var b = 10
console.log(generateArray(a, b))
宣告函式最常見的方式是這樣:
function greeting(){
console.log('hello')
}
greeting
greeting
等於一個 functiongreeting()
來執行這個 functionvar greeting = function(){
console.log('hello')
}
greeting() // 執行這個 function
// output: hello
範例一:
把 hello
函式當作參數,傳進 print
函式,結果就會印出 [Function: hello]
(也就是:hello
是一個函式),
hello
這個函式,因為我只是 console.log(anything)
function print(anything){
console.log(anything)
}
function hello(){
console.log('How are you')
}
print(hello)
// output: [Function: hello]
我現在把 print
函式改成:
anything()
,這樣就會去執行「傳進來的 hello
函式」了function print(anything){
anything()
}
function hello(){
console.log('How are you')
}
print(hello)
// output: How are you
anything
參數,其實就是 hello
函式,所以我就可以執行 anything()
,得到 hello
函式的結果範例二:
把 test
函式當作參數,傳進 transform
函式
function transform(a, transformFunction){
return transformFunction(a)
}
function test(a){
return a * 2
}
console.log(
transform(30, test)
)
// output: 60
範例三:
對每一個陣列元素 arr[i]
都使用「我傳進去的 transformFunction
函式」
function transform(arr, transformFunction){
let newArr = []
for(let i=0; i<arr.length; i++){
newArr.push(transformFunction(arr[i]))
}
return newArr
}
function double(x){
return x * 2
}
console.log(
transform([3, 4, 5], double)
)
// output: [ 6, 8, 10 ]
也可以這樣寫:
「匿名函式」就是:沒有名字的 function,像是這裡的
function(x){
return x * 2
}
不需要幫函式取名字,可以把整個匿名函式直接傳進去 transform
函式
function transform(arr, transformFunction){
let newArr = []
for(let i=0; i<arr.length; i++){
newArr.push(transformFunction(arr[i]))
}
return newArr
}
console.log(
transform([3, 4, 5], function(x){
return x * 2
})
)
// output: [ 6, 8, 10 ]
在使用 function 時,有兩個名詞常常會被混用:
範例一:
add
這個 function,接收了兩個參數:a 和 badd
函式的引數:3 和 5 function add(a, b) {
return a + b
}
add(3, 5)
範例二:
add
函式的參數:a 和 badd
函式的引數:c 和 dadd
函式的引數是 10 和 20function add(a, b){
return a + b
}
var c = 10
var d = 20
add(c, d)
在 add
函式裡面,用 console.log(arguments)
就可以把「引數」給印出來
function add(a, b){
console.log(arguments)
return a + b
}
console.log(add(7, 9))
output:
[Arguments] { '0': 7, '1': 9 }
16
也可以個別取得引數:
arguments[0]
arguments[1]
function add(a, b){
console.log(arguments[0])
console.log(arguments[1])
return a + b
}
console.log(add(7, 9))
output:
7
9
16
在 function 中,也可以不寫參數,直接用「取得引數」的方式做為回傳值 return arguments[0] + arguments[1]
(此方式會用到的機率很低,通常還是會直接把參數寫好)
function add(){
return arguments[0] + arguments[1]
}
console.log(add(7, 9))
// output: 16
因為印出來的 arguments 是用「大括號」包起來的,所以 arguments 是一個「物件」
arguments 不是一個「陣列」,因此,用 Array.isArray(arguments)
來看 arguments
是不是一個陣列,就會回傳 false
function add(a, b){
console.log(arguments)
console.log(Array.isArray(arguments))
return a + b
}
console.log(add(7, 9))
output:
[Arguments] { '0': 7, '1': 9 }
false
16
Array
-like)物件參考資料 Arguments 物件
但是,arguments 這種形式的物件(key 的值是 0, 1, 2...),就跟陣列沒兩樣,因為我同樣可以用 arguments[0]
讀取到第一個引數
其實正確是要用單引號把 key 包起來: arguments['0']
但是因為「number 的 0」和「string 的 '0'
」是沒有區別的,所以也可以直接寫 arguments[0]
這就是讀取物件值的第二種方式:用中括號來讀取 key 的值(如果 key 是字串,就一定要用單引號把 key 包起來)
var a = {
name: 'Harry'
}
console.log(a['name'])
arguments.length
共有幾個引數物件名稱.length
來知道物件長度的但是,因為 arguments 是一個類陣列(Array-like)物件,所以才可以用 arguments.length
來知道總共傳進了幾個引數
function add(a, b){
console.log(arguments.length)
console.log(Array.isArray(arguments))
return a + b
}
console.log(add(7, 9))
output:
2
false
16
arguments 有一些屬性像是 arguments.callee
和 arguments.caller
,可以印出:是誰在 call 這個 function 的
要印出「是誰在 call 這個 function 的」,就需要帶有一些資訊,這時就需要用到「物件」的格式來儲存資料(用 value 來儲存每個 key 帶有的資訊)
如果是 array 的格式的話,根本沒辦法做到「儲存每個 key 帶有的資訊」這件事
關於這個主題,如果想要了解的更深入、更複雜,可以參考:深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?
用一個 function 交換兩個變數:
因為是要交換兩個值,所以一定要有第三個變數 temp
,用來把 a 的值存起來
接著,宣告兩個變數 number1
、number2
,先把兩個變數的值印出來
執行 swap
函式後,再一次把兩個變數的值印出來
function swap(a, b){
var temp = a
a = b
b = temp
}
var number1 = 50
var number2 = 100
console.log(number1, number2)
swap(number1, number2) // 執行 swap 函式
console.log(number1, number2)
output:
50 100
50 100
結果會發現,兩個變數(number1, number2)的值並沒有交換!
在 swap
函式裡面,再把 a, b 印出來看看 console.log('a, b:', a, b)
會發現:
swap
函式的 a, b 確實有交換:是 100, 50function swap(a, b){
var temp = a
a = b
b = temp
console.log('a, b:', a, b)
}
var number1 = 50
var number2 = 100
console.log(number1, number2)
swap(number1, number2) // 執行 swap 函式
console.log(number1, number2)
output:
50 100
a, b: 100 50
50 100
在傳入引數時:
把 number1, number2 傳進去 swap
函式時,其實是「先複製一份」
function swap(a, b) {
/*
會先複製一份(可以想成是這樣,但實際上不是這樣的)
var a = number1
var b = number2
*/
var temp = a
a = b
b = temp
console.log('a, b:', a, b)
}
var number1 = 50
var number2 = 100
console.log(number1, number2)
swap(number1, number2) // 執行 swap 函式
console.log(number1, number2)
因為變數 number1, number2 的型別是 number(屬於 primitive 型別),因此變數存的是「值」,每個變數都會是獨立的,都會在不同的記憶體位置上
既然 a, b 是由 number1, number2 複製來的,因此型別也都會是 number,那麼「a, b, number1, number2」就會在四個完全不一樣的記憶體位置上
「a 和 number1」、「b 和 number2」只是「值一樣」而已,但是卻是「完全不同的變數」(都在不同的記憶體位置上)
所以,在 function 裡面的 a, b 的確是改變了(值交換了),但是這並不會影響到「分別在別的記憶體位置的 number1, number2」的值
有一個 addValue
函式,會傳一個 object 進去
addValue
function 裡面,會讓 obj.number
+1a = {number: 5}
addValue
函式,把 a 傳進去思考一下,a.number
會跟著 +1 嗎?
function addValue(obj){
obj.number++
return 1
}
var a = {
number: 5
}
addValue(a)
console.log(a)
output:
{ number: 6 }
結果會發現,物件 a 也被改到了!
原因是:
詳細說明可參考 讓兩個物件相等:指向同一個記憶體位置(改一個,也同時改到另一個)
在把 a 傳進 addValue
函式時,也會先複製一份,可以想成是:
在 addValue
函式裡面加上這句 var obj = a
因為物件存的是「記憶體位置」,所以複製一份之後,兩個物件(obj
和 a
)還是會指向「同一個記憶體位置」
所以,當我修改物件 obj
時,也會同時改到物件 a
function addValue(obj){
/*
先把「物件 a」複製一份
var obj = a
*/
obj.number++
return 1
}
var a = {
number: 5
}
addValue(a)
console.log(a)
function addValue(obj) {
/*
先把「物件 a」複製一份
var obj = a
*/
obj.test = 30
return 1
}
var a = {
number: 5
}
addValue(a)
console.log(a)
output:
{ number: 5, test: 30 }
結果是:物件 a 也跟著新增 test
屬性了!
obj
等於一個新的物件(改 function 裡面的,不會影響到 function 外的)這時,我讓變數 obj
等於一個新的物件: obj = {number: 100}
思考一下,最後印出來的物件 a,會是 {number: 5}
還是 {number: 100}
呢?
function addValue(obj) {
/*
先把「物件 a」複製一份
var obj = a
*/
obj = {
number: 100
}
return 1
}
var a = {
number: 5
}
addValue(a)
console.log(a)
output:
{ number: 5 }
結果是:物件 a
依然還是原本的 { number: 5 }
原因是:
因為我「用 =
」讓變數 obj
等於一個新的物件,變數 obj
就會指向另一個新的記憶體位置(斷開原本跟物件 a 的連結)
兩個物件(obj
和 a
)不再是同一個記憶體位置了,所以修改 obj
根本不會去改到 a
以上這些就是在使用 function 時,要注意的事情:
在 function 裡面修改 object(物件、陣列)時,有可能也會改到 function 外的物件、陣列
但如果是在 function 裡面修改 primitive 型別的變數,就絕對不會影響到外面的
在 function 裡面,
return 的作用是什麼?什麼時候要用?什麼時候不用?
可以把 function 簡單分成兩類:
例如:
function greeting(name){
console.log('hello', name)
}
greeting('Harry')
// output: hello Harry
當然,如果我想要回傳,也是可以加上一個 return
,例如下面的 return 1
,但是這不會有任何影響,output 還是一樣是 hello Harry
function greeting(name){
console.log('hello', name)
return 1
}
greeting('Harry')
return undefined
像是這樣:
function greeting(name){
console.log('hello', name)
return undefined
}
greeting('Harry')
要怎麼看到這個 return undefined
的結果呢?
var a = greeting('Harry')
,a 會去接收「greeting
這個 function 的回傳值」,也就是 return 的值結果就是:a 會是 undefined
function greeting(name){
// console.log('hello', name)
}
var a = greeting('Harry')
console.log(a)
// output: undefined
function greeting(name){
console.log('hello', name)
return 'I am a'
}
var a = greeting('Harry')
console.log(a)
output:
hello Harry
I am a
執行順序是:
greeting('Harry')
console.log('hello', name)
,印出 hello Harryreturn 'I am a'
,這時,a 就會等於我要 return 的值 'I am a'例如:
這個 double
函式,我想要知道一個數乘以 2 的結果是多少
如果我只有寫 double(8)
,是不會印出任何東西的
function double(x){
return x * 2
}
double(8)
宣告一個變數 var result = double(8)
,這個 result 就會是「double
函式 return 的值」
再把 result 印出來
function double(x){
return x * 2
}
var result = double(8)
console.log(result)
// output: 16
例如:
我把 console.log('hello')
放在 return 前面,就會印出 hello
function double(x){
console.log('hello')
return x * 2
}
var result = double(8)
console.log(result)
output:
hello
16
但是,如果把 console.log('hello')
放在 return 後面,執行完 return 後,就會跳出 function(不會執行到 console.log('hello')
這句),然後接著執行第六行
function double(x){
return x * 2
console.log('hello')
}
var result = double(8)
console.log(result)
output:
16
+
連接例如: return str[i] + ' ' + i
function position(str){
for(let i=0; i<str.length; i++){
if(str[i] >= 'A' && str[i] <= 'Z'){
return str[i] + ' ' + i
}
}
return -1
}
console.log(position("abCD"))
// output: C 2
]]>y = f(x) 是數學上的「函數」,它跟程式裡面的「函式」是一樣的東西,都叫做「function」
f(x)
就是「函數」x
就是「參數」y
就是「回傳值」return
也可以回傳一個 objectfunction twice(a){
return {
answer: a * 2
}
}
console.log(twice(5))
// output: { answer: 10 }
有些人為了排版,會把要回傳的東西按 Enter 到下一行去。
但這樣寫是錯的!
因為:
如果 return
後面沒有接東西的話,JavaScript 會把 return
和 {answer: a * 2}
當成「兩句不相干的東西」,所以 return
就只會回傳 undefined
function twice(a){
return
{
answer: a * 2
}
}
console.log(twice(5))
// output: undefined
好的做法:取個有意義的參數名稱,像是 min
、max
不好的做法:無意義的參數名稱,像是 a
、b
function generateArray(min, max){
var arr = []
for(let i=min; i<=max; i++){
arr.push(i)
}
return arr
}
console.log(generateArray(3, 10))
function generateArray(min, max){
var arr = []
for(let i=min; i<=max; i++){
arr.push(i)
}
return arr
}
var a = 3
var b = 10
console.log(generateArray(a, b))
宣告函式最常見的方式是這樣:
function greeting(){
console.log('hello')
}
greeting
greeting
等於一個 functiongreeting()
來執行這個 functionvar greeting = function(){
console.log('hello')
}
greeting() // 執行這個 function
// output: hello
範例一:
把 hello
函式當作參數,傳進 print
函式,結果就會印出 [Function: hello]
(也就是:hello
是一個函式),
hello
這個函式,因為我只是 console.log(anything)
function print(anything){
console.log(anything)
}
function hello(){
console.log('How are you')
}
print(hello)
// output: [Function: hello]
我現在把 print
函式改成:
anything()
,這樣就會去執行「傳進來的 hello
函式」了function print(anything){
anything()
}
function hello(){
console.log('How are you')
}
print(hello)
// output: How are you
anything
參數,其實就是 hello
函式,所以我就可以執行 anything()
,得到 hello
函式的結果範例二:
把 test
函式當作參數,傳進 transform
函式
function transform(a, transformFunction){
return transformFunction(a)
}
function test(a){
return a * 2
}
console.log(
transform(30, test)
)
// output: 60
範例三:
對每一個陣列元素 arr[i]
都使用「我傳進去的 transformFunction
函式」
function transform(arr, transformFunction){
let newArr = []
for(let i=0; i<arr.length; i++){
newArr.push(transformFunction(arr[i]))
}
return newArr
}
function double(x){
return x * 2
}
console.log(
transform([3, 4, 5], double)
)
// output: [ 6, 8, 10 ]
也可以這樣寫:
「匿名函式」就是:沒有名字的 function,像是這裡的
function(x){
return x * 2
}
不需要幫函式取名字,可以把整個匿名函式直接傳進去 transform
函式
function transform(arr, transformFunction){
let newArr = []
for(let i=0; i<arr.length; i++){
newArr.push(transformFunction(arr[i]))
}
return newArr
}
console.log(
transform([3, 4, 5], function(x){
return x * 2
})
)
// output: [ 6, 8, 10 ]
在使用 function 時,有兩個名詞常常會被混用:
範例一:
add
這個 function,接收了兩個參數:a 和 badd
函式的引數:3 和 5 function add(a, b) {
return a + b
}
add(3, 5)
範例二:
add
函式的參數:a 和 badd
函式的引數:c 和 dadd
函式的引數是 10 和 20function add(a, b){
return a + b
}
var c = 10
var d = 20
add(c, d)
在 add
函式裡面,用 console.log(arguments)
就可以把「引數」給印出來
function add(a, b){
console.log(arguments)
return a + b
}
console.log(add(7, 9))
output:
[Arguments] { '0': 7, '1': 9 }
16
也可以個別取得引數:
arguments[0]
arguments[1]
function add(a, b){
console.log(arguments[0])
console.log(arguments[1])
return a + b
}
console.log(add(7, 9))
output:
7
9
16
在 function 中,也可以不寫參數,直接用「取得引數」的方式做為回傳值 return arguments[0] + arguments[1]
(此方式會用到的機率很低,通常還是會直接把參數寫好)
function add(){
return arguments[0] + arguments[1]
}
console.log(add(7, 9))
// output: 16
因為印出來的 arguments 是用「大括號」包起來的,所以 arguments 是一個「物件」
arguments 不是一個「陣列」,因此,用 Array.isArray(arguments)
來看 arguments
是不是一個陣列,就會回傳 false
function add(a, b){
console.log(arguments)
console.log(Array.isArray(arguments))
return a + b
}
console.log(add(7, 9))
output:
[Arguments] { '0': 7, '1': 9 }
false
16
Array
-like)物件參考資料 Arguments 物件
但是,arguments 這種形式的物件(key 的值是 0, 1, 2...),就跟陣列沒兩樣,因為我同樣可以用 arguments[0]
讀取到第一個引數
其實正確是要用單引號把 key 包起來: arguments['0']
但是因為「number 的 0」和「string 的 '0'
」是沒有區別的,所以也可以直接寫 arguments[0]
這就是讀取物件值的第二種方式:用中括號來讀取 key 的值(如果 key 是字串,就一定要用單引號把 key 包起來)
var a = {
name: 'Harry'
}
console.log(a['name'])
arguments.length
共有幾個引數物件名稱.length
來知道物件長度的但是,因為 arguments 是一個類陣列(Array-like)物件,所以才可以用 arguments.length
來知道總共傳進了幾個引數
function add(a, b){
console.log(arguments.length)
console.log(Array.isArray(arguments))
return a + b
}
console.log(add(7, 9))
output:
2
false
16
arguments 有一些屬性像是 arguments.callee
和 arguments.caller
,可以印出:是誰在 call 這個 function 的
要印出「是誰在 call 這個 function 的」,就需要帶有一些資訊,這時就需要用到「物件」的格式來儲存資料(用 value 來儲存每個 key 帶有的資訊)
如果是 array 的格式的話,根本沒辦法做到「儲存每個 key 帶有的資訊」這件事
關於這個主題,如果想要了解的更深入、更複雜,可以參考:深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?
用一個 function 交換兩個變數:
因為是要交換兩個值,所以一定要有第三個變數 temp
,用來把 a 的值存起來
接著,宣告兩個變數 number1
、number2
,先把兩個變數的值印出來
執行 swap
函式後,再一次把兩個變數的值印出來
function swap(a, b){
var temp = a
a = b
b = temp
}
var number1 = 50
var number2 = 100
console.log(number1, number2)
swap(number1, number2) // 執行 swap 函式
console.log(number1, number2)
output:
50 100
50 100
結果會發現,兩個變數(number1, number2)的值並沒有交換!
在 swap
函式裡面,再把 a, b 印出來看看 console.log('a, b:', a, b)
會發現:
swap
函式的 a, b 確實有交換:是 100, 50function swap(a, b){
var temp = a
a = b
b = temp
console.log('a, b:', a, b)
}
var number1 = 50
var number2 = 100
console.log(number1, number2)
swap(number1, number2) // 執行 swap 函式
console.log(number1, number2)
output:
50 100
a, b: 100 50
50 100
在傳入引數時:
把 number1, number2 傳進去 swap
函式時,其實是「先複製一份」
function swap(a, b) {
/*
會先複製一份(可以想成是這樣,但實際上不是這樣的)
var a = number1
var b = number2
*/
var temp = a
a = b
b = temp
console.log('a, b:', a, b)
}
var number1 = 50
var number2 = 100
console.log(number1, number2)
swap(number1, number2) // 執行 swap 函式
console.log(number1, number2)
因為變數 number1, number2 的型別是 number(屬於 primitive 型別),因此變數存的是「值」,每個變數都會是獨立的,都會在不同的記憶體位置上
既然 a, b 是由 number1, number2 複製來的,因此型別也都會是 number,那麼「a, b, number1, number2」就會在四個完全不一樣的記憶體位置上
「a 和 number1」、「b 和 number2」只是「值一樣」而已,但是卻是「完全不同的變數」(都在不同的記憶體位置上)
所以,在 function 裡面的 a, b 的確是改變了(值交換了),但是這並不會影響到「分別在別的記憶體位置的 number1, number2」的值
有一個 addValue
函式,會傳一個 object 進去
addValue
function 裡面,會讓 obj.number
+1a = {number: 5}
addValue
函式,把 a 傳進去思考一下,a.number
會跟著 +1 嗎?
function addValue(obj){
obj.number++
return 1
}
var a = {
number: 5
}
addValue(a)
console.log(a)
output:
{ number: 6 }
結果會發現,物件 a 也被改到了!
原因是:
詳細說明可參考 讓兩個物件相等:指向同一個記憶體位置(改一個,也同時改到另一個)
在把 a 傳進 addValue
函式時,也會先複製一份,可以想成是:
在 addValue
函式裡面加上這句 var obj = a
因為物件存的是「記憶體位置」,所以複製一份之後,兩個物件(obj
和 a
)還是會指向「同一個記憶體位置」
所以,當我修改物件 obj
時,也會同時改到物件 a
function addValue(obj){
/*
先把「物件 a」複製一份
var obj = a
*/
obj.number++
return 1
}
var a = {
number: 5
}
addValue(a)
console.log(a)
function addValue(obj) {
/*
先把「物件 a」複製一份
var obj = a
*/
obj.test = 30
return 1
}
var a = {
number: 5
}
addValue(a)
console.log(a)
output:
{ number: 5, test: 30 }
結果是:物件 a 也跟著新增 test
屬性了!
obj
等於一個新的物件(改 function 裡面的,不會影響到 function 外的)這時,我讓變數 obj
等於一個新的物件: obj = {number: 100}
思考一下,最後印出來的物件 a,會是 {number: 5}
還是 {number: 100}
呢?
function addValue(obj) {
/*
先把「物件 a」複製一份
var obj = a
*/
obj = {
number: 100
}
return 1
}
var a = {
number: 5
}
addValue(a)
console.log(a)
output:
{ number: 5 }
結果是:物件 a
依然還是原本的 { number: 5 }
原因是:
因為我「用 =
」讓變數 obj
等於一個新的物件,變數 obj
就會指向另一個新的記憶體位置(斷開原本跟物件 a 的連結)
兩個物件(obj
和 a
)不再是同一個記憶體位置了,所以修改 obj
根本不會去改到 a
以上這些就是在使用 function 時,要注意的事情:
在 function 裡面修改 object(物件、陣列)時,有可能也會改到 function 外的物件、陣列
但如果是在 function 裡面修改 primitive 型別的變數,就絕對不會影響到外面的
在 function 裡面,
return 的作用是什麼?什麼時候要用?什麼時候不用?
可以把 function 簡單分成兩類:
例如:
function greeting(name){
console.log('hello', name)
}
greeting('Harry')
// output: hello Harry
當然,如果我想要回傳,也是可以加上一個 return
,例如下面的 return 1
,但是這不會有任何影響,output 還是一樣是 hello Harry
function greeting(name){
console.log('hello', name)
return 1
}
greeting('Harry')
return undefined
像是這樣:
function greeting(name){
console.log('hello', name)
return undefined
}
greeting('Harry')
要怎麼看到這個 return undefined
的結果呢?
var a = greeting('Harry')
,a 會去接收「greeting
這個 function 的回傳值」,也就是 return 的值結果就是:a 會是 undefined
function greeting(name){
// console.log('hello', name)
}
var a = greeting('Harry')
console.log(a)
// output: undefined
function greeting(name){
console.log('hello', name)
return 'I am a'
}
var a = greeting('Harry')
console.log(a)
output:
hello Harry
I am a
執行順序是:
greeting('Harry')
console.log('hello', name)
,印出 hello Harryreturn 'I am a'
,這時,a 就會等於我要 return 的值 'I am a'例如:
這個 double
函式,我想要知道一個數乘以 2 的結果是多少
如果我只有寫 double(8)
,是不會印出任何東西的
function double(x){
return x * 2
}
double(8)
宣告一個變數 var result = double(8)
,這個 result 就會是「double
函式 return 的值」
再把 result 印出來
function double(x){
return x * 2
}
var result = double(8)
console.log(result)
// output: 16
例如:
我把 console.log('hello')
放在 return 前面,就會印出 hello
function double(x){
console.log('hello')
return x * 2
}
var result = double(8)
console.log(result)
output:
hello
16
但是,如果把 console.log('hello')
放在 return 後面,執行完 return 後,就會跳出 function(不會執行到 console.log('hello')
這句),然後接著執行第六行
function double(x){
return x * 2
console.log('hello')
}
var result = double(8)
console.log(result)
output:
16
+
連接例如: return str[i] + ' ' + i
function position(str){
for(let i=0; i<str.length; i++){
if(str[i] >= 'A' && str[i] <= 'Z'){
return str[i] + ' ' + i
}
}
return -1
}
console.log(position("abCD"))
// output: C 2
]]>
do...while...
do...while...
因為程式碼是從上到下執行,所以
do
大括號」一定會先執行一次,才會去判斷「while
的條件」也就是說,就算「while
的條件」從一開始就不成立,「do
大括號」還是會被執行一次
記住!
小括號
裡面放「條件」大括號
代表「一個區塊」,裡面放「要執行的程式碼」do
後面接「大括號」(因為大括號就代表“一個區塊 block”)while
後面接「小括號」,裡面放「條件」var i = 1
do {
console.log(i)
i++
} while (i <= 100)
console.log('finished!')
另一種寫法:
當 i > 100
就會 break(跳出 do...while...
的區塊)
var i = 1
do {
console.log(i)
i++
if(i > 100){
break
}
} while (true)
console.log('finished!')
break
跳出整個迴圈continue
直接進入下一圈var i = 1
do {
console.log(i)
i++
if(i % 2 === 1){
continue
}
console.log('這個 i 是偶數')
} while (i <= 100)
console.log('finished!')
continue
就代表「直接跳到下一圈」(還是會先檢查 while
的條件是否成立)i % 2 === 1
是 true(代表 i 是奇數),就不會往下執行到 console.log('這個 i 是偶數')
這行,而會直接跳到「while
的條件判斷」,當條件是 true 就進入下一圈output:
1
這個 i 是偶數
2
3
這個 i 是偶數
4
5
這個 i 是偶數
6
7
這個 i 是偶數
...
「while 迴圈」跟「do...while...迴圈」的執行順序剛好相反
while
條件,條件成立再去執行大括號的程式碼結構都是「先一個小括號,再接一個大括號」
while () {
}
for () {
}
var i = 1
while (i <= 100) {
console.log(i)
i++
}
console.log('finished!')
另一種寫法:
i++
直接寫在 console.log()
裡面var i = 1
while (i <= 100) {
console.log(i++)
}
console.log('finished!')
因為,console.log(i++)
這句的執行順序會是:
console.log(i)
i++
while
迴圈也可以使用 continue
和 break
如果不小心寫出無窮迴圈,可以按 ctrl + C 來中斷執行程式,否則電腦會當機(資源都被吃光了)
下面兩種方式可以寫出無窮迴圈:
while
小括號裡面寫 truevar i = 1
while (true) {
console.log(i)
i++
}
console.log('finished!')
while
小括號裡面寫 1 (因為 1 就是 true 的意思)var i = 1
while (1) {
console.log(i)
i++
}
console.log('finished!')
在多數情況下,都會使用「while 迴圈」,因為可以先判斷條件
除非,你很確定「第一次一定會執行」,才會用到「do…while…迴圈」
for loop 通常會用在:我已經知道會有多少圈
最常見的迴圈,會需要這三個要素:
所以,for loop 的語法就是這樣:
for (初始值; 終止條件; i 每一圈要做的事情){
}
for(var i=1; i<=100; i++){
console.log(i)
}
也可以這樣寫:
把初始值 var i = 1
寫在外面
;
」,因為小括號裡面一定要有三個條件,否則程式會出錯var i = 1
for(; i<=10; i++){
console.log(i)
}
執行順序是:
var i=1
i<=100
console.log(i)
i++
i<=100
console.log(i)
continue
和 break
continue
範例一:印出 1~10 的偶數
當 i % 2 === 1
(i 是奇數)就不會執行到 console.log(i)
這行
for(var i=1; i<=10; i++){
if(i % 2 === 1) continue
console.log(i)
}
也可以寫成:
因為 1 就是 true
for(var i=1; i<=10; i++){
if(i % 2) continue
console.log(i)
}
output:
2
4
6
8
10
範例二:
當 i === 3
就不會執行到 console.log(i)
這行
for(var i=1; i<=5; i++){
if(i === 3) continue
console.log(i)
}
output:
1
2
4
5
break
for(var i=1; i<=5; i++){
if(i === 3) break
console.log(i)
}
output:
1
2
]]>do...while...
do...while...
因為程式碼是從上到下執行,所以
do
大括號」一定會先執行一次,才會去判斷「while
的條件」也就是說,就算「while
的條件」從一開始就不成立,「do
大括號」還是會被執行一次
記住!
小括號
裡面放「條件」大括號
代表「一個區塊」,裡面放「要執行的程式碼」do
後面接「大括號」(因為大括號就代表“一個區塊 block”)while
後面接「小括號」,裡面放「條件」var i = 1
do {
console.log(i)
i++
} while (i <= 100)
console.log('finished!')
另一種寫法:
當 i > 100
就會 break(跳出 do...while...
的區塊)
var i = 1
do {
console.log(i)
i++
if(i > 100){
break
}
} while (true)
console.log('finished!')
break
跳出整個迴圈continue
直接進入下一圈var i = 1
do {
console.log(i)
i++
if(i % 2 === 1){
continue
}
console.log('這個 i 是偶數')
} while (i <= 100)
console.log('finished!')
continue
就代表「直接跳到下一圈」(還是會先檢查 while
的條件是否成立)i % 2 === 1
是 true(代表 i 是奇數),就不會往下執行到 console.log('這個 i 是偶數')
這行,而會直接跳到「while
的條件判斷」,當條件是 true 就進入下一圈output:
1
這個 i 是偶數
2
3
這個 i 是偶數
4
5
這個 i 是偶數
6
7
這個 i 是偶數
...
「while 迴圈」跟「do...while...迴圈」的執行順序剛好相反
while
條件,條件成立再去執行大括號的程式碼結構都是「先一個小括號,再接一個大括號」
while () {
}
for () {
}
var i = 1
while (i <= 100) {
console.log(i)
i++
}
console.log('finished!')
另一種寫法:
i++
直接寫在 console.log()
裡面var i = 1
while (i <= 100) {
console.log(i++)
}
console.log('finished!')
因為,console.log(i++)
這句的執行順序會是:
console.log(i)
i++
while
迴圈也可以使用 continue
和 break
如果不小心寫出無窮迴圈,可以按 ctrl + C 來中斷執行程式,否則電腦會當機(資源都被吃光了)
下面兩種方式可以寫出無窮迴圈:
while
小括號裡面寫 truevar i = 1
while (true) {
console.log(i)
i++
}
console.log('finished!')
while
小括號裡面寫 1 (因為 1 就是 true 的意思)var i = 1
while (1) {
console.log(i)
i++
}
console.log('finished!')
在多數情況下,都會使用「while 迴圈」,因為可以先判斷條件
除非,你很確定「第一次一定會執行」,才會用到「do…while…迴圈」
for loop 通常會用在:我已經知道會有多少圈
最常見的迴圈,會需要這三個要素:
所以,for loop 的語法就是這樣:
for (初始值; 終止條件; i 每一圈要做的事情){
}
for(var i=1; i<=100; i++){
console.log(i)
}
也可以這樣寫:
把初始值 var i = 1
寫在外面
;
」,因為小括號裡面一定要有三個條件,否則程式會出錯var i = 1
for(; i<=10; i++){
console.log(i)
}
執行順序是:
var i=1
i<=100
console.log(i)
i++
i<=100
console.log(i)
continue
和 break
continue
範例一:印出 1~10 的偶數
當 i % 2 === 1
(i 是奇數)就不會執行到 console.log(i)
這行
for(var i=1; i<=10; i++){
if(i % 2 === 1) continue
console.log(i)
}
也可以寫成:
因為 1 就是 true
for(var i=1; i<=10; i++){
if(i % 2) continue
console.log(i)
}
output:
2
4
6
8
10
範例二:
當 i === 3
就不會執行到 console.log(i)
這行
for(var i=1; i<=5; i++){
if(i === 3) continue
console.log(i)
}
output:
1
2
4
5
break
for(var i=1; i<=5; i++){
if(i === 3) break
console.log(i)
}
output:
1
2
]]>
var score = 68
if(70 >= score >= 60){
console.log('good')
}
小括號內的條件會由左做到右,因此執行順序是:
70 >= score
,結果會是 trueif(true >= 60)
,結果會是 false&&
連接多個條件var score = 68
if(score >= 60 && score <= 70){
console.log('good')
}
// output: good
=
假設我不小心打錯,在 if 小括號裡面只有打了一個等號
var score = 68
if(score = 68){
console.log('good')
}
if 判斷式那部分會是這樣執行的:
score = 68
if (score) {
console.log('good')
}
因為 score
會是 true,所以就也會執行大括號裡面的程式碼
範例一:
var score = 68
if (score >= 60) {
console.log('good')
} else {
console.log('fail')
}
範例二:
判斷 number
是不是 5 的倍數
var number = 10
if (!(number % 5)) {
console.log('是 5 的倍數')
} else {
console.log('不是')
}
當我有多個條件時,就可以使用 if/else if statement
直覺會這樣子寫:
var score = 60
var isPass = false
if(score >= 60){
isPass = true
} else {
isPass = false
}
var score = 60
var isPass = score >= 60
var isPass = score >= 60
這句會先執行 score >= 60
score >= 60
是 true,那 isPass
就是 truescore >= 60
是 false,那 isPass
就是 false當我有三、四個條件以上時,使用 switch case 會較簡潔
break;
,否則就會全部都執行一次var month = 4
switch(month){
case 1:
console.log('一月')
break;
case 2:
console.log('二月')
break;
case 3:
console.log('三月')
break;
case 4:
console.log('四月')
break;
default:
console.log('hello')
}
// output: 四月
可以把兩個 case 合在一起
例如:
var month = 2
switch(month){
case 1:
case 2:
console.log('一月到二月')
break;
case 3:
console.log('三月')
break;
default:
console.log('hello')
}
// output: 一月到二月
var month = 3
var month_to_chinese = ['一月', '二月', '三月']
console.log(month_to_chinese[month - 1])
// output: 三月
var score = 60
var message = ''
if(score >= 60){
message = 'pass'
} else {
message = 'fail'
}
console.log(message)
語法:
condition ? A : B
condition
是「條件」?
代表「我要問問題」condition
是 true,就回傳 Acondition
是 false,就回傳 B例如:
console.log(20 > 7 ? 'bigger' : 'smaller')
// output: bigger
因此,上方的例子就可以改成:
var score = 60
var message = score >= 60 ? 'pass' : 'fail'
console.log(message)
// output: pass
在「三元運算子」裡面,再用一次「三元運算子」,但是因為這樣很難讓人閱讀程式碼,因此寫成 if else 比較好
var score = 100
var message = score >= 60 ? (score === 100 ? 'no1' : 'pass') : 'fail'
console.log(message)
// output: no1
執行順序會是:
score >= 60
(score === 100 ? 'no1' : 'pass')
score === 100
,是的話回傳 no1
,不是的話回傳 pass
var score = 68
if(70 >= score >= 60){
console.log('good')
}
小括號內的條件會由左做到右,因此執行順序是:
70 >= score
,結果會是 trueif(true >= 60)
,結果會是 false&&
連接多個條件var score = 68
if(score >= 60 && score <= 70){
console.log('good')
}
// output: good
=
假設我不小心打錯,在 if 小括號裡面只有打了一個等號
var score = 68
if(score = 68){
console.log('good')
}
if 判斷式那部分會是這樣執行的:
score = 68
if (score) {
console.log('good')
}
因為 score
會是 true,所以就也會執行大括號裡面的程式碼
範例一:
var score = 68
if (score >= 60) {
console.log('good')
} else {
console.log('fail')
}
範例二:
判斷 number
是不是 5 的倍數
var number = 10
if (!(number % 5)) {
console.log('是 5 的倍數')
} else {
console.log('不是')
}
當我有多個條件時,就可以使用 if/else if statement
直覺會這樣子寫:
var score = 60
var isPass = false
if(score >= 60){
isPass = true
} else {
isPass = false
}
var score = 60
var isPass = score >= 60
var isPass = score >= 60
這句會先執行 score >= 60
score >= 60
是 true,那 isPass
就是 truescore >= 60
是 false,那 isPass
就是 false當我有三、四個條件以上時,使用 switch case 會較簡潔
break;
,否則就會全部都執行一次var month = 4
switch(month){
case 1:
console.log('一月')
break;
case 2:
console.log('二月')
break;
case 3:
console.log('三月')
break;
case 4:
console.log('四月')
break;
default:
console.log('hello')
}
// output: 四月
可以把兩個 case 合在一起
例如:
var month = 2
switch(month){
case 1:
case 2:
console.log('一月到二月')
break;
case 3:
console.log('三月')
break;
default:
console.log('hello')
}
// output: 一月到二月
var month = 3
var month_to_chinese = ['一月', '二月', '三月']
console.log(month_to_chinese[month - 1])
// output: 三月
var score = 60
var message = ''
if(score >= 60){
message = 'pass'
} else {
message = 'fail'
}
console.log(message)
語法:
condition ? A : B
condition
是「條件」?
代表「我要問問題」condition
是 true,就回傳 Acondition
是 false,就回傳 B例如:
console.log(20 > 7 ? 'bigger' : 'smaller')
// output: bigger
因此,上方的例子就可以改成:
var score = 60
var message = score >= 60 ? 'pass' : 'fail'
console.log(message)
// output: pass
在「三元運算子」裡面,再用一次「三元運算子」,但是因為這樣很難讓人閱讀程式碼,因此寫成 if else 比較好
var score = 100
var message = score >= 60 ? (score === 100 ? 'no1' : 'pass') : 'fail'
console.log(message)
// output: no1
執行順序會是:
score >= 60
(score === 100 ? 'no1' : 'pass')
score === 100
,是的話回傳 no1
,不是的話回傳 pass
變數,就是一個「放東西的箱子」,這個箱子會取一個名稱,之後就可以用箱子的名稱去代指裡面裝的東西
宣告一個變數,例如:
var box = hello
這裡的 =
不是「等於」的意思
=
是「賦值」(賦予值)的意思可以看作是 var box <= hello
代表「我要把 hello 放到 box 裡面」
例如:下面這樣是錯誤的
var 335box = 57
變數的命名方式有分為兩種:
var api_response = 57
var apiResponse = 0
重點是!要統一!用了駝峰式就統一用駝峰式
宣告變數 box 和 BOX 會是兩個不同的變數
var box = hello
var BOX = 456
在 box 裡面沒有裝東西
var box
console.log(box)
會印出 undefined
「undefined」的意思是:有宣告這個變數,但是沒有給值
var box
console.log(boxkkk)
會印出 boxkkk is not defined
「is not defined」的意思是:根本沒有 boxkkk 宣告這個變數
var box = 57
console.log(box & 1)
會印出 1,因為 box 是奇數
++
和 --
var a = 0
a = a + 5
console.log(a)
上面程式碼的意思是:
會先執行 =
右邊的,把 a + 5
算完後的值,再放到 a 裡面
因此會印出 5
a = a + 5
也可以寫成 a += 5
兩種寫法是同樣的意思
var a = 0
a += 5
console.log(a)
a = a - 5
也可以寫成 a -= 5
a += 1
也可以寫成 a++
a -= 1
也可以寫成 a--
因為 +1 和 -1 在 JS 裡面很常用到,所以就決定給它一個專門的運算子來用
++
放在“後面”跟放在“前面”的差別在哪呢?a++
var a = 0
console.log(a++ && 30)
console.log('a:', a)
印出來的結果會是:
0
a: 1
第一句 console.log(a++ && 30)
會印出 0,
是因為 console.log(a++ && 30)
這句可以看成是:
console.log(a && 30)
a++
所以,
++
放後面的話第一步:會先執行 console.log(a && 30)
這句(這時的 a = 0,所以會印出 0)
第二步:再執行 a++
(這時的 a 才會是 1)
++a
var a = 0
console.log(++a && 30)
console.log('a:', a)
印出來的結果會是:
30
a: 1
第一句 console.log(++a && 30)
會印出 30,
是因為 console.log(++a && 30)
這句可以看成是:
a++
console.log(a && 30)
所以,
++
放前面的話第一步:會先執行 a++
(這時的 a 會是 1)
第二步:再執行 console.log(a && 30)
這句(這時的 a = 1,所以會印出 30)
在宣告變數時,雖然不用指定變數的型態,但在 JS 運作時,還是會給變數一個型態
變數型態有分幾種:
Primitive 就是「最原始的型別」,分成三種:
typeof
用 typeof
後面加上想看的東西,
typeof
會回傳一個字串,告訴你那個東西是什麼型別
console.log('type of true', typeof true)
會印出:
type of true boolean
null
的型別是 object這是從 JavaScript 早期就存在的性質,沿用到現在
console.log('type of [1]', typeof [1])
console.log('type of {a: 1}}', typeof { a: 1 })
console.log('type of null', typeof null)
console.log('type of undefined', typeof undefined)
console.log('type of function', typeof function(){})
用 typeof
可以得到下面回傳的結果:
type of [1] object
type of {a: 1}} object
type of null object
type of undefined undefined
type of function function
array 是一種資料結構
用來存「性質相似」的資料
object 也是一種資料結構
object 的格式是:
{
key: value,
key: value,
key: value
}
方式一:
用 Number(a)
把字串 a
轉成數字
var a = '58'
var b = 30
console.log(Number(a) + b)
方式二:
用一個 function 叫做 parseInt()
10
代表:我這個數字是「10 進位」var a = '58'
var b = 30
console.log(parseInt(a, 10) + b)
「浮點數」就是「小數」
為了避免碰到「浮點數誤差」,如果可以的話,盡量不要用到小數
例如:
var a = 0.1 + 0.2
console.log(a == 0.3)
結果竟然是回傳 false
var a = 0.1 + 0.2
console.log(a == 0.3)
console.log(a)
用 console.log(a)
來印出 a 的值,竟然是 0.30000000000000004,而不是 0.3
原因是:
電腦在存「小數」時,沒辦法存的那麼精準(會有一些誤差)
因此,我存了 0.2 這個數字,但在電腦裡可能其實是 0.2000000000000003
有些小數可以存的很精準,有些卻不行
更多關於浮點數的說明可參考 [CS101]初心者的計概與 coding 火球術 - 浮點數誤差、數字在電腦是怎麼儲存的
==
與 ===
=
是「賦值」的意思var a = 10
console.log(a = 5)
如果在 console.log()
裡面只放入一個等號(不小心打錯了,少打一個等號),結果就是回傳 5
var a = 20
console.log(a = 5)
// output: 5
因為它的執行順序會是:
a = 5
console.log(a)
var a = 20 == 20
console.log(a)
// output: true
會回傳 true 是因為:它的執行順序會是「從右執行到左」
20 == 20
,先判斷 20 是否等於 20(是 true)var a = true
console.log(a)
就會印出 truevar a = (20 == 20)
console.log(a)
==
與 ===
的差異==
不會「比較兩個東西的型態」兩個等號: ==
就是「判斷這兩個東西是不是相等」,但不會去管型態
var a = 20
console.log(a == 20)
// output: true
console.log(30 == '30')
//output: true
可以把「空字串」看成是「0」
console.log(0 == '')
// output: true
===
會「比較兩個東西的型態」三個等號: ===
就是「判斷這兩個東西是不是相等」,且會去比較型態
number 不等於 string
console.log(30 === '30')
// output: false
console.log(0 === '')
// output: false
===
來判斷,這樣最不容易出錯如果只有使用 ==
,可能會因為沒有判斷出型態不相同而出現問題
例如:console.log(30 + '30')
會等於 3030 而不是 60
補充教學文章 從博物館寄物櫃理解變數儲存模型
console.log([] === []) // output: false console.log([1] === [1]) // output: false console.log({} === {}) // output: false console.log({a: 1} === {a: 1}) // output: false
以上的判斷結果都會是 false,為什麼呢?
注意!在做題目的時後,當我要判斷「陣列 arr
是否為一個空陣列」
這是錯誤寫法:
因為就算 arr
是一個空陣列,跟小括號裡面的 []
也會是不同的兩個空陣列(在兩個不同的記憶體位置上)
if(arr === []){
return 'empty'
}
這是正確寫法:
用「陣列長度是否等於 0」來判斷「arr
是否為空陣列」
if(arr.length === 0){
return 'empty'
}
在變數裡面放入一個 number,例如: var a = 20
a === 20
就會是 true 沒錯
因為:變數的箱子裡面放的就是「20」這個值
var a = 20
console.log(a === 20)
// output: true
這裡以 number 為例:
a = 3
a === b
就是 true(因為 a, b 的值都等於 3)var a = 3
var b = a
console.log('a:', a, 'b:', b)
console.log(a === b)
output:a: 3 b: 3
true
現在,我把 b 的值改成 8a === b
就是 false(因為 a, b 的值不相等了)var a = 3
var b = a
b = 8
console.log('a:', a, 'b:', b)
console.log(a === b)
output:a: 3 b: 8
false
總結:例如:var a = 3
在變數 a 裡面存的就是「3」這個值
但是,對於「陣列、物件」來說,卻不是這樣子
var obj = {
a: 1
}
console.log(obj === {a: 1})
// output: false
下圖是錯的:
在物件 obj
的箱子裡面,放的並不是 {a: 1}
這個值
typeof 陣列、物件
的型別都是 object
):var obj = {a: 1}
當我宣告變數 obj
等於一個物件 {a: 1}
時,它會先把物件放在某個地方叫做「記憶體位置」(是一個真正在電腦裡的記憶體位置,例如這裡假設的 0x01
)
在 obj
裡面存的東西其實是這個「記憶體位置 0x01
」,而不是直接存 {a: 1}
這個物件
我們沒有任何方法可以得知這個「記憶體位置」是哪裡,因為在使用 JavaScript 時,它就會直接把「這個記憶體位置上面的東西 {a: 1}
」給我
obj2
var obj = {a: 1}
var obj2 = {a: 1}
儘管兩個物件存的“數值”是一樣的(都是 a: 1
),但是 obj2
會再建立一個「新的記憶體位置」(這裡假設是 0x05
)
obj
不會等於 obj2
,因為「兩個物件存的記憶體位置是不同的」(兩個箱子裡面放的是不同的記憶體位置)怎麼做才會讓兩個物件相等?
我讓 obj
= obj2
,這樣兩個物件就會相等了
var obj = {
a: 1
}
var obj2 = obj
console.log(obj === obj2)
// output: true
當我改變 obj2.a
的值(讓 obj2.a = 50
),也會同時改到 obj.a
的值
var obj = {
a: 1
}
var obj2 = obj
obj2.a = 50
console.log('obj', obj)
console.log('obj2', obj2)
console.log(obj === obj2)
output:
obj { a: 50 }
obj2 { a: 50 }
true
原因是:
這也跟「記憶體位置」有關
因為此時的 obj
和 obj2
,裡面的東西是「指向同一個記憶體位置」
我原本有一個變數 obj
,裡面存的記憶體位置是 0x01
var obj2 = obj
,背後做的事情是:宣告一個變數 obj2
,指向同一個物件 {a: 1}
(所以記憶體位置也是存 0x01
)
因此,這也是為什麼 obj === obj2
會是 true(因為是指向同一個記憶體位置、同一個物件)
obj2.a = 50
,背後做的事情是:針對「同一個記憶體位置」上的物件,去新增、修改屬性.
的方式去新增、修改屬性,例如:在 obj2
新增一個 obj2.b = 200
,obj
也會一起被新增 obj.b = 200
會改到 0x01
這個記憶體位置裡面的 a,因此,在改 obj2.a
的同時,也會一起改到 obj.a
=
」讓 obj2
等於一個新的物件 {b: 1}
,背後做的事情是:obj2
會指向一個新的記憶體位置obj2
就會斷開原本跟 obj
的連結,並建立另一個新的記憶體位置 0x05
,因此 obj
就不會跟 obj2
相等了(是兩個不同的物件)
var obj = {
a: 1
}
var obj2 = obj
obj2.a = 50
obj2 = {b: 1}
console.log('obj', obj)
console.log('obj2', obj2)
console.log(obj === obj2)
output:
obj { a: 50 }
obj2 { b: 1 }
false
]]>變數,就是一個「放東西的箱子」,這個箱子會取一個名稱,之後就可以用箱子的名稱去代指裡面裝的東西
宣告一個變數,例如:
var box = hello
這裡的 =
不是「等於」的意思
=
是「賦值」(賦予值)的意思可以看作是 var box <= hello
代表「我要把 hello 放到 box 裡面」
例如:下面這樣是錯誤的
var 335box = 57
變數的命名方式有分為兩種:
var api_response = 57
var apiResponse = 0
重點是!要統一!用了駝峰式就統一用駝峰式
宣告變數 box 和 BOX 會是兩個不同的變數
var box = hello
var BOX = 456
在 box 裡面沒有裝東西
var box
console.log(box)
會印出 undefined
「undefined」的意思是:有宣告這個變數,但是沒有給值
var box
console.log(boxkkk)
會印出 boxkkk is not defined
「is not defined」的意思是:根本沒有 boxkkk 宣告這個變數
var box = 57
console.log(box & 1)
會印出 1,因為 box 是奇數
++
和 --
var a = 0
a = a + 5
console.log(a)
上面程式碼的意思是:
會先執行 =
右邊的,把 a + 5
算完後的值,再放到 a 裡面
因此會印出 5
a = a + 5
也可以寫成 a += 5
兩種寫法是同樣的意思
var a = 0
a += 5
console.log(a)
a = a - 5
也可以寫成 a -= 5
a += 1
也可以寫成 a++
a -= 1
也可以寫成 a--
因為 +1 和 -1 在 JS 裡面很常用到,所以就決定給它一個專門的運算子來用
++
放在“後面”跟放在“前面”的差別在哪呢?a++
var a = 0
console.log(a++ && 30)
console.log('a:', a)
印出來的結果會是:
0
a: 1
第一句 console.log(a++ && 30)
會印出 0,
是因為 console.log(a++ && 30)
這句可以看成是:
console.log(a && 30)
a++
所以,
++
放後面的話第一步:會先執行 console.log(a && 30)
這句(這時的 a = 0,所以會印出 0)
第二步:再執行 a++
(這時的 a 才會是 1)
++a
var a = 0
console.log(++a && 30)
console.log('a:', a)
印出來的結果會是:
30
a: 1
第一句 console.log(++a && 30)
會印出 30,
是因為 console.log(++a && 30)
這句可以看成是:
a++
console.log(a && 30)
所以,
++
放前面的話第一步:會先執行 a++
(這時的 a 會是 1)
第二步:再執行 console.log(a && 30)
這句(這時的 a = 1,所以會印出 30)
在宣告變數時,雖然不用指定變數的型態,但在 JS 運作時,還是會給變數一個型態
變數型態有分幾種:
Primitive 就是「最原始的型別」,分成三種:
typeof
用 typeof
後面加上想看的東西,
typeof
會回傳一個字串,告訴你那個東西是什麼型別
console.log('type of true', typeof true)
會印出:
type of true boolean
null
的型別是 object這是從 JavaScript 早期就存在的性質,沿用到現在
console.log('type of [1]', typeof [1])
console.log('type of {a: 1}}', typeof { a: 1 })
console.log('type of null', typeof null)
console.log('type of undefined', typeof undefined)
console.log('type of function', typeof function(){})
用 typeof
可以得到下面回傳的結果:
type of [1] object
type of {a: 1}} object
type of null object
type of undefined undefined
type of function function
array 是一種資料結構
用來存「性質相似」的資料
object 也是一種資料結構
object 的格式是:
{
key: value,
key: value,
key: value
}
方式一:
用 Number(a)
把字串 a
轉成數字
var a = '58'
var b = 30
console.log(Number(a) + b)
方式二:
用一個 function 叫做 parseInt()
10
代表:我這個數字是「10 進位」var a = '58'
var b = 30
console.log(parseInt(a, 10) + b)
「浮點數」就是「小數」
為了避免碰到「浮點數誤差」,如果可以的話,盡量不要用到小數
例如:
var a = 0.1 + 0.2
console.log(a == 0.3)
結果竟然是回傳 false
var a = 0.1 + 0.2
console.log(a == 0.3)
console.log(a)
用 console.log(a)
來印出 a 的值,竟然是 0.30000000000000004,而不是 0.3
原因是:
電腦在存「小數」時,沒辦法存的那麼精準(會有一些誤差)
因此,我存了 0.2 這個數字,但在電腦裡可能其實是 0.2000000000000003
有些小數可以存的很精準,有些卻不行
更多關於浮點數的說明可參考 [CS101]初心者的計概與 coding 火球術 - 浮點數誤差、數字在電腦是怎麼儲存的
==
與 ===
=
是「賦值」的意思var a = 10
console.log(a = 5)
如果在 console.log()
裡面只放入一個等號(不小心打錯了,少打一個等號),結果就是回傳 5
var a = 20
console.log(a = 5)
// output: 5
因為它的執行順序會是:
a = 5
console.log(a)
var a = 20 == 20
console.log(a)
// output: true
會回傳 true 是因為:它的執行順序會是「從右執行到左」
20 == 20
,先判斷 20 是否等於 20(是 true)var a = true
console.log(a)
就會印出 truevar a = (20 == 20)
console.log(a)
==
與 ===
的差異==
不會「比較兩個東西的型態」兩個等號: ==
就是「判斷這兩個東西是不是相等」,但不會去管型態
var a = 20
console.log(a == 20)
// output: true
console.log(30 == '30')
//output: true
可以把「空字串」看成是「0」
console.log(0 == '')
// output: true
===
會「比較兩個東西的型態」三個等號: ===
就是「判斷這兩個東西是不是相等」,且會去比較型態
number 不等於 string
console.log(30 === '30')
// output: false
console.log(0 === '')
// output: false
===
來判斷,這樣最不容易出錯如果只有使用 ==
,可能會因為沒有判斷出型態不相同而出現問題
例如:console.log(30 + '30')
會等於 3030 而不是 60
補充教學文章 從博物館寄物櫃理解變數儲存模型
console.log([] === []) // output: false console.log([1] === [1]) // output: false console.log({} === {}) // output: false console.log({a: 1} === {a: 1}) // output: false
以上的判斷結果都會是 false,為什麼呢?
注意!在做題目的時後,當我要判斷「陣列 arr
是否為一個空陣列」
這是錯誤寫法:
因為就算 arr
是一個空陣列,跟小括號裡面的 []
也會是不同的兩個空陣列(在兩個不同的記憶體位置上)
if(arr === []){
return 'empty'
}
這是正確寫法:
用「陣列長度是否等於 0」來判斷「arr
是否為空陣列」
if(arr.length === 0){
return 'empty'
}
在變數裡面放入一個 number,例如: var a = 20
a === 20
就會是 true 沒錯
因為:變數的箱子裡面放的就是「20」這個值
var a = 20
console.log(a === 20)
// output: true
這裡以 number 為例:
a = 3
a === b
就是 true(因為 a, b 的值都等於 3)var a = 3
var b = a
console.log('a:', a, 'b:', b)
console.log(a === b)
output:a: 3 b: 3
true
現在,我把 b 的值改成 8a === b
就是 false(因為 a, b 的值不相等了)var a = 3
var b = a
b = 8
console.log('a:', a, 'b:', b)
console.log(a === b)
output:a: 3 b: 8
false
總結:例如:var a = 3
在變數 a 裡面存的就是「3」這個值
但是,對於「陣列、物件」來說,卻不是這樣子
var obj = {
a: 1
}
console.log(obj === {a: 1})
// output: false
下圖是錯的:
在物件 obj
的箱子裡面,放的並不是 {a: 1}
這個值
typeof 陣列、物件
的型別都是 object
):var obj = {a: 1}
當我宣告變數 obj
等於一個物件 {a: 1}
時,它會先把物件放在某個地方叫做「記憶體位置」(是一個真正在電腦裡的記憶體位置,例如這裡假設的 0x01
)
在 obj
裡面存的東西其實是這個「記憶體位置 0x01
」,而不是直接存 {a: 1}
這個物件
我們沒有任何方法可以得知這個「記憶體位置」是哪裡,因為在使用 JavaScript 時,它就會直接把「這個記憶體位置上面的東西 {a: 1}
」給我
obj2
var obj = {a: 1}
var obj2 = {a: 1}
儘管兩個物件存的“數值”是一樣的(都是 a: 1
),但是 obj2
會再建立一個「新的記憶體位置」(這裡假設是 0x05
)
obj
不會等於 obj2
,因為「兩個物件存的記憶體位置是不同的」(兩個箱子裡面放的是不同的記憶體位置)怎麼做才會讓兩個物件相等?
我讓 obj
= obj2
,這樣兩個物件就會相等了
var obj = {
a: 1
}
var obj2 = obj
console.log(obj === obj2)
// output: true
當我改變 obj2.a
的值(讓 obj2.a = 50
),也會同時改到 obj.a
的值
var obj = {
a: 1
}
var obj2 = obj
obj2.a = 50
console.log('obj', obj)
console.log('obj2', obj2)
console.log(obj === obj2)
output:
obj { a: 50 }
obj2 { a: 50 }
true
原因是:
這也跟「記憶體位置」有關
因為此時的 obj
和 obj2
,裡面的東西是「指向同一個記憶體位置」
我原本有一個變數 obj
,裡面存的記憶體位置是 0x01
var obj2 = obj
,背後做的事情是:宣告一個變數 obj2
,指向同一個物件 {a: 1}
(所以記憶體位置也是存 0x01
)
因此,這也是為什麼 obj === obj2
會是 true(因為是指向同一個記憶體位置、同一個物件)
obj2.a = 50
,背後做的事情是:針對「同一個記憶體位置」上的物件,去新增、修改屬性.
的方式去新增、修改屬性,例如:在 obj2
新增一個 obj2.b = 200
,obj
也會一起被新增 obj.b = 200
會改到 0x01
這個記憶體位置裡面的 a,因此,在改 obj2.a
的同時,也會一起改到 obj.a
=
」讓 obj2
等於一個新的物件 {b: 1}
,背後做的事情是:obj2
會指向一個新的記憶體位置obj2
就會斷開原本跟 obj
的連結,並建立另一個新的記憶體位置 0x05
,因此 obj
就不會跟 obj2
相等了(是兩個不同的物件)
var obj = {
a: 1
}
var obj2 = obj
obj2.a = 50
obj2 = {b: 1}
console.log('obj', obj)
console.log('obj2', obj2)
console.log(obj === obj2)
output:
obj { a: 50 }
obj2 { b: 1 }
false
]]>