您好,登錄后才能下訂單哦!
這篇文章主要講解了“怎么解決JavaScript相關的問題”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“怎么解決JavaScript相關的問題”吧!
一、如何在 window 對象上顯式設置屬性
對于使用過 JavaScript 的開發者來說,對于 window.MyNamespace = window.MyNamespace || {}; 這行代碼并不會陌生。為了避免開發過程中出現沖突,我們一般會為某些功能設置獨立的命名空間。
然而,在 TS 中對于 window.MyNamespace = window.MyNamespace || {}; 這行代碼,TS 編譯器會提示以下異常信息:
Property 'MyNamespace' does not exist on type 'Window & typeof globalThis'.(2339)
以上異常信息是說在 Window & typeof globalThis 交叉類型上不存在MyNamespace 屬性。那么如何解決這個問題呢?最簡單的方式就是使用類型斷言:
(window as any).MyNamespace = {};
雖然使用 any 大法可以解決上述問題,但更好的方式是擴展 lib.dom.d.ts 文件中的Window 接口來解決上述問題,具體方式如下:
declare interface Window { MyNamespace: any; } window.MyNamespace = window.MyNamespace || {};
下面我們再來看一下 lib.dom.d.ts 文件中聲明的 Window 接口:
/** * A window containing a DOM document; the document property * points to the DOM document loaded in that window. */ interface Window extends EventTarget, AnimationFrameProvider, GlobalEventHandlers, WindowEventHandlers, WindowLocalStorage, WindowOrWorkerGlobalScope, WindowSessionStorage { // 已省略大部分內容 readonly devicePixelRatio: number; readonly document: Document; readonly top: Window; readonly window: Window & typeof globalThis; addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; removeEventListener<K extends keyof WindowEventMap>(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | EventListenerOptions): void; [index: number]: Window; }
在上面我們聲明了兩個相同名稱的 Window 接口,這時并不會造成沖突。TypeScript 會自動進行接口合并,即把雙方的成員放到一個同名的接口中。
二、如何為對象動態分配屬性
在 JavaScript 中,我們可以很容易地為對象動態分配屬性,比如:
let developer = {}; developer.name = "semlinker";
以上代碼在 JavaScript 中可以正常運行,但在 TypeScript 中,編譯器會提示以下異常信息:
Property 'name' does not exist on type '{}'.(2339)
{} 類型表示一個沒有包含成員的對象,所以該類型沒有包含 name 屬性。為了解決這個問題,我們可以聲明一個 LooseObject 類型:
interface LooseObject { [key: string]: any }
該類型使用 索引簽名 的形式描述 LooseObject 類型可以接受 key 類型是字符串,值的類型是 any 類型的字段。有了 LooseObject 類型之后,我們就可以通過以下方式來解決上述問題:
interface LooseObject { [key: string]: any } let developer: LooseObject = {}; developer.name = "semlinker";
對于 LooseObject 類型來說,它的約束是很寬松的。在一些應用場景中,我們除了希望能支持動態的屬性之外,也希望能夠聲明一些必選和可選的屬性。
比如對于一個表示開發者的 Developer 接口來說,我們希望它的 name 屬性是必填,而 age 屬性是可選的,此外還支持動態地設置字符串類型的屬性。針對這個需求我們可以這樣做:
interface Developer { name: string; age?: number; [key: string]: any } let developer: Developer = { name: "semlinker" }; developer.age = 30; developer.city = "XiaMen";
其實除了使用 索引簽名 之外,我們也可以使用 TypeScript 內置的工具類型 Record來定義 Developer 接口:
// type Record<K extends string | number | symbol, T> = { [P in K]: T; } interface Developer extends Record<string, any> { name: string; age?: number; } let developer: Developer = { name: "semlinker" }; developer.age = 30; developer.city = "XiaMen";
三、如何理解泛型中的 <T>
對于剛接觸 TypeScript 泛型的讀者來說,首次看到
參考上面的圖片,當我們調用 identity
其中 T 代表 Type,在定義泛型時通常用作第一個類型變量名稱。但實際上 T 可以用任何有效名稱代替。除了 T 之外,以下是常見泛型變量代表的意思:
K(Key):表示對象中的鍵類型;
V(Value):表示對象中的值類型;
E(Element):表示元素類型。
其實并不是只能定義一個類型變量,我們可以引入希望定義的任何數量的類型變量。比如我們引入一個新的類型變量 U,用于擴展我們定義的 identity 函數:
function identity <T, U>(value: T, message: U) : T { console.log(message); return value; } console.log(identity<Number, string>(68, "Semlinker"));
除了為類型變量顯式設定值之外,一種更常見的做法是使編譯器自動選擇這些類型,從而使代碼更簡潔。我們可以完全省略尖括號,比如:
function identity <T, U>(value: T, message: U) : T { console.log(message); return value; } console.log(identity(68, "Semlinker"));
對于上述代碼,編譯器足夠聰明,能夠知道我們的參數類型,并將它們賦值給 T 和 U,而不需要開發人員顯式指定它們。
四、如何理解裝飾器的作用
在 TypeScript 中裝飾器分為類裝飾器、屬性裝飾器、方法裝飾器和參數裝飾器四大類。裝飾器的本質是一個函數,通過裝飾器我們可以方便地定義與對象相關的元數據。
比如在 ionic-native 項目中,它使用 Plugin 裝飾器來定義 IonicNative 中 Device 插件的相關信息:
@Plugin({ pluginName: 'Device', plugin: 'cordova-plugin-device', pluginRef: 'device', repo: 'https://github.com/apache/cordova-plugin-device', platforms: ['Android', 'Browser', 'iOS', 'macOS', 'Windows'], }) @Injectable() export class Device extends IonicNativePlugin {}
在以上代碼中 Plugin 函數被稱為裝飾器工廠,調用該函數之后會返回類裝飾器,用于裝飾 Device 類。Plugin 工廠函數的定義如下:
// https://github.com/ionic-team/ionic-native/blob/v3.x/src/%40ionic-native/core/decorators.ts export function Plugin(config: PluginConfig): ClassDecorator { return function(cls: any) { // 把config對象中屬性,作為靜態屬性添加到cls類上 for (let prop in config) { cls[prop] = config[prop]; } cls['installed'] = function(printWarning?: boolean) { return !!getPlugin(config.pluginRef); }; // 省略其他內容 return cls; }; }
通過觀察 Plugin 工廠函數的方法簽名,我們可以知道調用該函數之后會返回 ClassDecorator 類型的對象,其中 ClassDecorator 類型的聲明如下所示:
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
類裝飾器顧名思義,就是用來裝飾類的。它接收一個參數 —— target: TFunction,表示被裝飾器的類。介紹完上述內容之后,我們來看另一個問題 @Plugin({...}) 中的 @ 符號有什么用?
其實 @Plugin({...}) 中的 @ 符號只是語法糖,為什么說是語法糖呢?這里我們來看一下編譯生成的 ES5 代碼:
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var Device = /** @class */ (function (_super) { __extends(Device, _super); function Device() { return _super !== null && _super.apply(this, arguments) || this; } Device = __decorate([ Plugin({ pluginName: 'Device', plugin: 'cordova-plugin-device', pluginRef: 'device', repo: 'https://github.com/apache/cordova-plugin-device', platforms: ['Android', 'Browser', 'iOS', 'macOS', 'Windows'], }), Injectable() ], Device); return Device; }(IonicNativePlugin));
通過生成的代碼可知,@Plugin({...}) 和 @Injectable() 最終會被轉換成普通的方法調用,它們的調用結果最終會以數組的形式作為參數傳遞給 __decorate 函數,而在 __decorate 函數內部會以 Device 類作為參數調用各自的類型裝飾器,從而擴展對應的功能。
此外,如果你有使用過 Angular,相信你對以下代碼并不會陌生。
const API_URL = new InjectionToken('apiUrl'); @Injectable() export class HttpService { constructor( private httpClient: HttpClient, @Inject(API_URL) private apiUrl: string ) {} }
在 Injectable 類裝飾器修飾的 HttpService 類中,我們通過構造注入的方式注入了用于處理 HTTP 請求的 HttpClient 依賴對象。而通過 Inject 參數裝飾器注入了API_URL 對應的對象,這種方式我們稱之為依賴注入(Dependency Injection)。
關于什么是依賴注入,在 TS 中如何實現依賴注入功能,出于篇幅考慮,這里阿寶哥就不繼續展開了。感興趣的小伙伴可以閱讀 “了不起的 IoC 與 DI” 這篇文章。
五、如何理解函數重載的作用
5.1 可愛又可恨的聯合類型
由于 JavaScript 是一個動態語言,我們通常會使用不同類型的參數來調用同一個函數,該函數會根據不同的參數而返回不同的類型的調用結果:
function add(x, y) { return x + y; } add(1, 2); // 3 add("1", "2"); //"12"
由于 TypeScript 是 JavaScript 的超集,因此以上的代碼可以直接在 TypeScript 中使用,但當 TypeScript 編譯器開啟 noImplicitAny 的配置項時,以上代碼會提示以下錯誤信息:
Parameter 'x' implicitly has an 'any' type. Parameter 'y' implicitly has an 'any' type.
該信息告訴我們參數 x 和參數 y 隱式具有 any 類型。為了解決這個問題,我們可以為參數設置一個類型。因為我們希望 add 函數同時支持 string 和 number 類型,因此我們可以定義一個 string | number 聯合類型,同時我們為該聯合類型取個別名:
type Combinable = string | number;
在定義完 Combinable 聯合類型后,我們來更新一下 add 函數:
function add(a: Combinable, b: Combinable) { if (typeof a === 'string' || typeof b === 'string') { return a.toString() + b.toString(); } return a + b; }
為 add 函數的參數顯式設置類型之后,之前錯誤的提示消息就消失了。那么此時的 add 函數就完美了么,我們來實際測試一下:
const result = add('semlinker', ' kakuqo'); result.split(' ');
在上面代碼中,我們分別使用 'semlinker' 和 ' kakuqo' 這兩個字符串作為參數調用 add 函數,并把調用結果保存到一個名為 result 的變量上,這時候我們想當然的認為此時 result 的變量的類型為 string,所以我們就可以正常調用字符串對象上的 split 方法。但這時 TypeScript 編譯器又出現以下錯誤信息了:
Property 'split' does not exist on type 'Combinable'. Property 'split' does not exist on type 'number'.
很明顯 Combinable 和 number 類型的對象上并不存在 split 屬性。問題又來了,那如何解決呢?這時我們就可以利用 TypeScript 提供的函數重載。
5.2 函數重載
函數重載或方法重載是使用相同名稱和不同參數數量或類型創建多個方法的一種能力。
function add(a: number, b: number): number; function add(a: string, b: string): string; function add(a: string, b: number): string; function add(a: number, b: string): string; function add(a: Combinable, b: Combinable) { // type Combinable = string | number; if (typeof a === 'string' || typeof b === 'string') { return a.toString() + b.toString(); } return a + b; }
在以上代碼中,我們為 add 函數提供了多個函數類型定義,從而實現函數的重載。在 TypeScript 中除了可以重載普通函數之外,我們還可以重載類中的成員方法。
方法重載是指在同一個類中方法同名,參數不同(參數類型不同、參數個數不同或參數個數相同時參數的先后順序不同),調用時根據實參的形式,選擇與它匹配的方法執行操作的一種技術。所以類中成員方法滿足重載的條件是:在同一個類中,方法名相同且參數列表不同。下面我們來舉一個成員方法重載的例子:
class Calculator { add(a: number, b: number): number; add(a: string, b: string): string; add(a: string, b: number): string; add(a: number, b: string): string; add(a: Combinable, b: Combinable) { if (typeof a === 'string' || typeof b === 'string') { return a.toString() + b.toString(); } return a + b; } } const calculator = new Calculator(); const result = calculator.add('Semlinker', ' Kakuqo');
這里需要注意的是,當 TypeScript 編譯器處理函數重載時,它會查找重載列表,嘗試使用第一個重載定義。 如果匹配的話就使用這個。 因此,在定義重載的時候,一定要把最精確的定義放在最前面。另外在 Calculator 類中,add(a: Combinable, b: Combinable){ } 并不是重載列表的一部分,因此對于 add 成員方法來說,我們只定義了四個重載方法。
六、interfaces 與 type 之間有什么區別
6.1 Objects/Functions
接口和類型別名都可以用來描述對象的形狀或函數簽名:
接口
interface Point { x: number; y: number; } interface SetPoint { (x: number, y: number): void; }
類型別名
type Point = { x: number; y: number; }; type SetPoint = (x: number, y: number) => void;
6.2 Other Types
與接口類型不一樣,類型別名可以用于一些其他類型,比如原始類型、聯合類型和元組:
// primitive type Name = string; // object type PartialPointX = { x: number; }; type PartialPointY = { y: number; }; // union type PartialPoint = PartialPointX | PartialPointY; // tuple type Data = [number, string];
6.3 Extend
接口和類型別名都能夠被擴展,但語法有所不同。此外,接口和類型別名不是互斥的。接口可以擴展類型別名,而反過來是不行的。
Interface extends interface
interface PartialPointX { x: number; } interface Point extends PartialPointX { y: number; }
Type alias extends type alias
type PartialPointX = { x: number; }; type Point = PartialPointX & { y: number; };
Interface extends type alias
type PartialPointX = { x: number; }; interface Point extends PartialPointX { y: number; }
Type alias extends interface
interface PartialPointX { x: number; } type Point = PartialPointX & { y: number; };
6.4 Implements
類可以以相同的方式實現接口或類型別名,但類不能實現使用類型別名定義的聯合類型:
interface Point { x: number; y: number; } class SomePoint implements Point { x = 1; y = 2; } type Point2 = { x: number; y: number; }; class SomePoint2 implements Point2 { x = 1; y = 2; } type PartialPoint = { x: number; } | { y: number; }; // A class can only implement an object type or // intersection of object types with statically known members. class SomePartialPoint implements PartialPoint { // Error x = 1; y = 2; }
6.5 Declaration merging
與類型別名不同,接口可以定義多次,會被自動合并為單個接口。
interface Point { x: number; } interface Point { y: number; } const point: Point = { x: 1, y: 2 };
七、object, Object 和 {} 之間有什么區別
7.1 object 類型
object 類型是:TypeScript 2.2 引入的新類型,它用于表示非原始類型。
// node_modules/typescript/lib/lib.es5.d.ts interface ObjectConstructor { create(o: object | null): any; // ... } const proto = {}; Object.create(proto); // OK Object.create(null); // OK Object.create(undefined); // Error Object.create(1337); // Error Object.create(true); // Error Object.create("oops"); // Error
7.2 Object 類型
Object 類型:它是所有 Object 類的實例的類型,它由以下兩個接口來定義:
Object 接口定義了 Object.prototype 原型對象上的屬性;
// node_modules/typescript/lib/lib.es5.d.ts interface Object { constructor: Function; toString(): string; toLocaleString(): string; valueOf(): Object; hasOwnProperty(v: PropertyKey): boolean; isPrototypeOf(v: Object): boolean; propertyIsEnumerable(v: PropertyKey): boolean; }
ObjectConstructor 接口定義了 Object 類的屬性。
// node_modules/typescript/lib/lib.es5.d.ts interface ObjectConstructor { /** Invocation via `new` */ new(value?: any): Object; /** Invocation via function calls */ (value?: any): any; readonly prototype: Object; getPrototypeOf(o: any): any; // ··· } declare var Object: ObjectConstructor;
Object 類的所有實例都繼承了 Object 接口中的所有屬性。
7.3 {} 類型
{} 類型描述了一個沒有成員的對象。當你試圖訪問這樣一個對象的任意屬性時,TypeScript 會產生一個編譯時錯誤。
// Type {} const obj = {}; // Error: Property 'prop' does not exist on type '{}'. obj.prop = "semlinker";
但是,你仍然可以使用在 Object 類型上定義的所有屬性和方法,這些屬性和方法可通過 JavaScript 的原型鏈隱式地使用:
// Type {} const obj = {}; // "[object Object]" obj.toString();
八、數字枚舉與字符串枚舉之間有什么區別
8.1 數字枚舉
在 JavaScript 中布爾類型的變量含有有限范圍的值,即 true 和 false。而在 TypeScript 中利用枚舉,你也可以自定義相似的類型:
enum NoYes { No, Yes, }
No 和 Yes 被稱為枚舉 NoYes 的成員。每個枚舉成員都有一個 name 和一個 value。數字枚舉成員值的默認類型是 number 類型。也就是說,每個成員的值都是一個數字:
enum NoYes { No, Yes, } assert.equal(NoYes.No, 0); assert.equal(NoYes.Yes, 1);
除了讓 TypeScript 為我們指定枚舉成員的值之外,我們還可以手動賦值:
enum NoYes { No = 0, Yes = 1, }
這種通過等號的顯式賦值稱為 initializer。如果枚舉中某個成員的值使用顯式方式賦值,但后續成員未顯示賦值, TypeScript 會基于當前成員的值加 1 作為后續成員的值。
8.2 字符串枚舉
除了數字枚舉,我們還可以使用字符串作為枚舉成員值:
enum NoYes { No = 'No', Yes = 'Yes', } assert.equal(NoYes.No, 'No'); assert.equal(NoYes.Yes, 'Yes');
8.3 數字枚舉 vs 字符串枚舉
數字枚舉與字符串枚舉有什么區別呢?這里我們來分別看一下數字枚舉和字符串枚舉編譯的結果:
數字枚舉編譯結果
"use strict"; var NoYes; (function (NoYes) { NoYes[NoYes["No"] = 0] = "No"; NoYes[NoYes["Yes"] = 1] = "Yes"; })(NoYes || (NoYes = {}));
字符串枚舉編譯結果
"use strict"; var NoYes; (function (NoYes) { NoYes["No"] = "No"; NoYes["Yes"] = "Yes"; })(NoYes || (NoYes = {}));
通過觀察以上結果,我們知道數值枚舉除了支持 從成員名稱到成員值 的普通映射之外,它還支持 從成員值到成員名稱 的反向映射。另外,對于純字符串枚舉,我們不能省略任何初始化程序。而數字枚舉如果沒有顯式設置值時,則會使用默認值進行初始化。
8.4 為數字枚舉分配越界值
講到數字枚舉,這里我們再來看個問題:
const enum Fonum { a = 1, b = 2 } let value: Fonum = 12; // Ok
相信很多讀者看到 let value: Fonum = 12; 這一行,TS 編譯器并未提示任何錯誤會感到驚訝。很明顯數字 12 并不是 Fonum 枚舉的成員。 為什么會這樣呢?我們來看一下 TypeScript issues 26362 中 DanielRosenwasser 大佬的回答:
The behavior is motivated by bitwise operations. There are times when SomeFlag.Foo | SomeFlag.Bar is intended to produce another SomeFlag. Instead you end up with number, and you don't want to have to cast back to SomeFlag.
該行為是由按位運算引起的。有時 SomeFlag.Foo | SomeFlag.Bar 用于生成另一個 SomeFlag。相反,你最終得到的是數字,并且你不想強制回退到 SomeFlag。
了解完上述內容,我們再來看一下 let value: Fonum = 12; 這個語句,該語句 TS 編譯器不會報錯,是因為數字 12 是可以通過 Fonum 已有的枚舉成員計算而得。
let value: Fonum = Fonum.a << Fonum.b << Fonum.a | Fonum.a << Fonum.b; // 12
九、使用 # 定義的私有字段與 private 修飾符定義字段有什么區別
在 TypeScript 3.8 版本就開始支持 ECMAScript 私有字段,使用方式如下:
class Person { #name: string; constructor(name: string) { this.#name = name; } greet() { console.log(`Hello, my name is ${this.#name}!`); } } let semlinker = new Person("Semlinker"); semlinker.#name; // ~~~~~ // Property '#name' is not accessible outside class 'Person' // because it has a private identifier.
與常規屬性(甚至使用 private 修飾符聲明的屬性)不同,私有字段要牢記以下規則:
私有字段以 # 字符開頭,有時我們稱之為私有名稱;
每個私有字段名稱都唯一地限定于其包含的類;
不能在私有字段上使用 TypeScript 可訪問性修飾符(如 public 或 private);
私有字段不能在包含的類之外訪問,甚至不能被檢測到。
說到這里使用 # 定義的私有字段與 private 修飾符定義字段有什么區別呢?現在我們先來看一個 private 的示例:
class Person { constructor(private name: string){} } let person = new Person("Semlinker"); console.log(person.name);
在上面代碼中,我們創建了一個 Person 類,該類中使用 private 修飾符定義了一個私有屬性 name,接著使用該類創建一個 person 對象,然后通過 person.name 來訪問 person 對象的私有屬性,這時 TypeScript 編譯器會提示以下異常:
Property 'name' is private and only accessible within class 'Person'.(2341)
那如何解決這個異常呢?當然你可以使用類型斷言把 person 轉為 any 類型:
console.log((person as any).name);
通過這種方式雖然解決了 TypeScript 編譯器的異常提示,但是在運行時我們還是可以訪問到 Person 類內部的私有屬性,為什么會這樣呢?我們來看一下編譯生成的 ES5 代碼,也許你就知道答案了:
var Person = /** @class */ (function () { function Person(name) { this.name = name; } return Person; }()); var person = new Person("Semlinker"); console.log(person.name);
這時相信有些小伙伴會好奇,在 TypeScript 3.8 以上版本通過 # 號定義的私有字段編譯后會生成什么代碼:
class Person { #name: string; constructor(name: string) { this.#name = name; } greet() { console.log(`Hello, my name is ${this.#name}!`); } }
以上代碼目標設置為 ES2015,會編譯生成以下代碼:
"use strict"; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to set private field on non-instance"); } privateMap.set(receiver, value); return value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to get private field on non-instance"); } return privateMap.get(receiver); }; var _name; class Person { constructor(name) { _name.set(this, void 0); __classPrivateFieldSet(this, _name, name); } greet() { console.log(`Hello, my name is ${__classPrivateFieldGet(this, _name)}!`); } } _name = new WeakMap();
通過觀察上述代碼,使用 # 號定義的 ECMAScript 私有字段,會通過 WeakMap 對象來存儲,同時編譯器會生成 __classPrivateFieldSet 和 __classPrivateFieldGet這兩個方法用于設置值和獲取值。
感謝各位的閱讀,以上就是“怎么解決JavaScript相關的問題”的內容了,經過本文的學習后,相信大家對怎么解決JavaScript相關的問題這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。