跳到內容
Go back

JavaScript 與前端開發基礎(2):從 C# 開發者視角

Published:  at  01:17 PM

本文專為熟悉 C# 等後端語言的開發者介紹 JavaScript 的主要特性,重點在於語言間的差異和前端開發中常用的 JavaScript 特性。

AI 簡報

變數宣告與作用域

JavaScript 中的變數宣告有三種方式:varletconst。對比 C#:

JavaScriptC# 等價特性對比
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 是弱型別語言:

JavaScriptC#主要差異
動態型別靜態型別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

Suggest Changes

Next Post
JavaScript 與前端開發基礎(1):環境篇