本文專為熟悉 C# 等後端語言的開發者介紹 JavaScript 的主要特性,重點在於語言間的差異和前端開發中常用的 JavaScript 特性。
變數宣告與作用域
JavaScript 中的變數宣告有三種方式:var
、let
與 const
。對比 C#:
JavaScript | C# 等價 | 特性對比 |
---|---|---|
var (ES5) | - | 函式作用域,可重複宣告,會被提升 |
let (ES6) | var | 區塊作用域,類似 C# 的變數行為 |
const (ES6) | readonly | 不可重新賦值,但物件內容可修改 |
提升 (Hoisting)
// 變數提升 (Hoisting)
console.log(x); // undefined (而非錯誤)
var x = 10;
// 相當於
var x;
console.log(x);
x = 10;
// 與 C# 不同,C# 會有編譯錯誤
區塊作用域
// JavaScript
{
let blockScoped = "只在區塊內可見";
var functionScoped = "在函式內都可見";
}
console.log(functionScoped); // 正常運作
console.log(blockScoped); // ReferenceError
// C# 中所有變數都是區塊作用域
全域變數
// JavaScript
var globalVar = "我會成為 window 物件的屬性";
let scopedVar = "我不會成為 window 物件的屬性";
console.log(window.globalVar); // "我會成為 window 物件的屬性"
console.log(window.scopedVar); // undefined
// C# 中沒有類似的全域物件概念
型別系統
JavaScript 是弱型別語言:
JavaScript | C# | 主要差異 |
---|---|---|
動態型別 | 靜態型別 | JS 變數可隨時改變型別;C# 變數型別固定 |
隱含型別轉換 | 明確型別轉換 | JS 會自動進行型別轉換;C# 需要明確轉換 |
7種原始型別 | 多種值型別和參考型別 | JS 的型別更簡單但也更容易出錯 |
原始型別
// 數字 - 所有數值都是浮點數,沒有整數/浮點數區分
const num = 123;
const decimal = 123.45;
// C# 有 int, long, float, double, decimal 等
// 字串 - 單引號或雙引號皆可
const str1 = 'Hello';
const str2 = "World";
// C# 字串必須使用雙引號,單引號表示 char
// 布林值
const bool = true;
// 與 C# 相同
// undefined - 變數未賦值
let notDefined;
console.log(notDefined); // undefined
// C# 沒有 undefined,參考型別為 null,值型別有預設值
// null - 明確的空值
const empty = null;
// C# 中 null 只適用於參考型別
// symbol - 唯一識別符(C# 沒有直接等價物)
const sym = Symbol('id');
// bigint - 大整數(C# 的 BigInteger)
const bigInt = 9007199254740992n;
型別轉換的陷阱
// JavaScript 自動型別轉換
console.log('5' == 5); // true
console.log('' == 0); // true
console.log(true == 1); // true
// 嚴格相等避免型別轉換
console.log('5' === 5); // false
console.log(1 === true); // false
// C# 中無法直接比較不同型別
// if ("5" == 5) // 編譯錯誤
函式與委派
JavaScript 的函式是一級公民,而 C# 使用委派或 Lambda 表達式:
函式宣告的方式
// 函式宣告 (Function Declaration)
function add(a, b) {
return a + b;
}
// 函式表達式 (Function Expression)
const multiply = function(a, b) {
return a * b;
};
// 箭頭函式 (Arrow Function) - ES6
const subtract = (a, b) => a - b;
C# 等價:
public int Add(int a, int b) { return a + b; }
Func<int, int, int> multiply = (a, b) => a * b;
Func<int, int, int> subtract = (a, b) => a - b;
函式的獨特特性
// 函式作為參數傳遞
function calculate(a, b, operation) {
return operation(a, b);
}
// 與 C# 的委派或 Action/Func 類似,但更輕量
// this 關鍵字行為
function normalFunction() {
console.log(this); // 依呼叫方式而定
}
const arrowFunction = () => {
console.log(this); // 總是捕獲定義時的 this
};
// C# 中 this 總是參考當前實例,無動態變化
物件與類別
JavaScript 物件比 C# 物件更為動態:
物件宣告與操作
// 物件字面值建立 - 無需類別定義
const person = {
name: 'John',
age: 30,
greet() {
return `Hello, I'm ${this.name}`;
}
};
// 動態新增屬性
person.location = 'Taipei';
// 動態刪除屬性
delete person.age;
// C# 必須先定義類別,無法隨意新增或刪除屬性
ES6 類別語法糖
ES6 的 class
語法糖,實際上是 function
的語法糖。詳細解釋請見附錄。
// JavaScript 類別 (ES6)
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, I'm ${this.name}`;
}
}
C# 類似但有更多特性:
public class Person {
public string Name { get; set; }
public int Age { get; set; }
public Person(string name, int age) {
Name = name;
Age = age;
}
public string Greet() {
return $"Hello, I'm {Name}";
}
}
繼承
// JavaScript 繼承
class Employee extends Person {
constructor(name, age, company) {
super(name, age);
this.company = company;
}
work() {
return `${this.name} works at ${this.company}`;
}
}
C# 等價:
public class Employee : Person {
public string Company { get; set; }
public Employee(string name, int age, string company) : base(name, age) {
Company = company;
}
public string Work() {
return $"{Name} works at {Company}";
}
}
原型鏈與繼承
JavaScript 使用原型鏈進行繼承,這與 C# 的類繼承有本質不同:
// 原型鏈繼承
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return `${this.name} makes a noise`;
};
function Dog(name, breed) {
Animal.call(this, name); // 呼叫「父類」建構函式
this.breed = breed;
}
// 設定繼承關係
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// 覆寫方法
Dog.prototype.speak = function() {
return `${this.name} barks`;
};
// C# 使用更傳統的類繼承模型,更嚴格且結構化
異步模型
JavaScript 與 C# 的異步模型有相似之處,但有不同的實現:
// JavaScript 回調 (早期模式)
function fetchData(callback) {
setTimeout(() => {
callback('Data');
}, 1000);
}
// Promise (ES6) - 類似 C# 的 Task
function fetchDataPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data');
}, 1000);
});
}
// Async/Await (ES2017) - 類似 C# 的 async/await
async function getData() {
try {
const data = await fetchDataPromise();
return data;
} catch (error) {
console.error(error);
}
}
C# 等價:
public async Task<string> GetDataAsync() {
try {
string data = await FetchDataAsync();
return data;
} catch (Exception ex) {
Console.WriteLine(ex);
return null;
}
}
模組系統
JavaScript 模組系統與 C# 的命名空間和組件概念不同:
// ES6 模組
// module.js
export const PI = 3.14159;
export function square(x) {
return x * x;
}
// 在另一個檔案
import { PI, square } from './module.js';
// 或
import * as Math from './module.js';
C# 使用命名空間和 using
指令:
using System;
using MyNamespace;
附錄:作為物件導向語言的 JavaScript
我們可以使用 Object
建構子函式來建立物件,例如:
const obj = new Object();
obj.name = 'John';
obj.age = 20;
我們也可以進一步使用建構子函式(Constructor Function)來建立物件,例如:
function Person(name, age) {
// `this` 指向的是新建立的物件
this.name = name;
this.age = age;
}
const person = new Person('John', 20);
console.log(person); // { name: 'John', age: 20 }
這時候,被建構出來的實例(Instance,即 person
),原型鏈會指向建構子函式的 prototype
屬性(在這裡是 Person.prototype
),並且能在 instanceof
中顯示出實例化關係。
// 延續上面的例子
console.log(person.__proto__ === Person.prototype); // true
console.log(person instanceof Person); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
在 ES6 中,我們可以使用 class
來假裝自己是在寫物件導向的程式語言,例如:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
const person = new Person('John', 20);
person.sayHello(); // 'Hello, my name is John and I am 20 years old.'
class
的語法糖,實際上是 function
的語法糖。
對於繼承,我們也可以有兩種寫法:
// 使用 function 宣告
function Animal(name) {
this.name = name;
}
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
const dog = new Dog('Buddy', 'Labrador');
// 使用 class 宣告
class Animal {
constructor(name) {
this.name = name;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
}
const dog = new Dog('Buddy', 'Labrador');
它們會有相同的以下執行結果:
console.log(dog); // { name: 'Buddy', breed: 'Labrador' }
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
// 檢查原型鏈
console.log(Animal.prototype.isPrototypeOf(dog)); // true