URI は下記のような構成をしています (RFC3986)。

各パートの区切り文字として、次のような予約文字 (reserved characters) が定義されており、例えば query 部分のデータとしてこれらの文字を含む場合は、適切にパーセントエンコーディングしてやる必要があります。
reserved    = gen-delims / sub-delims
gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"
sub-delims  = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
sub-delims に分類された文字をエンコードすべきかは、作成するシステムによりますが、少なくとも & や = などは、query 文字列のキー&バリューの区切りに使用されるのでパーセントエンコーディングの必要があるでしょう。
一方で、URI の各パート内にそのまま含められる文字として、次のような非予約文字 (nonreserved characters) も定義されています。
unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
これらは、URI のどのパートにおいてもそのまま使用できる文字です。
これらの文字もパーセントエンコーディングで表すことはできますが(例: A→%41)、無意味なエンコーディングは避けた方がよいでしょう(RFC3986 においても、そのようなパーセントエンコーディングは行うべきではないとされています)。
JavaScript には、URI のパーセントエンコード(およびデコード)を行うための関数として下記のものが用意されています。
/ や ? などの予約語はエスケープされません。encodeURI によってエンコードされた文字列を元に戻します。-、_、.、!、~、*、(、)。encodeURIComponent によってエンコードされた文字列を元に戻します。下記の表は、変換前の文字 (ch) が、encodeURI 関数と encodeURIComponent 関数によってどのようにエンコードされるかを示しています(表の生成に使用したスクリプト)。
| ch | encodeURI(ch) | encodeURIComponent(ch) | 
|---|---|---|
| あ | %E3%81%82 | %E3%81%82 | 
| ` ` (スペース) | %20 | %20 | 
| A | A | A | 
| ; | ; | %3B | 
| , | , | %2C | 
| / | / | %2F | 
| ? | ? | %3F | 
| : | : | %3A | 
| @ | @ | %40 | 
| & | & | %26 | 
| = | = | %3D | 
| + | + | %2B | 
| $ | $ | %24 | 
| # | # | %23 | 
| - | - | - | 
| _ | _ | _ | 
| . | . | . | 
| ! | ! | ! | 
| ~ | ~ | ~ | 
| * | * | * | 
| ' | ' | ' | 
| ( | ( | ( | 
| ) | ) | ) | 
| [ | %5B | %5B | 
| ] | %5D | %5D | 
URI を構成する際に、パーセントエンコーディングが必要になる主なケースは、query 部分の値としてユーザ入力値を使用するケースでしょう。
例えば、ユーザが Web サイトのフォーム上で Nuts&Milk のような値を入力した場合、その値を query 部分の値として扱うには、& を %26 にエスケープしなければいけません。
const userInput = 'Nuts&Milk';
console.log(encodeURIComponent(userInput));  //=> 'Nuts%26Milk'
query 部分の文字列として、key1=val1&key2=val2&key3=val3 といった 3 対のキー&バリューで構成したい場合は、val1、val2、val3 のそれぞれの値を encodeURIComponent でエスケープしてから結合しなければならないことに注意してください。
3 対のキー&バリューを & や = で結合した後で、まとめて encodeURIComponent で変換してしまうと、セパレータ文字として扱うべき & や = までエスケープされてしまいます。
JavaScript でユーザ入力を含む query 文字列を構成する場合、下記のようなユーティリティ関数を使用できます。
// キー&バリューのオブジェクトから query 文字列を作成する
function buildQuery(params) {
  const esc = encodeURIComponent;
  return Object.keys(params).map(k => {
    return esc(k) + '=' + esc(params[k]);
  }).join('&');
}
ECMAScript 2017 の Object.entries() を使用すると、下記のように記述できます。
function buildQuery(params) {
  return Object.entries(params)
    .map(pair => pair.map(encodeURIComponent).join('='))
    .join('&');
}
const params = {
  key1: 'a&b',  // ユーザー入力で構成されていると仮定
  key2: 'c&d'
};
const query = buildQuery(params);
console.log(query);  //=> 'key1=a%26b&key2=c%26d'
Node.js では、組み込みの querystring モジュール を使ってクエリ文字列を構築することができます。
const querystring = require('querystring');
const query = querystring.stringify({ key1: 'a&b', key2: 'c&d' });
console.log(query);  //=> 'key1=a%26b&key2=c%26d'