1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

JavaScript基礎概念勉強ノート6

Last updated at Posted at 2022-04-27

初めに

今回はES6一部の基礎概念を勉強ノートとしてまとめてみました。

Scope in JavaScript

基本的に以下の四種類があります。

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

letconstBlock 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を使用するのはおすすめしません。
varFunction Scopeだけ持っていて、さきほど書いたようにJSファイルを動かせるというのは、main()関数式を呼び出すような感覚で、varが一番上にあるGlobal Scope(main()というFunction Scope)で変数宣言をしても問題ありませんが、それ以外のBlock ScopeModule 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を指定するより、もっとわかりやすくなっていると思います。
分割代入構文に続いて、importexportの話も少し触れていきたいと思います。

import & export

es5_module.exports
// ES5
function add(a, b) {
  return a + b
}
module.exports = add
es5_require()
const add = require('./utils') // .js省略可
console.log(add(3, 5)) // 8

module.exportsrequire()の書き方が少し古いけれど、この書き方では馴染みがあってどんな環境でも使えてやりやすい方法だと思います。
実際、Node.js環境でimportexportという新しい書き方にするには、事前作業が必要です。
まずは$ npm initpackage.jsonファイルを作って、そのなかに

package.json
{
  "type": "module"
}

入れるだけ。
参考資料はこちら↓
javascript - "Uncaught SyntaxError: Cannot use import statement outside a module" when importing ECMAScript 6 - Stack Overflow

あともう一つ大事なポイントがあります。
import...from... 使うとき、リソースの拡張子は省略不可です

utils.js
// 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
}
import.js
// 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に参照)

utils.js
// 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を使って関数式ならFunctionFnなどつければ読みやすくなりますね。

import.js
// 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のところみたい、一個ずつより全部を導入してからドット法で指定していくのが一番よく使う方法です。

utils.js
// example4
export default function add(a, b) {
  return a + b
}
export const PI = 3.14
note: use 'export default' => import add from './utils.js'
import.js
// example4
import add, { PI } from './utils.js'
console.log(add(3, 5)) // 8
console.log(PI) // 3.14

ここでは少しnamed exportsdefault 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 ] ]

arr6arr7も、もう一度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 operatorrest 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ではかなり基礎レベルの概念をまとめました。
ほかには非同期処理、静的プロパティ・メソッド等々がありますが、引き続き頑張っていきたいと思います。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?