関数( ''function'' )は、他の言語のサブルーチンやプロシージャと類似したサブプログラムです。JavaScriptでは関数もオブジェクト（ Functionオブジェクト ）であり第一級関数ですこのことから、JavaScriptを関数型言語とされることもありますが、主要な制御構造が式ではないので一般的な認識ではありません。。
関数は、0個以上のパラメータを受け取り、1つの戻り値を返す（あるいは返さない）プログラムの実行単位です。
多くのプログラム言語では、構文の一部として提供されている「命令」や「組込み関数」で入出力や数値演算などの機能を提供しますが、JavaScriptでは標準組込みクラスとホスト環境の提供するオブジェクトがその役割を担います。
またユーザーが自分で必要な関数やオブジェクトを定義できます。
JavaScriptの関数は function文を使って定義・作成します。
関数定義の構文:
function 関数名 ( 引数1 , 引数2 , … 引数n ) {
  /*   処理内容;   */
  [return [ 戻り値 ];]
}
[https://paiza.io/projects/e9H5iSLoYWng0MKJ4P0ndw?language=javascript 例]:
"use strict";
function /*足し算*/ add(a, b) {
  return a + b;
}
console.log(`\
typeof add = ${typeof add}
add.toString() = ${add.toString()}
add(1, 2) = ${add(1, 2)}
add("abc", "xyz") = ${add("abc", "xyz")}
`);
実行結果:
typeof add = function
add.toString() = function /*足し算*/ add(a, b) {
  return a + b;
}
add(1, 2) = 3
add("abc", "xyz") = abcxyz
関数は、グローバルオブジェクト（windowやglobalThis）に束縛されます。
例えば、 add() は、 globalThis.add() です。
関数を呼出すには、add(1, 2)のように関数の名前の後に()を付けて、()の中に引数として関数に渡す値を入れます。つまり
構文:
関数名 ( [引数1 [, 引数2 [..., 引数n ] ] ] )
なお、上記の関数の内容は数値''a'', ''b''を受け取り、''a''と''b''を足した結果''sum''を返すadd関数の例です。''a''や''b''のように関数が受け取るデータを引数( ''parameter'' )といいます。
作成した関数が値を返す場合、return文を使って返します。return文によって、return文の直後にある値を、呼出し元の関数に返します。また、その値を戻値( ''return value'' )といいます。上記コード例の場合なら a + b の式の値が戻値です。
関数でreturn文が実行されると、制御が呼出し元のコードに移るため、その関数のreturn文以降のコードは実行されません。
上記コード例の場合、関数 add が呼出されると、1は''a''、2は''b''に代入されて、関数の本体が実行され、+演算子式の評価結果が返されます。
''a''や''b''のように関数定義の引数を仮引数( ''argument'' )と呼びます。
三角関数など、いくつかの数学の関数は、たとえばコサイン関数を呼出したい場合には、
数学メソッドの呼出例:
Math.cos(引数)
なお、引数の単位はラジアンです。
"use strict";
console.log(`\
Math.cos(Math.PI / 4) = ${Math.cos(Math.PI / 4)}
Math.cos(Math.PI / 4) ** 2 = ${Math.cos(Math.PI / 4) ** 2}`)
実行結果:
Math.cos(Math.PI / 4) = 0.7071067811865476 
Math.cos(Math.PI / 4) ** 2 = 0.5000000000000001
Mathオブジェクトは、標準組込みオブジェクトなので、importなどの特別な手続きなく、またホスト環境に関係なく使うことができます。
また、Math.cos() などの数学メソッドは、Mathオブジェクトに属し、Mathオブジェクトをnewを使ってインスタンス化することはできません。
Mathオブジェクトには円周率などの定数もMath.PIの形でプロパティとして用意されています。
関数はオブジェクト( ''object'' )の一種であり、ArrayオブジェクトやStringオブジェクトなど、他のオブジェクトと同じように操作できます。
構文:
function ( 引数1 [ , 引数2 [ , 引数3 ] … [ , 引数n ] ] ) {
 // 処理内容
 [ return [ 戻値 ] ;]
}
[https://paiza.io/projects/PGOTjGGYbob-kDX9JVd6gg?language=javascript 例]:
"use strict";
const add = function (a, b) {
  // ２つの引数をとり +演算子を適用して値を返す。
  return a + b;
};
function fsub() {
  // 無名関数を返す関数
  return function (a, b) {
    // 関数オブジェクトの本体
    return a - b;
  };
}
const sub = fsub();
console.log(`\
add(1, 1) = ${add(1, 1)}
add.name = "${add.name}"
typeof add = ${typeof add}
add.toString() = ${add.toString()}
sub(1, 1) = ${sub(1, 1)}
sub.name = "${sub.name}"
typeof sub = ${typeof sub}
sub.toString() = ${sub.toString()}
`);
実行結果:
add(1, 1) = 2
add.name = "add"
typeof add = function
add.toString() = function (a, b) {
  // ２つの引数をとり +演算子を適用して値を返す。
  return a + b;
}
sub(1, 1) = 0
sub.name = ""
typeof sub = function
sub.toString() = function (a, b) {
    // 関数オブジェクトの本体
    return a - b;
  }
関数式では関数本体の関数名は省略可能で、省略された場合に関数式がスカラ変数の初期値であった場合 Function.nameはスカラ変数の変数名になります。
関数の戻り値で関数式を返した場合、Function.nameは "" となります。
（関数呼出し演算子）を使います。
構文:
関数式の値([引数1[,引数2[..., 引数n]]])
関数の中でvarキーワードを用いて宣言された変数は、関数の中からしか見えません。
言い換えれば、関数の中でvarキーワードを用いて宣言された変数と、関数の外で宣言された変数は別物です。
var f = function() {
    var i = 0;
    return i + 1;
};
console.log(f()); // 1
console.log(i); // 0 は表示されず ReferenceError: i is not defined となります。
関数ブロック内での var による変数宣言は、C言語など他言語でいう「ローカル変数」に似ていますがC言語には関数スコープはなく似て非なるものです。
なお、最後の2個のconsole.logの順番を入れ替えると、下記のようになります。
var f = function() {
    var i = 0;
    return i;
};
var i = 1;
console.log(i);   // 1
console.log(f()); // 0
関数ブロック外（トップレベル）での var による変数宣言は、C言語など他言語でいう「グローバル変数」に相当し、その実体はグローバルオブジェクト（典型的には window）のプロパティです。
var x = 0;
x === window.x; // true
{{コラム|var と let そして const|
ECMA2015（ES6）で{    }で囲まれた範囲をスコープとする letと const が導入されました。
let とconst のスコープをブロックスコープと呼びます。
関数本体もブロックなので、let が関数の外から参照されることも有りませんしvar の様な巻き上げも起こりません。letについて詳しくは『JavaScript/変数#let』の節で説明しています。
letとconstが導入されても、varの意味が変わることは有りません。
もし変わったのならば深刻な非互換性を引き起こします。
逆に、var の意味論的な位置づけを保つ必要があったので新しく let と const が導入されたといえます。
なお、関数の内外でletを使用えますし推奨されます。
たとえば、いくつか前の節で紹介した足し算を関数にしたコード例の var を let に置き換えても同様の結果です。
let に置き換え
function add(a, b) {
    let sum = a + b;
    return sum;
}
let three = add(1, 2); // 1 + 2
console.log(three);    // 3
const に置き換え
function add(a, b) {
    const sum = a + b;
    return sum;
}
const three = add(1, 2); // 1 + 2
console.log(three);      // 3
一度しか代入されない変数は、const に置き換えることも出来ます。
このことから、変数の宣言には
という一般則が成り立ちます。
var なし
関数の中で変数を宣言せず代入するとグローバル変数を置き換えます（下記コードのi = 0;の箇所のことです）。
i = 3;
var f = function() {
    i = 0;
    return i + 1;
};
console.log(f()); // 1
console.log(i);   // 0
グローバル変数が置き換えられているので、「3」ではなく（「3」はもはや置き換えによって値が失われた）、「0」が表示されます。
ですが、JavaScript では、このような用法は非推奨です。var のキーワード無しの変数宣言をJavaScriptは非推奨にしているからです。strictモードでは未宣言のグローバル変数への代入は ReferenceError になります。
strictモード下でグローバル変数にアクセスは、globalThis.i = 0; の様にグローバルオブジェクトのプロパティとしてアクセスします。
globalThis.i = 3;
var f = function() {
    globalThis.i = 0;
    return globalThis.i + 1;
};
console.log(f());          // 1
console.log(globalThis.i); // 0
function文による定義や関数式での生成とは別に、関数コンストラクタコンストラクタとは違いますを使って関数を生成する方法もあります。
ただし、関数コンストラクタにより生成する方法は稀にしか使われず、function文か関数式を使うのが一般的です。
[ new ] Function ( 引数1 , 引数2 , 関数の本体 )
関数リテラルと似ていますが、最後の引数が関数の本体であるところが違います。
また、関数コンストラクタでは引数は全て文字列です。
加えて、関数コンストラクタはグローバルスコープで実行される関数のみを生成します。
例:
const add = new Function('a', 'b', 'return a + b');
console.log(add(1, 1)); // 1 + 1 == 2
console.log(add.toString());
/*
function anonymous(a,b
) {
return a + b
}
/prototype|prototype]]
Functionオブジェクトのプロトタイプです。
decodeURI
decodeURIComponent
encodeURI
encodeURIComponent
eval
isFinite
isNaN
parseFloat
parseInt
void
再帰呼出しとは、関数が自分自身を呼出すことをいいます。
再帰呼出しの例:
// 階乗 n!
function factorial(n) {
    return n ?
        n * factorial(n - 1) :
        1;
}
console.log(`factorial(5) = ${factorial(5)}`);
// n 番目のフィボナッチ数
function fibonacci(n) {
    return n < 2 ?
        n :
        fibonacci(n - 2) + fibonacci(n - 1);
}
console.log(`fibonacci(10) = ${fibonacci(10)}`);
// a, b の最大公約数
function gcd(a, b) {
    return b ?
        gcd(b, a % b) :
        Math.abs(a);
}
console.log(`gcd(42, 56) = ${gcd(42, 56)}`);
実行結果:
factorial(5) = 120
fibonacci(10) = 55
gcd(42, 56) = 14
脱出条件を間違えて無限再帰にならないように注意してください。
即時関数とは、定義後その場で評価される関数です。
(function(a, b){
    console.log(a + b); // "3" と表示
})(1, 2);
/arguments/callee|arguments.callee]]プロパティを使用します。
strict モードでの、arguments.callee の使用は TypeError となります。
// 階乗 n!
(function(n){
    return n
         ? n * arguments.callee(n - 1)
         : 1;
})(5);
// n 番目のフィボナッチ数
(function(n){
    return n < 2
         ? n
         : arguments.callee(n - 2) + arguments.callee(n - 1);
})(10);
// a, b の最大公約数
(function(a, b){
    return b
         ? arguments.callee(b, a % b)
         : Math.abs(a);
})(42, 56);
arguments.calleeプロパティを使用せずに無名再帰を行うには、不動点コンビネータ( ''fixed-point operator'' )を用います。
// Z不動点コンビネータ
var Z = function(f) {
    return function(x) {
        return function(y) {
            return f(x(x))(y);
        };
    }(function(x) {
        return function(y) {
            return f(x(x))(y);
        };
    });
};
// 階乗 n!
Z(function(f) {
    return function(n) {
        return n ?
            n * f(n - 1) :
            1;
    };
})(5);
// n 番目のフィボナッチ数
Z(function(f) {
    return function(n) {
        return n < 2 ?
            n :
            f(n - 2) + f(n - 1);
    };
})(10);
// a, b の最大公約数
Z(function(f) {
    return function(a) {
        return function(b) {
            return b ?
                f(b)(a % b) :
                Math.abs(a);
        };
    };
})(42)(56);
ラムダ計算も参照してください。
関数リテラルのもう1つの構文にアロー関数構文があります。
アロー関数では、他の関数リテラル異なる this はアロー関数が宣言された場所によって決まます。
以下は全て同じ意味になります。
var f = function(x) { return `x: ${x}`; }
var f = Function('x','`x: ${x}`');
var f = (x) => { return `x: ${x}`; }
var f = x => { return `x: ${x}`; }
var f = x => `x: ${x}`;
前節の不動点コンビネータをアロー関数を使って書いてみます。
// Z不動点コンビネータ
var Z = f => (
    x => y => f(x(x))(y)
)(x => y => f(x(x))(y))
// 階乗 n!
Z(f => n => n ? n * f(n - 1) : 1)(5)
// n 番目のフィボナッチ数
Z(f => n => n < 2 ? n : f(n - 2) + f(n - 1))(10)
// a, b の最大公約数
Z(f => a => b => b ? f(b)(a % b) : Math.abs(a))(42)(56)
簡素に書くことが出来ることが判ると思う。
{{コラム|比較演算子に擬態したアロー関数に注意|
次のようなコードは常に処理が実行されます。
if (a=>0) {
 // 処理
}
a=>0 は a>=0 の間違えですがエラーとはならず、
(a) => { return 0; } と解され、Booleanコンテキストでは真となってしまったことが原因です。
クロージャ( レキシカルスコープ )で解決する関数のことです。教科書などによく出てくる典型的なクロージャは、次のようなカウンタ変数を用いた例です。
// 関数を返す関数
function f() {
    var i = 0;
    return function() {
        return i++;    // ここで参照される i が問題
    };
};
const g = f();
console.log(typeof g); // function
console.log(g());      // 0
console.log(g());      // 1
console.log(g());      // 2
var i = 0;             // グローバルな i を書き換えても
console.log(g());      // 3 -- 値は変わらない
関数''f''は変数''i''をインクリメントして返す関数を返す関数です。''f()''によって生成された関数''g''を呼出すと、''i''の値が0, 1, 2, ...と1ずつ増やして返されます。ここで''i''の値を書き換えても、変数''g''が示す関数に束縛された''i''の値は変わりません。変数''g''が示す関数と環境はクロージャになっているからです（''g''を呼出したときではなく、''g''が示す関数を定義したときの''i''を参照しています）。
f() 中の変数''i''に注目してください。''i''は外側の関数式の中でvarキーワードを用いて宣言されているので、関数スコープになり、関数定義を出た時点で消滅します。しかし、関数''g''を呼出すと''i''をインクリメントした値が返ってきます。繰り返しになりますが、クロージャとはすべての変数を、呼出した時点ではなく定義した時点で束縛した関数と環境のことでした。f() が呼出され内側の関数式を返した時点の''i''が束縛されたので、グローバルな''i''が見えなくなっても環境の''i''の値を参照しつづけられるというわけです。
ジェネレーター関数は、Generator オブジェクトを返す特殊な関数です。
ジェネレーター関数定義
ジェネレーター関数式
返されたGenerator オブジェクトは反復動作（例えば for..of）と反復構造（例えばスプレッド構文 ...iter）の両方をサポートします。
JavaScriptにありそうでないRangeオブジェクトを作ってみます。
ジェネレーター関数によるRangeの実装:
function* Range(from = 0, to = Infinity) {
    for (let index = from; index <= to; index++) {
        yield index;
    }
}
for (const n of Range(2, 4)) {
    console.log(n)
}
/*
 2
 3
 4
 */
console.log([...Range(1, 10)]);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(Array.from(Range(0, 12)));
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
演算子は（ジェネレータではない）関数の return に相当する制御演算子です。反復動作/反復構造については、改めて詳しく説明したいと思います。
ここでは、ジェネレーター関数/Generatorオブジェクトを定義することで Array や Set の様な反復動作/反復構造をユーザーが実現できるということだけ覚えてください。
オブジェクトのメソッドとして再実装:
let range = {
  start: 0,
  end: Infinity,
  *[Symbol.iterator]() {
    for (let value = this.start; value <= this.end; value++) {
      yield value;
    }
  }
};
range.start = 2;
range.end = 4;
for (const n of range){ console.log(n) }
/*
 2
 3
 4
 */
range.start = 1;
range.end = 10;
console.log([...range]); 
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
range.start = 0;
range.end = 12;
console.log(Array.from(range)); 
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
classとして再実装:
class Range {
  constructor(start = 0, end = Infinity) {
    this.start = start;
    this.end = end;
  }
  *[Symbol.iterator]() {
    for (let value = this.start; value <= this.end; value++) {
      yield value;
    }
  }
  forEach(f) {
    for (let value = this.start; value <= this.end; value++) {
      f(value);
    }
  }
};
r = new Range(2, 4);
r.forEach(x => console.log(x));
/*
 2
 3
 4
 */
console.log([...new Range(1,10)]); 
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(Array.from(new Range(0,12))); 
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
この例では、forEach メソッドも定義し中間配列を生成せずRangeオブジェクト自身で反復を可能にしています。
他の配列の操作(map,filter,find,some,every)も同様に実装することで中間配列を撲滅できます。
varで宣言された変数を関数内で使う場合に、値の代入は代入した場所で行われるが、宣言（および宣言に伴う代入）は関数内のどこでしても関数の先頭でしたことになるという落とし穴が存在します。この挙動はvarの巻き上げ（var hoisting）と呼ばれます。
(function() {
  console.log(dream); // undefined -- なぜかReferenceErrorにならない
  var dream = true;
  console.log(dream); // true
})();
上記コードでは表示結果として、trueが表示されます。
なお、上記コードの様に無名関数を定義すると同時に呼ぶ出すことをは即時実行と呼びます。
関数内でvarで宣言した場合にだけ、巻き上げが発生します。
巻き上げが起きるのは、あくまで関数内での出来事であるので関数を用いてない場所では巻き上げは行われません。
これに巻き込まれないよう、var宣言する前には変数を使わないように注意しましょう。
varの使用にはこのように問題があるので、 var を使わずに const あるいは let を使うことが望ましいです。
const と let はブロックスコープで巻き上げは起こりません。
また strict モードを使用することで、ミススペルなどで偶発的にvar相当の変数を無宣言で作ってしまうことをエラーを上げることで検知できます。
関数にも巻上げが起こます。
関数定義:
func("text");
function func(str) { console.log(str); }
上記のコードは、func() が前方参照になっているにも関わらずエラーなく実行されます（"text"がconsole.logされます）。
関数式でconstで宣言された変数を初期化:
func("text");
const func = function (str) { console.log(str); }
3行目を関数式から関数リテラルに変えconstに保持すると、 ReferenceError: Cannot access 'func' before initialization となります。
関数式でvarで宣言された変数を初期化:
func("text");
var func = function (str) { console.log(str); }
constをvarに変えると、TypeError: func is not a function。
関数式でletで宣言された変数を初期化:
func("text");
let func = function (str) { console.log(str); }
let に変えると、ReferenceError: func is not defined となります。
このように、関数の巻上げは関数式では起こらず関数定義に限られます。
オブジェクトのプロパティが関数の場合を、メソッドあるいは関数プロパティと呼びます。
const obj = {
    val: 42,
    func: function() /* プロパティvalの値をヘッダーつきで文字列化します */ {
        return `val = ${this.val}`;
    },
};
console.log(obj.func()); // "val = 42"
const o2 = { val: 99 };
o2.func = obj.func;
console.log(o2.func()); // "val = 99"
console.log(o2.func.toString()); // 下の3行が表示されます。
// function() /* プロパティvalの値をヘッダーつきで文字列化します */ {
//        return `val = ${this.val}`;
//    }
メソッドは以下のように簡略表記ができます。
const obj = {
    val: 42,
    func() /* プロパティvalの値をヘッダーつきで文字列化します */ {
        return `val = ${this.val}`;
    },
};
console.log(obj.func()); // "val = 42"
const o2 = { val: 99 };
o2.func = obj.func;
console.log(o2.func()); // "val = 99"
console.log(o2.func.toString()); // 下の3行が表示されます。
// func() /* プロパティvalの値をヘッダーつきで文字列化します */ {
//         return `val = ${this.val}`;
//    }
関数の引数が省略した場合の値を定義出来ます。
従来
function Vector3( x, y, z ) {
    this.x = x || 0;
    this.y = y || 0;
    this.z = z || 0;
    this.toString = function() { return `x:${this.x}, y:${this.y}, z:${this.z}` }
}
let v = new Vector3(1, 3);
console.log("" + v); // x:1, y:3, z:0
ECMACScript 2015/ES6以降
function Vector3( x = 0, y = 0, z = 0 ) {
    Object.assign(this, {x, y, z });
    this.toString = function() { return `x:${this.x}, y:${this.y}, z:${this.z}` }
}
let v = new Vector3(1, void 0, 7);
console.log("" + v); // x:1, y:0, z:7
デフォルト引数とともにプロパティ名と同じ関数名による簡略表記をつかっています。
 が渡された場合も既定値が渡されたとみなす。
 をタイプ数少なく書くイディオム。
残余引数( ''Rest parameters'' )は、可変引数数関数を作る仕組みですhttps://262.ecma-international.org/#sec-functiondeclarationinstantiation ECMAScriptR 2020 Language Specification :: 9.2.10 FunctionDeclarationInstantiation ( func, argumentsList )。
従来は arguments を使うところですが、strict モードで禁止になり非推奨なのでES6以降は残余引数を使います。
例:
function sum(...args) {
  return args.reduce((result, current) => result + current, 0);
}
const ary = [ 2, 3, 5, 7, 11 ];
console.log(sum(...ary)); // 28
 残余引数構文、引数リストを配列として保持します。
 よく似ているがスプレッド構文で異なる構文です。
JavaScriptでは、ES6以降、分割代入という機能を使用して、引数の受け取り方をより柔軟にすることができます。
具体的には、以下のように引数を分割代入することができます。
 
function greet({name, age}) {
  console.log(`Hello, ${name}! You are ${age} years old.`);
}
const person = {name: 'Alice', age: 30};
greet(person);
この場合、引数としてpersonオブジェクトを渡していますが、関数内では分割代入を使用して、nameとageを抽出しています。
また、分割代入を使用して、デフォルト値を指定することもできます。
 
function greet({name = 'Anonymous', age = 18}) {
  console.log(`Hello, ${name}! You are ${age} years old.`);
}
greet({name: 'Bob'});
この場合、nameには渡されたオブジェクトのnameプロパティが、ageにはデフォルト値の18が設定されます。ただし、渡されたオブジェクトにageプロパティがある場合は、その値が優先されます。
分割代入を使用することで、引数の受け取り方をより柔軟にすることができますが、あまり複雑にすると可読性が低下することもあるため、適切なバランスを保つ必要があります。
JavaScriptの分割代入では、オブジェクトだけでなく配列に対しても使うことができます。また、残余引数(rest parameters)を使って、可変長引数を受け取ることもできます。これらを組み合わせることで、柔軟に引数を受け取ることができます。
例えば、以下のように可変長引数を受け取りつつ、先頭の要素を変数に分割代入することができます。
 
function greet([name, ...others], message) {
  console.log(`${message}, ${name}!`);
  console.log(`Other members: ${others.join(', ')}`);
}
const members = ['Alice', 'Bob', 'Charlie'];
greet(members, 'Welcome');
この場合、greet関数は配列を受け取り、先頭の要素をnameに、残りの要素をothersに分割代入しています。また、第二引数としてmessageを受け取っています。
このように分割代入と残余引数を組み合わせることで、可変長引数を受け取りつつ、必要な要素を柔軟に受け取ることができます。ただし、引数の受け取り方が複雑になりすぎないように、適切なバランスを保つ必要があります。
関数の引数の数は、Function.prototype.length プロパティhttps://262.ecma-international.org/#sec-function-instances-length ECMAScriptR 2020 Language Specification :: 19.2.4.1 lengthで得ることが出来ます。
例:
function add(a, b) {
    return a  + b;
}
function sum(...args) {
  return args.reduce((result, current) => result + current, 0);
}
console.log(add.length); // 2
console.log(sum.length); // 0!
console.log([].forEach.length); // 1
 普通に引数数2
 残余引数構文
 2を返します；普通です
 残余引数構文は0を返します；1ではありません
なので省略可能な引数の数は含まれません。
オブジェクトの生成と初期化のためのメソッドをコンストラクタと呼び、new 演算子と組み合わせて使われます。
function Complex(real, imag = 0) {
    this.real = real;
    this.imag = imag;
}
let c = new Complex(10, 14);
console.log(c); // Complex {real: 10, imag: 14}
Complex.prototype.toString = function() {
    return `r=${this.real}, i=${this.imag}`;
}
console.log(c.toString()); // r=10, i=14
同等の機能を ES6 で追加された class 宣言を使って書くと... 
class Complex {
    constructor(real, imag = 0) {
      this.real = real;
      this.imag = imag;
    }
    toString() { return `r=${this.real}, i=${this.imag}` }
}
let c = new Complex(10, 14);
console.log(c); // Complex {real: 10, imag: 14}
console.log(c.toString()); // r=10, i=14
のようになります。
一見すると prototype は関係ないように見えますが
console.log(Complex.prototype.toString.toString()) // "toString() { return `r=${this.real}, i=${this.imag}` }"
と、実体は function をコンストラクタに使った記法と同じく prototype プロパティにメソッドが追加されています。