初めに
今回はES6一部の基礎概念を勉強ノートとしてまとめてみました。
Scope in JavaScript
基本的に以下の四種類があります。
- Global Scope:どこでもアクセスできる。
- Function Scope:関数式内だけ。
- Block Scope:ブロック({})内だけ。
- Module Scope:モジュール内だけ。
参考資料はこちら↓
function - What is the scope of variables in JavaScript? - Starck Overflow
javascript - ES6 module scope - Starck Overflow
let
& const
- Block Scope
// different between let and const
// declared by const means variable can not be changed or covered
const a = 1
a = 2
console.log(a)
// TypeError: Assignment to constant variable.
// declared by let means variable can be changed or covered
let b = 1
b = 2
console.log(b) // 2
// const and let only exist in "block scope"
// block scope in global, it can be accessed from any scope
const score4 = 60
if (score4 >= 60) {
console.log(score4) // 60
}
console.log(score4) // 60
// block scope: {}, it only exist its {}
const score5 = 60
if (score5 >= 60) {
let text = '合格'
console.log(text) // 合格
}
// console.log(text) // ReferenceError: text is not defined
let
と const
はBlock Scopeだけ持っていて、その中に生きている。Global Scopeに宣言されたらどこでもアクセスできるようになりますが、Global Scopeを持っているわけではなく、実際Global Scopeというのはmain()(JSの本体)という関数式内で一番上のBlock Scope({})と言ってもいいぐらいです。
一番上にあるGlobal Scopeに対して、Local Scopeという言葉もよく使われる。
Global Scopeで宣言された変数、どこのLocal Scopeからでもアクセスできる。(let
& const
& var
共通)
var
// var has function scope (and global scope)
// global scope
var score1 = 60
if (score1 >= 60) {
console.log('pass') // pass
}
// function scope? "if...else" is not a function
if (score2 >= 60) { // 1. can not find "score2", break out
var score2 = 60
console.log('pass')
}
console.log(score2) // undefined // "if...else" doesn't execute
// var in block scope means global variable declaration
const score6 = 60
if (score6 >= 60) {
var text = '合格' // "if...else" is not function, so it's a global variable declaration
console.log(text) // 合格
}
console.log(text) // 合格
//
function pass(n) {
const score6 = 60
if (score6 >= 60) {
var text = '合格'
console.log(text) // 合格
}
console.log(text) // 合格
}
pass()
// console.log(text) // text is not defined
正直なところ、var
を使用するのはおすすめしません。
var
はFunction Scopeだけ持っていて、さきほど書いたようにJSファイルを動かせるというのは、main()関数式を呼び出すような感覚で、var
が一番上にあるGlobal Scope(main()というFunction Scope)で変数宣言をしても問題ありませんが、それ以外のBlock ScopeやModule Scopeなど、JSを書く過程のなか不注意による問題が出てくるかもしれません。
例えば、if
のようなBlock Scope、その中に宣言したらif
条件が満たされてない状態でif
が実行されず、Block Scopeのなかの値がアクセスできず見つからないことになった。(undefined:あるはずの値が見つかりません)
また、特定のFunction Scope内のBlock Scopeではなく、GlobalでBlock Scopeに宣言するというのは、main()というFunction Scopeの変数宣言、つまり変数がGlobal Variableになってしまいます。
var
宣言は生きる範疇が広すぎる(main()Function Scope)のが問題、変数をreassignすることもできるので、コードが長ければ長いほど値の不一致性/不安定性が高まる。
Undeclared variable
// always remember to declare variable
score3 = 60 // 1. if we don't declare it, it would be a global variable
if (score3 >= 60) { // 2.
// var score3 // it's not related to be declared by var or not
console.log('pass') // pass // 3.
}
// doesn't work
// var score // 1.
// if (score >= 60) { // 2. score didn't be assigned any value, break out
// score = 60
// console.log('pass')
// }
宣言を忘れたら、変数が強制的にグローバル変数に転換されてしまいます。
(ほかの議題も触れているので、後日補足していきたいと思います。)
Template literals
// ES5 string joint
let str1 = '' + '\n' +
'abcd' + '\n' +
'abcde'
console.log(str1)
/*
abcd
abcde
*/
function sayHi(name) {
console.log('Hello, ' + name + ' now is ' + new Date())
}
sayHi('Nick') // Hello, Nick now is Fri Apr 22 2022 10:31:38
// ES6 string joint
let str2 = `
Hello,
Nick
`
console.log(str2)
/*
Hello,
Nick
*/
function hello(name) {
console.log(`Hello, ${name} now is ${new Date()}`)
// console.log(`Hello, ${name.toUpperCase()} now is ${new Date()}`)
}
hello('Nick')
hello('Mary')
// Hello, Nick now is Fri Apr 22 2022 10: 43: 08
// Hello, Mary now is Fri Apr 22 2022 10: 43: 08
Web開発のとき、よくテンプレート文字列が使用しています。
${}
のなかは、変数や関数など入れることが可能で、console.log()
で出力か、値としてreturn
もできる。
Destructuring assignment
// Array
// ES5
const arr = [1, 2, 3, 4]
// let first = arr[0]
// let second = arr[1]
// let third = arr[2]
// let fourth = arr[3]
// console.log(second, third) // 2 3
// ES6 destructuring assignment
let [first, second, third, fourth] = arr
console.log(second, third) // 2 3
let [one] = arr
console.log(one) // 1
let [ichi, ni] = arr
console.log(ichi, ni) // 1 2
// note: variables in left-hand side, corresponding object or array in right-hand side (the source variable)
// note: extract required array elements into distinct variables
自分からみれば、分割代入構文というのは要素自身を変数として宣言し、right-hand side
は値のアクセス元を指定するみたいな作法です。
配列は比較的に簡単で、インデックスの位置が合えば変数名が自由にネーミングしてもよいのです。
しかし下のようにオブジェクトには気を付けないといけないところがあります。
// Object
// ES5
const obj = {
name: 'Nick',
age: 30,
address: 'America',
family: {
Father: 'Mick',
Mather: 'Mary'
}
}
// let name = obj.name
// let age = obj.age
// let address = obj.address
// console.log(address) // America
// ES6 destructuring assignment
// const { name, age, address } = obj
// console.log(address) // America
// const { name, age, add } = obj
// console.log(add) // undefined
// note: be careful that the object is corresponding to the key
まず、変数名はオブジェクトのkey
と同じでなければなりません。key-value pairなので理解しやすいと思います。
// let { family } = obj
// console.log(family) // { father: 'Mick' }
// let { father } = family
// console.log(father) // Mick
let { family: { Father, Mather } } = obj
console.log(Father) // Mick
console.log(Mather) // Mary
console.log(family) // family is not defined
// note: 'family' seems like a tree name, not a variable
全部ではなく、オブジェクトにあるkey
、一つか複数を取り出すことも可能です。そしてなによりも、ツリーみたいにその下にある値を取得することもできる。(値のアクセス元をちゃんと指定していれば)
// destructuring assignment in function
// before
// function test(obj) {
// console.log(obj.a) // 1
// }
// test({
// a: 1,
// b: 2
// })
// after
function test({ a, b }) {
console.log(a) // 1
console.log(b) // 2
}
test({
a: 1,
b: 2
})
関数式の引数(argument)も分割代入構文ができる。従来のドット法でkey
を指定するより、もっとわかりやすくなっていると思います。
分割代入構文に続いて、import
とexport
の話も少し触れていきたいと思います。
import
& export
// ES5
function add(a, b) {
return a + b
}
module.exports = add
const add = require('./utils') // .js省略可
console.log(add(3, 5)) // 8
module.exports
とrequire()
の書き方が少し古いけれど、この書き方では馴染みがあってどんな環境でも使えてやりやすい方法だと思います。
実際、Node.js
環境でimport
とexport
という新しい書き方にするには、事前作業が必要です。
まずは$ npm init
でpackage.json
ファイルを作って、そのなかに
{
"type": "module"
}
入れるだけ。
参考資料はこちら↓
javascript - "Uncaught SyntaxError: Cannot use import statement outside a module" when importing ECMAScript 6 - Stack Overflow
あともう一つ大事なポイントがあります。
import
...from
... 使うとき、リソースの拡張子は省略不可です
// ES6
// example1
export function add(a, b) {
return a + b
}
export const PI = 3.14
// example2
function add(a, b) {
return a + b
}
const PI = 3.14
export {
add,
PI
}
// ES6
// example1 & example2
import { add, PI } from './utils.js' // Node.js環境では.js省略不可
console.log(add(3, 5)) // 8
console.log(PI) // 3.14
// if we use node.js to run it, it will show SyntaxError
// SyntaxError: Cannot use import statement outside a module
/* solution for node.js:
add
{
"type": "module"
}
into package.json
(Solution for the script tag in HTML file, please check the Reference)
Reference: https://stackoverflow.com/questions/58211880/uncaught-syntaxerror-cannot-use-import-statement-outside-a-module-when-import
*/
/* important memo:
you should always use the full file name, like 'utils.js' to import,
otherwise, it will still show 'Error [ERR_MODULE_NOT_FOUND]'
Reference: https://stackoverflow.com/questions/61291633/expressjs-is-return-error-err-module-not-found-if-i-import-the-file-without-j
*/
example1 & example2のように、書き方としては一個ずつでexport
しても、オブジェクトでexport
しても、import
したらfunction名、変数名そのまま使えるようになる。(もちろん名前を変えることも可能 ⇒ example3に参照)
// example3
function add(a, b) {
return a + b
}
const PI = 3.14
export {
add as addFunction, // change export alias
PI
}
utils.jsファイルが大きくなり、変数と関数が混じっている状態でimport
してもよくわからないことがよくあります。なのでexport
するときalias(別名)として、as
を使って関数式ならFunctionかFnなどつければ読みやすくなりますね。
// ES6
// example3
import { addFunction, PI } from './utils.js'
console.log(addFunction(3, 5)) // 8
console.log(PI) // 3.14
import { addFunction as a, PI } from './utils.js' // change import alias
console.log(a(3, 5)) // 8
console.log(PI) // 3.14
// for all
import * as utils from './utils.js' // * usually means anything
console.log(utils.addFunction(3, 5)) // 8
console.log(utils.PI) // 3.14
もちろんimport
もその別名として導入しなければならない。もう一度名前を変えたいなら、import
のとき、as
を使って別名を書いていればいいです。
また、for allのところみたい、一個ずつより全部を導入してからドット法で指定していくのが一番よく使う方法です。
// example4
export default function add(a, b) {
return a + b
}
export const PI = 3.14
note: use 'export default' => import add from './utils.js'
// example4
import add, { PI } from './utils.js'
console.log(add(3, 5)) // 8
console.log(PI) // 3.14
ここでは少しnamed exports
とdefault export
の違いを書きたいと思います。
named exports
はモジュール内は0個以上存在することが可能に対し、
default export
は1個だけ指定することができる。
つまりdefault export
はこのモジュール内一番重要な要素として扱われることで、
なくても大丈夫が、あるとしたら1個限定です。
その代わりにnamed exports
何個存在しても構いません。
example1 ~ example3 の書き方はnamed exports
example4 の書き方はdefault export
Default parameters
function repeat(str, times) {
console.log(times) // undefined
return str.repeat(times)
}
console.log(repeat('abc')) // *empty string*
// if we don't give any value to parameters, it will be undefined
// "undefined" means it should be a value but can't be found
// "null" means this value doesn't exist
// use default parameters
function repeat1(str, times = 5) {
console.log(times) // 5
return str.repeat(times)
}
console.log(repeat1('abc')) // abcabcabcabcabc
// even though we didn't give any value as an argument, it always keep default value itself
function repeat2(str = 'hello', times = 5) {
return str.repeat(times)
}
console.log(repeat2()) // hellohellohellohellohello
デフォルト引数は、引数がundefinedだったとき自動的にデフォルト値を渡すパラメータです。
上の例のようにとてもわかりやすいと思います。
しかし分割代入構文と一緒に使うなら、気を付けないといけないところがいくつあります。
// use with destructuring assignment
const obj = {
b: 2
}
// const { a, b } = obj
// console.log(a, b) // undefined 2
// const { a = 123, b } = obj
// console.log(a, b) // 123 2
const obj1 = {
a: 1
}
const { a = 123, b = 'hello' } = obj1
console.log(a, b) // 1 hello
/*
note: when we use destructuring assignment which puts the default value inside,
if the original object had value itself, it won't be changed.
*/
// note: default value only show up when it was undefined state
もともと値が存在している場合は、デフォルト値は使われない。
// note: but you should be careful the original object didn't be changed
const obj2 = {
d: 2
}
const { c = 123, d = 'hello' } = obj2
console.log(c, d) // 123 2
console.log(obj2) // { d: 2 }
console.log(obj2.c) // undefined
/*
if you try to access the default value,
it is always undefined (for example, "c" didn't exist in obj2),
so default parameters are only a temporary value when we call.
"c" didn't exist, and we didn't assign any value to "c".
*/
また、デフォルト値が設定したとしても元のオブジェクトは変えられません。
もちろん、そのデフォルト値をアクセスしようとしても見つかりません。
Spread operator
let arr = [1, 2, 3]
let arr2 = [4, 5, 6, arr]
console.log(arr2) // [ 4, 5, 6, [ 1, 2, 3 ] ]
// use spread operator
let arr3 = [4, 5, 6, ...arr]
console.log(arr3) // [ 4, 5, 6, 1, 2, 3 ]
arr3 = [4, ...arr, 5, 6]
console.log(arr3) // [ 4, 1, 2, 3, 5, 6 ]
function add(a, b, c) {
return a + b + c
}
console.log(add(arr)) // 1,2,3undefinedundefined
// note: a = arr, b = undefined, c = undefined
console.log(add(...arr)) // 6
console.log(add(...arr2)) // 15 // a = 4, b = 5, c = 6
console.log(add(...arr3)) // 7 // a = 4, b = 1, c = 2
スプレッド構文は、中身の要素を展開するパラメータです。
しかし上の例のように、展開すると言っても要素全部入れられるとは言えない。
関数の要求する引数を越えたら無視される。
let obj1 = {
a: 1,
b: 2
}
let obj2 = {
d: 4
}
let obj3 = {
...obj1,
c: 3,
...obj2
}
console.log(obj3) // { a: 1, b: 2, c: 3, d: 4 }
オブジェクトの中でも要素展開できますが、
let obj4 = {
a: 1,
b: 2
}
// let obj5 = {
// ...obj4,
// b: 3
// }
// console.log(obj5) // { a: 1, b: 3 }
// note: when it's the same as the key, it will be replaced by latter
let obj5 = {
b: 3,
...obj4
}
console.log(obj5) // { b: 2, a: 1 }
同じkey
の要素が後のものに上書きされてしまいます。
また、オブジェクト型の厳密等価(===)も、
// copy an array
// let arr4 = [1, 2, 3]
// let arr5 = arr4
// console.log(arr4 === arr5) // true
// they refer to the same address, it wasn't a copy
let arr4 = [1, 2, 3]
let arr5 = [...arr4]
console.log(arr4 === arr5) // false
// because spread operator, arr5 take value from arr4, not address
let nestedArray = [4]
let arr6 = [1, 2, 3, nestedArray]
let arr7 = [...arr6]
console.log(arr6) // [ 1, 2, 3, [ 4 ] ]
console.log(arr7) // [ 1, 2, 3, [ 4 ] ]
console.log(arr6 === arr7) // false
// but if the element was an assigned array,
console.log(arr6[3] === arr7[3]) // true
展開されたのが同じ要素であれば、どこに展開されても厳密等価がtrue
になります。
// if we chang the value (by new address)
nestedArray = [7, 8, 9]
console.log(arr6) // [ 1, 2, 3, [ 4 ] ]
console.log(arr7) // [ 1, 2, 3, [ 4 ] ]
// at this time, nestedArray in arr6 & arr7 was still assigned by old address
// passing by sharing
// but if you reassign it
arr6 = [1, 2, 3, nestedArray]
arr7 = [...arr6]
console.log(arr6) // [ 1, 2, 3, [ 7, 8, 9 ] ]
console.log(arr7) // [ 1, 2, 3, [ 7, 8, 9 ] ]
arr6
もarr7
も、もう一度nestedArray
をreassignする前、古いアドレスを記憶していて、nestedArray
の中身がどう変更されても変わらずnestedArray
の古いアドレスの値を記憶している。
オブジェクトにも同じ状況が起こる。
// copy an object
// let obj6 = {
// a: 1,
// b: 2
// }
// let obj7 = obj6
// console.log(obj6, obj7, obj6 === obj7)
// { a: 1, b: 2 } { a: 1, b: 2 } true
let obj6 = {
a: 1,
b: 2
}
let obj7 = {
...obj6
}
console.log(obj6, obj7, obj6 === obj7)
// { a: 1, b: 2 } { a: 1, b: 2 } false
obj6 = {
a: 3,
b: 4
}
console.log(obj7) // { a: 1, b: 2 } // old address
obj7 = {
...obj6
}
console.log(obj7) // { a: 3, b: 4 }
rest parameters
// difference between spread operator
// Array
let arr = [1, 2, 3]
let arr2 = [...arr, 4] // spread operator
console.log(arr2) // [ 1, 2, 3, 4 ]
// rest parameters (with destructuring assignment)
let arr3 = [1, 2, 3, 4, 5]
let [first, ...rest] = arr3
console.log(first) // 1
console.log(rest) // [ 2, 3, 4, 5 ]
// put the rest of elements in arr3 into variable "rest"
// let [one, ...rest, last] = arr3
// console.log(rest) // SyntaxError: Rest element must be last element
残余引数は残りの要素を梱包して、一つの変数に入れるような感じのパラメータです。
残りの要素への方法なので、一番下のように書いたらSyntaxErrorが出てしまいます。
オブジェクトなら、
// Object
// let obj1 = {
// a: 1,
// b: 2,
// c: 3
// }
// let { a, ...obj2 } = obj1
// console.log(a, obj2) // 1 { b: 2, c: 3 }
let obj3 = {
a: 1,
b: 2
}
let obj4 = {
...obj3, // spread operator
c: 3
}
let { a, ...restObject } = obj4
console.log(a, restObject) // 1 { b: 2, c: 3 }
console.log(obj4) // { a: 1, b: 2, c: 3 }
let { ...restAll } = obj4
console.log(restAll) // { a: 1, b: 2, c: 3 }
console.log(obj4) // { a: 1, b: 2, c: 3 }
obj4
のようにspread operator
で要素を展開して、rest parameters
で要素をまとめることも可能です。
ただ一つ注意すべきところがあります。分割代入構文と一緒に使用するとき、
a
の出力は1、
restObject
の出力は{ b: 2, c: 3 }
です。
配列の例でも、
let arr3 = [1, 2, 3, 4, 5]
let [first, ...rest] = arr3
console.log(first) // 1
console.log(rest) // [ 2, 3, 4, 5 ]
console.log(arr3) // [ 1, 2, 3, 4, 5 ]
つまり残余引数は、要素を梱包するのは今にいる環境のデータ形態と関連している。
とすると、こんな感じになるかな。
const restObject = {
b:2
c:3
}
const rest = [2, 3, 4, 5]
let {a, ...restObject} = obj4 // 要素展開
let {first, ...rest} = arr3 // 要素展開
spread operator
とrest parameters
は使い方が分けられて使っているが、本質は一緒です。
argumentsとの違い
// function
function add(...args) { // args is array
console.log(typeof args) // object
console.log(args) // [ 1, 2 ]
console.log(arguments) // [Arguments] { '0': 1, '1': 2 }
console.log(arguments[0], arguments[1]) // 1 2
return args[0] + args[1]
}
console.log(add(1, 2)) // 3
/*
note: "args" looks like arguments, the difference is
"arguments" is an array-like object, then "...args" is a real array
*/
functionのなかで引数たちのボックスとしてのargumentsとの違いは、
spread operator
は本当のオブジェクトとして要素を扱っている。(この点はrest parameters
も)
Arrow function
let arr = [1, 2, 3, 4, 5]
// console.log(
// arr
// .filter(function (value) {
// return value > 1
// })
// .map(function (value) {
// return value * 2
// })
// )
// [ 4, 6, 8, 10 ]
// console.log(
// arr
// .filter((value) => {
// return value > 1
// })
// .map((value) => {
// return value * 2
// })
// )
// console.log(
// arr
// .filter(value => {
// return value > 1
// })
// .map(value => {
// return value * 2
// })
// )
console.log(
arr
.filter(value => value > 1)
.map(value => value * 2)
)
// note: it can be written by one line when the function only returns a value
// memo: Reserved Word 'this'
アロー関数式は伝統の書き方より可読性が高くなり、書きやすく読みやすくなります。
参考資料のまとめ
function - What is the scope of variables in JavaScript? - Starck Overflow
javascript - ES6 module scope - Starck Overflow
javascript - "Uncaught SyntaxError: Cannot use import statement outside a module" when importing ECMAScript 6 - Stack Overflow
感想
今回はES6ではかなり基礎レベルの概念をまとめました。
ほかには非同期処理、静的プロパティ・メソッド等々がありますが、引き続き頑張っていきたいと思います。