沐鳴總代平台_es6-快速掌握Proxy、Reflect

前言

ES6新增的代理和反射為開發者提供了攔截並向基本操作嵌入額外行為的能力。具體地說,可以給目標對象定義一個關聯的代理對象,而這個代理對象可以作為抽象的目標對象來使用。在對目標對象的各種操作影響目標對象之前,可以在代理對象中對這些操作加以控制。

Proxy (代理)

代理是使用 Proxy 構造函數創建的。這個構造函數接收兩個參數:目標對象和處理程序對象。缺少其中任何一個參數都會拋出 TypeError。

創建空代理

如下面的代碼所示,在代理對象上執行的任何操作實際上都會應用到目標對象。唯一可感知的不同
就是代碼中操作的是代理對象。

const target = { 
 id: 'target' 
}; 
const handler = {}; 
const proxy = new Proxy(target, handler); 
// id 屬性會訪問同一個值
console.log(target.id); // target 
console.log(proxy.id); // target

// 給目標屬性賦值會反映在兩個對象上
// 因為兩個對象訪問的是同一個值
target.id = 'foo'; 
console.log(target.id); // foo 
console.log(proxy.id); // foo

// 給代理屬性賦值會反映在兩個對象上
// 因為這個賦值會轉移到目標對象
proxy.id = 'bar'; 
console.log(target.id); // bar 
console.log(proxy.id); // bar

定義捕獲器

捕獲器可以理解為處理程序對象中定義的用來直接或間接在代理對象上使用的一種“攔截器”,每次在代理對象上調用這些基本操作時,代理可以在這些操作傳播到目標對象之前先調用捕獲器函數,從而攔截並修改相應的行為。

const target = { 
 foo: 'bar' 
};
const handler = { 
 // 捕獲器在處理程序對象中以方法名為鍵
 get() { 
 return 'handler override'; 
 } 
};
const proxy = new Proxy(target, handler); 
console.log(target.foo); // bar 
console.log(proxy.foo); // handler override

get() 捕獲器會接收到目標對象,要查詢的屬性和代理對象三個參數。我們可以對上述代碼進行如下改造

const target = { 
 foo: 'bar' 
};
const handler = { 
 // 捕獲器在處理程序對象中以方法名為鍵
 get(trapTarget, property, receiver) { 
 console.log(trapTarget === target); 
 console.log(property); 
 console.log(receiver === proxy); 
 return trapTarget[property]
 } 
};
const proxy = new Proxy(target, handler); 
proxy.foo; 
// true 
// foo 
// true
console.log(proxy.foo); // bar 
console.log(target.foo); // bar

處理程序對象中所有可以捕獲的方法都有對應的反射(Reflect)API 方法。這些方法與捕獲器攔截的方法具有相同的名稱和函數簽名,而且也具有與被攔截方法相同的行為。因此,使用反射 API 也可以像下面這樣定義出空代理對象:

const target = { 
 foo: 'bar' 
}; 
const handler = { 
 get() { 
     // 第一種寫法
     return Reflect.get(...arguments); 
     // 第二種寫法
     return Reflect.get
 } 
}; 
const proxy = new Proxy(target, handler); 
console.log(proxy.foo); // bar 
console.log(target.foo); // bar

我們也可以以此,來對將要訪問的屬性的返回值進行修飾。

const target = { 
 foo: 'bar', 
 baz: 'qux' 
}; 
const handler = { 
 get(trapTarget, property, receiver) { 
 let decoration = ''; 
 if (property === 'foo') { 
 decoration = ' I love you'; 
 } 
 return Reflect.get(...arguments) + decoration; 
 } 
}; 
const proxy = new Proxy(target, handler); 
console.log(proxy.foo); // bar I love you 
console.log(target.foo); // bar 
console.log(proxy.baz); // qux 
console.log(target.baz); // qux

可撤銷代理

有時候可能需要中斷代理對象與目標對象之間的聯繫。對於使用 new Proxy()創建的普通代理來說,這種聯繫會在代理對象的生命周期內一直持續存在。Proxy 也暴露了 revocable()方法,這個方法支持撤銷代理對象與目標對象的關聯。撤銷代理的操作是不可逆的。而且,撤銷函數(revoke())是冪等的,調用多少次的結果都一樣。撤銷代理之後再調用代理會拋出 TypeError。

const target = { 
 foo: 'bar' 
}; 
const handler = { 
 get() { 
 return 'intercepted'; 
 } 
}; 
const { proxy, revoke } = Proxy.revocable(target, handler); 
console.log(proxy.foo); // intercepted 
console.log(target.foo); // bar 
revoke(); 
console.log(proxy.foo); // TypeError

代理另一個代理

代理可以攔截反射 API 的操作,而這意味着完全可以創建一個代理,通過它去代理另一個代理。這樣就可以在一個目標對象之上構建多層攔截網:

const target = { 
 foo: 'bar' 
}; 
const firstProxy = new Proxy(target, { 
 get() { 
 console.log('first proxy'); 
 return Reflect.get(...arguments); 
 } 
}); 
const secondProxy = new Proxy(firstProxy, { 
 get() { 
 console.log('second proxy'); 
 return Reflect.get(...arguments); 
 } 
}); 
console.log(secondProxy.foo); 
// second proxy 
// first proxy 
// bar

代理的問題與不足

1. 代理中的this

const target = { 
 thisValEqualsProxy() { 
 return this === proxy; 
 } 
} 
const proxy = new Proxy(target, {}); 
console.log(target.thisValEqualsProxy()); // false 
console.log(proxy.thisValEqualsProxy()); // true

這樣看起來並沒有什麼問題,this指向調用者。但是如果目標對象依賴於對象標識,那就可能碰到意料之外的問題。

const wm = new WeakMap(); 
class User { 
 constructor(userId) { 
     wm.set(this, userId); 
 } 
 set id(userId) { 
     wm.set(this, userId); 
 } 
 get id() { 
     return wm.get(this); 
 } 
}
const user = new User(123); 
console.log(user.id); // 123 
const userInstanceProxy = new Proxy(user, {}); 
console.log(userInstanceProxy.id); // undefined

這是因為 User 實例一開始使用目標對象作為 WeakMap 的鍵,代理對象卻嘗試從自身取得這個實
例。要解決這個問題,就需要重新配置代理,把代理 User 實例改為代理 User 類本身。之後再創建代
理的實例就會以代理實例作為 WeakMap 的鍵了:

const UserClassProxy = new Proxy(User, {}); 
const proxyUser = new UserClassProxy(456); 
console.log(proxyUser.id);

2. 代理與內部槽位

在代理Date類型時:根據 ECMAScript 規範,Date 類型方法的執行依賴 this 值上的內部槽位[[NumberDate]]。代理對象上不存在這個內部槽位,而且這個內部槽位的值也不能通過普通的 get()和 set()操作訪問到,於是代理攔截后本應轉發給目標對象的方法會拋出 TypeError:

const target = new Date(); 
const proxy = new Proxy(target, {}); 
console.log(proxy instanceof Date); // true 
proxy.getDate(); // TypeError: 'this' is not a Date object

Reflect(反射)

Reflect對象與Proxy對象一樣,也是 ES6 為了操作對象而提供的新 API。Reflect的設計目的:

  1. 將Object對象的一些明顯屬於語言內部的方法(比如Object.defineProperty),放到Reflect對象上。
  2. 修改某些Object方法的返回結果,讓其變得更合理。比如,Object.defineProperty(obj, name, desc)在無法定義屬性時,會拋出一個錯誤,而Reflect.defineProperty(obj, name, desc)則會返回false。
  3. 讓Object操作都變成函數行為。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)讓它們變成了函數行為。
  4. Reflect對象的方法與Proxy對象的方法一一對應,只要是Proxy對象的方法,就能在Reflect對象上找到對應的方法。這就讓Proxy對象可以方便地調用對應的Reflect方法,完成默認行為,作為修改行為的基礎。也就是說,不管Proxy怎麼修改默認行為,你總可以在Reflect上獲取默認行為。

代理與反射API

get()

接收參數:

  • target:目標對象。
  • property:引用的目標對象上的字符串鍵屬性。
  • receiver:代理對象或繼承代理對象的對象。
    返回:
  • 返回值無限制
    get()捕獲器會在獲取屬性值的操作中被調用。對應的反射 API 方法為 Reflect.get()。
const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 get(target, property, receiver) { 
 console.log('get()'); 
 return Reflect.get(...arguments) 
 } 
}); 
proxy.foo; 
// get()

set()

接收參數:

  • target:目標對象。
  • property:引用的目標對象上的字符串鍵屬性。
  • value:要賦給屬性的值。
  • receiver:接收最初賦值的對象。
    返回:
  • 返回 true 表示成功;返回 false 表示失敗,嚴格模式下會拋出 TypeError。

set()捕獲器會在設置屬性值的操作中被調用。對應的反射 API 方法為 Reflect.set()。

const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 set(target, property, value, receiver) { 
 console.log('set()'); 
 return Reflect.set(...arguments) 
 } 
}); 
proxy.foo = 'bar'; 
// set()

has()

接收參數:

  • target:目標對象。
  • property:引用的目標對象上的字符串鍵屬性。

返回:

  • has()必須返回布爾值,表示屬性是否存在。返回非布爾值會被轉型為布爾值。

has()捕獲器會在 in 操作符中被調用。對應的反射 API 方法為 Reflect.has()。

const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 has(target, property) { 
 console.log('has()'); 
 return Reflect.has(...arguments) 
 } 
}); 
'foo' in proxy; 
// has()

defineProperty()

Reflect.defineProperty方法基本等同於Object.defineProperty,用來為對象定義屬性。

接收參數:

  • target:目標對象。
  • property:引用的目標對象上的字符串鍵屬性。
  • descriptor:包含可選的 enumerable、configurable、writable、value、get 和 set定義的對象。

返回:

  • defineProperty()必須返回布爾值,表示屬性是否成功定義。返回非布爾值會被轉型為布爾值。
const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 defineProperty(target, property, descriptor) { 
 console.log('defineProperty()'); 
 return Reflect.defineProperty(...arguments) 
 } 
}); 
Object.defineProperty(proxy, 'foo', { value: 'bar' }); 
// defineProperty()

getOwnPropertyDescriptor()

Reflect.getOwnPropertyDescriptor基本等同於Object.getOwnPropertyDescriptor,用於得到指定屬性的描述對象。

接收參數:

  • target:目標對象。
  • property:引用的目標對象上的字符串鍵屬性。

返回:

  • getOwnPropertyDescriptor()必須返回對象,或者在屬性不存在時返回 undefined。
const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 getOwnPropertyDescriptor(target, property) { 
 console.log('getOwnPropertyDescriptor()'); 
 return Reflect.getOwnPropertyDescriptor(...arguments) 
 } 
}); 
Object.getOwnPropertyDescriptor(proxy, 'foo'); 
// getOwnPropertyDescriptor()

deleteProperty()

Reflect.deleteProperty方法等同於delete obj[name],用於刪除對象的屬性。

接收參數:

  • target:目標對象。
  • property:引用的目標對象上的字符串鍵屬性。

返回:

  • deleteProperty()必須返回布爾值,表示刪除屬性是否成功。返回非布爾值會被轉型為布爾值。

ownKeys()

Reflect.ownKeys方法用於返回對象的所有屬性,基本等同於Object.getOwnPropertyNames與Object.getOwnPropertySymbols之和。

接收參數:

  • target:目標對象。

返回:

  • ownKeys()必須返回包含字符串或符號的可枚舉對象。

getPrototypeOf()

Reflect.getPrototypeOf方法用於讀取對象的__proto__屬性

接收參數:

  • target:目標對象。

返回:

  • getPrototypeOf()必須返回對象或 null。

等等。。

代理模式

跟蹤屬性訪問

通過捕獲 get、set 和 has 等操作,可以知道對象屬性什麼時候被訪問、被查詢。把實現相應捕獲器的某個對象代理放到應用中,可以監控這個對象何時在何處被訪問過:

const user = { 
 name: 'Jake' 
}; 
const proxy = new Proxy(user, { 
 get(target, property, receiver) { 
 console.log(`Getting ${property}`); 
 return Reflect.get(...arguments); 
 }, 
 set(target, property, value, receiver) { 
 console.log(`Setting ${property}=${value}`); 
 return Reflect.set(...arguments); 
 } 
}); 
proxy.name; // Getting name 
proxy.age = 27; // Setting age=27

隱藏屬性

代理的內部實現對外部代碼是不可見的,因此要隱藏目標對象上的屬性也輕而易舉。

const hiddenProperties = ['foo', 'bar']; 
const targetObject = { 
 foo: 1, 
 bar: 2, 
 baz: 3 
}; 
const proxy = new Proxy(targetObject, { 
 get(target, property) { 
 if (hiddenProperties.includes(property)) { 
 return undefined; 
 } else { 
 return Reflect.get(...arguments); 
 } 
 }, 
 has(target, property) {
  if (hiddenProperties.includes(property)) { 
 return false; 
 } else { 
 return Reflect.has(...arguments); 
 } 
 } 
}); 
// get() 
console.log(proxy.foo); // undefined 
console.log(proxy.bar); // undefined 
console.log(proxy.baz); // 3 
// has() 
console.log('foo' in proxy); // false 
console.log('bar' in proxy); // false 
console.log('baz' in proxy); // true

屬性驗證

因為所有賦值操作都會觸發 set()捕獲器,所以可以根據所賦的值決定是允許還是拒絕賦值:

const target = { 
 onlyNumbersGoHere: 0 
}; 
const proxy = new Proxy(target, { 
 set(target, property, value) { 
 if (typeof value !== 'number') { 
 return false; 
 } else { 
 return Reflect.set(...arguments); 
 } 
 } 
}); 
proxy.onlyNumbersGoHere = 1; 
console.log(proxy.onlyNumbersGoHere); // 1 
proxy.onlyNumbersGoHere = '2'; 
console.log(proxy.onlyNumbersGoHere); // 1

函數與構造函數參數驗證

跟保護和驗證對象屬性類似,也可對函數和構造函數參數進行審查。比如,可以讓函數只接收某種類型的值:

function median(...nums) { 
     return nums.sort()[Math.floor(nums.length / 2)]; 
} 
const proxy = new Proxy(median, { 
     apply(target, thisArg, argumentsList) { 
         for (const arg of argumentsList) { 
             if (typeof arg !== 'number') { 
                 throw 'Non-number argument provided'; 
             } 
         }
  return Reflect.apply(...arguments); 
 } 
}); 
console.log(proxy(4, 7, 1)); // 4 
console.log(proxy(4, '7', 1)); 
// Error: Non-number argument provided 
類似地,可以要求實例化時必須給構造函數傳參:
class User { 
 constructor(id) { 
     this.id_ = id; 
 } 
} 
const proxy = new Proxy(User, { 
 construct(target, argumentsList, newTarget) { 
     if (argumentsList[0] === undefined) { 
         throw 'User cannot be instantiated without id'; 
     } else { 
         return Reflect.construct(...arguments); 
     } 
 } 
}); 
new proxy(1); 
new proxy(); 
// Error: User cannot be instantiated without id

數據綁定與可觀察對象

通過代理可以把運行時中原本不相關的部分聯繫到一起。這樣就可以實現各種模式,從而讓不同的代碼互操作。比如,可以將被代理的類綁定到一個全局實例集合,讓所有創建的實例都被添加到這個集合中:

const userList = []; 
class User { 
 constructor(name) { 
 this.name_ = name; 
 } 
} 
const proxy = new Proxy(User, { 
 construct() { 
 const newUser = Reflect.construct(...arguments); 
 userList.push(newUser); 
 return newUser; 
 } 
}); 
new proxy('John'); 
new proxy('Jacob'); 
new proxy('Jingleheimerschmidt'); 
console.log(userList); // [User {}, User {}, User{}]

另外,還可以把集合綁定到一個事件分派程序,每次插入新實例時都會發送消息:

const userList = []; 
function emit(newValue) { 
 console.log(newValue); 
} 
const proxy = new Proxy(userList, { 
 set(target, property, value, receiver) { 
 const result = Reflect.set(...arguments); 
 if (result) { 
 emit(Reflect.get(target, property, receiver)); 
 } 
 return result; 
 } 
}); 
proxy.push('John'); 
// John 
proxy.push('Jacob'); 
// Jacob

使用 Proxy 實現觀察者模式

const queuedObservers = new Set();

const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});

function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver);
  queuedObservers.forEach(observer => observer());
  return result;
}

const person = observable({
  name: '張三',
  age: 20
});

function print() {
  console.log(`${person.name}, ${person.age}`)
}

observe(print);
person.name = '李四';
// 輸出
// 李四, 20

結尾

本文主要參考阮一峰es6教程、js紅寶書第四版

來自:https://segmentfault.com/a/1190000039956559

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10266

沐鳴下載_如何在 JavaScript 中使用宏

在語言當中,宏常見用途有實現 DSL 。通過宏,開發者可以自定義一些語言的格式,比如實現 jsX 語法。在 WASM 已經實現的今天,用其他語言來寫網頁其實並不是沒有可能。像 Rust 語言就帶有強大的宏功能,這使得基於 Rust 的 Yew 框架,不需要實現類似 Babel 的東西,而是靠語言本身就能實現類似 jsX 的語法。一個 Yew 組件的例子,支持類 JSX 的語法。

impl Component for MyComponent {
// ...

fn view(&self) -> html {
let onclick = self.link.callback(|_| Msg::Click);
html! {
<button onclick=onclick>{ self.props.button_text }</button>
}
}
}

JavaScript 宏的局限性

不同於 Rust ,JavaScript 本身是不支持宏的,所以整個工具鏈也是沒有考慮宏的。因此,你是可以寫個識別自定義語法的宏,但是由於配套的工具鏈並不支持,比如最常見的 VSCode 和 Typescript ,你會得到一個語法錯誤。同樣對於 babel 本身所用的 parser 也是不支持擴展語法的,除非你另 Fork 出來一個 Babel 。因此 babel-plugin-macros 不支持自定義語法。不過,藉助模板字符串函數,我們可以曲線救國,至少獲得部分自定義語法樹的能力。一個 GraphQL 的例子,支持在 JavaScript 中直接編寫 GraphQL。

import { gql } from 'graphql.macro';

const query = gql`
query User {
user(id: 5) {
lastName
...UserEntry1
}
}
`;

// 在編譯期會轉換成 ↓ ↓ ↓ ↓ ↓ ↓

const query = {
"kind": "Document",
"definitions": [{
...

為什麼要用宏而非 Babel 插件

Babel 插件的能力確實遠大於宏,而且有些情況下確實是不得不用插件。宏比起 Babel 插件好的一點在於,宏的理念在於開箱即用。使用 react 的開發者,相信都聽過的大名鼎鼎的 Create-React-App ,幫你封裝好了各種底層細節,開發者專註於編寫代碼即可。但是 CRA 的問題在於其封裝的太嚴了,但凡你有一點需要自定義 Babel 插件的需求,基本上就需要執行 yarn react-script eject ,將所有底層細節暴露出來。而對於宏來說,你只需要在項目的 Babel 配置內添加一個 babel-plugin-macros 插件,那麼對於任何自定義的 Babel 宏都可以完美支持,而不是像插件一樣,需要下載各種各樣的插件。CRA 已經內置了 babel-plugin-macros ,你可以在 CRA 項目中使用任意的 Babel 宏。

如何寫一個宏?

介紹

一個宏非常像一個 Babel 插件,因此事先了解如何編寫 Babel 插件是非常有幫助的,對於如何編寫 Babel 插件, Babel 官方有一本手冊 [1] ,專門介紹了如何從零編寫一個 Babel 插件。在知道如何編寫 Babel 插件之後,我們首先通過一個使用宏的例子,來介紹下, Babel 是如何識別文件中的宏的。是某種的特殊的語法,還是用爛的 $ 符號?

import preval from 'preval.macro'

const one = preval`module.exports = 1 + 2 - 1 - 1`

這是非常常見的一個宏,其作用是在編譯期間執行字符串中的 JavaScript 代碼,然後將執行的結果替換到相應的地方,如上的代碼在編譯期會被展開為:

import preval from 'preval.macro'

const one = 1

從使用來方式來看,唯一與識別宏沾點關係的就是 *.macro 字符,這也確實就是 Babel 如何識別宏的方式,實際上不僅對於 *.macro 的形式, Babel 認為庫名匹配正則 /[./]macro(\.c?js)?$/ 表達式的庫就是 Babel 宏,這些匹配表達式的一些例子:

'my.macro'
'my.macro.js'
'my.macro.cjs'
'my/macro'
'my/macro.js'
'my/macro.cjs'

編寫

接下來,我們將簡單編寫一個 importURL 宏,其作用是通過 url 來引入一些庫,並在編譯期間將這些庫的代碼預先拉取下來,處理一下然後引入到文件中。我知道有些 webpack 插件已經支持 從 url 來引入庫,不過這同樣是一個很好的例子來學習如何編寫宏,為了有趣!以及如何在 NodeJS 中發起同步請求! 🙂

準備

首先創建一個名為 importURL 的文件夾,執行 npm init -y ,來快速創建一個項目。在項目使用宏的人需要安裝 babel-plugin-macros ,同樣的,編寫宏的同樣需要安裝這個插件,在寫之前,我們也需要提前安裝一些其他的庫來輔助我們編寫宏,在開發之前,需要事先:

package.json
name
import-url.macro

我們需要用 Babel 提供的輔助方法來創建宏。執行 yarn add babel-plugin-macros

yarn add fs-extra ,一個更容易使用的代替 Node fs 模塊的庫

yarn add find-root ,編寫宏的過程我們需要根據所處理文件的路徑找到其所在的工作目錄,從而寫入緩存,這是一個已經封裝好的庫

示例

我們的目標就是將如下代碼轉換成

import importURL from 'importurl.macros';

const React = importURL('https://unpkg.com/react@17.0.1/umd/react.development.js');

// 編譯成

import importURL from 'importurl.macros';

const React = require('../cache/pkg1.js');

我們會解析代碼 importURL 函數的第一個參數,當做遠程庫的地址,然後在編譯期間同步的通過 Get 請求拉取代碼內容。然後寫入項目頂層文件夾下 .chache 下,並替換相應的 importURL 語句成 require(…) 語句,路徑 … 則是使用 importURL 的文件相對 .cache 文件中的相對路徑,使得 webpack 在最終打包的時候能夠找到對應的代碼。

開始

我們先看看最終的代碼長什麼樣子

import { execSync } from 'child_process';
import findRoot from 'find-root';
import path from 'path';
import fse from 'fs-extra';

import { createMacro } from 'babel-plugin-macros';

const syncGet = (url) => {
const data = execSync(`curl -L ${url}`).toString();
if (data === '') {
throw new Error('empty data');
}
return data;
}

let count = 0;
export const genUniqueName = () => `pkg${++count}.js`;

module.exports = createMacro((ctx) => {
const {
references, // 文件中所有對宏的引用
babel: {
types: t,
}
} = ctx;
// babel 會把當前處理的文件路徑設置到 ctx.state.filename
const workspacePath = findRoot(ctx.state.filename);
// 計算出緩存文件夾
const cacheDirPath = path.join(workspacePath, '.cache');
//
const calls = references.default.map(path => path.findParent(path => path.node.type === 'CallExpression' ));
calls.forEach(nodePath => {
// 確定 astNode 的類型
if (nodePath.node.type === 'CallExpression') {
// 確定函數的第一個參數是純字符串
if (nodePath.node.arguments[0]?.type === 'StringLiteral') {
// 獲取一個參數,當做遠程庫的地址
const url = nodePath.node.arguments[0].value;
// 根據 url 拉取代碼
const codes = syncGet(url);
// 生成一個唯一包名,防止衝突
const pkgName = genUniqueName();
// 確定最終要寫入的文件路徑
const cahceFilename = path.join(cacheDirPath, pkgName);
// 通過 fse 庫,將內容寫入, outputFileSync 會自動創建不存在的文件夾
fse.outputFileSync(cahceFilename, codes);
// 計算出相對路徑
const relativeFilename = path.relative(ctx.state.filename, cahceFilename);
// 最終計算替換 importURL 語句
nodePath.replaceWith(t.stringLiteral(`require('${relativeFilename}')`))
}
}
});
});

創建一個宏

我們通過 createMacro 函數來創建一個宏, createMacro 接受我們編寫的函數當做參數來生成一個宏,但實際上我們並不關心 createMacro 的返回時值是什麼,因為我們的代碼最終都將會被自己替換掉,不會在運行期間執行到。我們編寫的函數的第一個參數是 Babel 傳遞給我們的一些狀態,我們可以大概看下其類型都有什麼。

function createMacro(handler: MacroHandler, options?: Options): any;
interface MacroParams {
references: { default: Babel.NodePath[] } & References;
state: Babel.PluginPass;
babel: typeof Babel;
config?: { [key: string]: any };
}
export interface PluginPass {
file: BabelFile;
key: string;
opts: PluginOptions;
cwd: string;
filename: string;
[key: string]: unknown;
}

可視化 AST

我們可以通過 astexplorer [2] 來觀察我們將要處理代碼的語法樹,對於如下代碼

import importURL from 'importurl.macros';

const React = importURL('https://unpkg.com/react@17.0.1/umd/react.development.js');

會生成如下語法樹

紅色標紅的語法樹節點,就是 Babel 會通過 ctx.references 傳遞給我們的,因此我們需要通過 .findParent() 方法來向上找到父節點 CallExpresstion ,才能去獲取 arguments 屬性下的參數,拿到遠程庫的 URL 地址。

同步請求

這裏的一個難點在於, Babel 不支持異步轉換,所有的轉換操作都是同步的,因此在發起請求時也必須是同步的請求。我本來以為這是一件很簡單的事情, Node 會提供一個類似 sync: true 的選項。但是並沒有的, Node 確實不支持任何同步請求,除非你選擇用下面這種很怪異的方式:

const syncGet = (url) => {
const data = execSync(`curl -L ${url}`).toString();
if (data === '') {
throw new Error('empty data');
}
return data;
}

收尾

在拿到代碼后,我們將代碼寫入到開始計算出的文件路徑中,這裏我們使用 fs-extra 的目的在於, fs-extra 在寫入的時候如果遇到不存在文件夾,不會像 fs 一樣直接拋出錯誤,而是自動創建相應的文件件。在寫入完成后,我們通過 Babel 提供的輔助方法 stringLiteral 創字符串節點,隨後替換掉我們的 importURL(…) ,自此我們的整個轉換流程就結束了。

最後

這個宏存在一些缺陷,有興趣的同學可以繼續完善:

沒有識別同一 URL 的庫,進行復用,不過我想這些已經滿足如何編寫一個宏的目的了。

genUniqueName 在跨文件是會計算出重複包名,正確的算法應該是根據 url 計算哈希值來當做唯一包名

參考資料

[1] 手冊: https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/README.md [2] astexplorer: https://astexplorer.net/

原文來自:https://mp.weixin.qq.com/s/8Dtjy9clLINaxIdIko0bWw

作者:字節前端 ByteFE

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10275

杏耀註冊平台官網_Redis 使用的 10 個小技巧

來源:objectrocket.com/blog/how-to/10-quick-tips-about-redis/

Redis 使用的 10 個小技巧

  • 1、停止使用 KEYS

  • 2、找出拖慢 Redis 的罪魁禍首

  • 3、將 Redis-Benchmark 結果作為參考,而不要一概而論

  • 4、Hashes 是你的最佳選擇

  • 5、設置 key 值的存活時間

  • 6、 選擇合適的回收策略

  • 7、如果你的數據很重要,請使用 Try/Except

  • 8、不要耗盡一個實例

  • 9、內核越多越好嗎?!

  • 10、高可用

Redis 在當前的技術社區里是非常熱門的。從來自 Antirez 一個小小的個人項目到成為內存數據存儲行業的標準,Redis已經走過了很長的一段路。

隨之而來的一系列最佳實踐,使得大多數人可以正確地使用 Redis。

下面我們將探索正確使用 Redis 的10個技巧。

停止使用 KEYS

Okay,以挑戰這個命令開始這篇文章,或許並不是一個好的方式,但其確實可能是最重要的一點。

很多時候當我們關注一個redis實例的統計數據,我們會快速地輸入”KEYS *”命令,這樣key的信息會很明顯地展示出來。平心而論,從程序化的角度出發往往傾向於寫出下面這樣的偽代碼:

for key in 'keys *':
doAllTheThings()

但是當你有1300萬個key時,執行速度將會變慢。因為KEYS命令的時間複雜度是O(n),其中n是要返回的keys的個數,這樣這個命令的複雜度就取決於數據庫的大小了。並且在這個操作執行期間,其它任何命令在你的實例中都無法執行。

作為一個替代命令,看一下 SCAN 吧,其允許你以一種更友好的方式來執行… SCAN 通過增量迭代的方式來掃描數據庫。這一操作基於游標的迭代器來完成的,因此只要你覺得合適,你可以隨時停止或繼續。

找出拖慢 Redis 的罪魁禍首

由於 Redis 沒有非常詳細的日誌,要想知道在 Redis 實例內部都做了些什麼是非常困難的。幸運的是 Redis 提供了一個下面這樣的命令統計工具:

127.0.0.1:6379> INFO commandstats
# Commandstats
cmdstat_get:calls=78,usec=608,usec_per_call=7.79
cmdstat_setex:calls=5,usec=71,usec_per_call=14.20
cmdstat_keys:calls=2,usec=42,usec_per_call=21.00
cmdstat_info:calls=10,usec=1931,usec_per_call=193.10

通過這個工具可以查看所有命令統計的快照,比如命令執行了多少次,執行命令所耗費的毫秒數(每個命令的總時間和平均時間)

只需要簡單地執行 CONFIG RESETSTAT 命令就可以重置,這樣你就可以得到一個全新的統計結果。

將 Redis-Benchmark 結果作為參考,而不要一概而論

Redis 之父 Salvatore 就說過:“通過執行GET/SET命令來測試Redis就像在雨天檢測法拉利的雨刷清潔鏡子的效果”。很多時候人們跑到我這裏,他們想知道為什麼自己的Redis-Benchmark統計的結果低於最優結果 。但我們必須要把各種不同的真實情況考慮進來,例如:

  • 可能受到哪些客戶端運行環境的限制?

  • 是同一個版本號嗎?

  • 測試環境中的表現與應用將要運行的環境是否一致?

Redis-Benchmark的測試結果提供了一個保證你的 Redis-Server 不會運行在非正常狀態下的基準點,但是你永遠不要把它作為一個真實的“壓力測試”。壓力測試需要反應出應用的運行方式,並且需要一個盡可能的和生產相似的環境。

Hashes 是你的最佳選擇

以一種優雅的方式引入 hashes 吧。hashes 將會帶給你一種前所未有的體驗。之前我曾看到過許多類似於下面這樣的key結構:

foo:first_name
foo:last_name
foo:address

上面的例子中,foo 可能是一個用戶的用戶名,其中的每一項都是一個單獨的 key。這就增加了 犯錯的空間,和一些不必要的 key。使用 hash 代替吧,你會驚奇地發現竟然只需要一個 key :

127.0.0.1:6379> HSET foo first_name "Joe"
(integer) 1
127.0.0.1:6379> HSET foo last_name "Engel"
(integer) 1
127.0.0.1:6379> HSET foo address "1 Fanatical Pl"
(integer) 1
127.0.0.1:6379> HGETALL foo
1) "first_name"
2) "Joe"
3) "last_name"
4) "Engel"
5) "address"
6) "1 Fanatical Pl"
127.0.0.1:6379> HGET foo first_name
"Joe"

設置 key 值的存活時間

無論什麼時候,只要有可能就利用key超時的優勢。一個很好的例子就是儲存一些諸如臨時認證key之類的東西。

當你去查找一個授權key時——以OAUTH為例——通常會得到一個超時時間。這樣在設置key的時候,設成同樣的超時時間,Redis就會自動為你清除!而不再需要使用KEYS *來遍歷所有的key了,怎麼樣很方便吧?

選擇合適的回收策略

既然談到了清除key這個話題,那我們就來聊聊回收策略。當 Redis 的實例空間被填滿了之後,將會嘗試回收一部分key。根據你的使用方式,我強烈建議使用 volatile-lru 策略——前提是你對key已經設置了超時。但如果你運行的是一些類似於 cache 的東西,並且沒有對 key 設置超時機制,可以考慮使用 allkeys-lru 回收機制。我的建議是先在這裏查看一下可行的方案。

如果你的數據很重要,請使用 Try/Except

如果必須確保關鍵性的數據可以被放入到 Redis 的實例中,我強烈建議將其放入 try/except 塊中。幾乎所有的Redis客戶端採用的都是“發送即忘”策略,因此經常需要考慮一個 key 是否真正被放到 Redis 數據庫中了。至於將 try/expect 放到 Redis 命令中的複雜性並不是本文要講的,你只需要知道這樣做可以確保重要的數據放到該放的地方就可以了。

不要耗盡一個實例

無論什麼時候,只要有可能就分散多redis實例的工作量。從3.0.0版本開始,Redis就支持集群了。Redis集群允許你基於key範圍分離出部分包含主/從模式的key。完整的集群背後的“魔法”可以在這裏找到。

但如果你是在找教程,那這裡是一個再適合不過的地方了。如果不能選擇集群,考慮一下命名空間吧,然後將你的key分散到多個實例之中。關於怎樣分配數據,在redis.io網站上有這篇精彩的評論。

內核越多越好嗎?!

當然是錯的。Redis 是一個單線程進程,即使啟用了持久化最多也只會消耗兩個內核。除非你計劃在一台主機上運行多個實例——希望只會是在開發測試的環境下!——否則的話對於一個 Redis 實例是不需要2個以上內核的。

高可用

到目前為止 Redis Sentinel 已經經過了很全面的測試,很多用戶已經將其應用到了生產環境中(包括 ObjectRocket )。如果你的應用重度依賴於 Redis ,那就需要想出一個高可用方案來保證其不會掉線。

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10272

沐鳴登陸地址_CSS新特性contain的語法、作用及使用場景

contain 屬性

這個 contain 屬性的主要目的是隔離指定內容的樣式、布局和渲染。開發人員可以使用這個 contain 屬性來限制指定的DOM元素和它的子元素同頁面上其它內容的聯繫;我們可以把它看做一個iframe。跟iframe很相似,它能建立起一個邊界,產生一個新的根布局;保證了它和它的子元素的DOM變化不會觸發父元素重新布局、渲染等。

開發人員可以用這個 contain 屬性聲明一個元素和它的子元素是——盡可能的——和頁面上的其它元素保持獨立。— 來自 W3C 規範

contain 使用場景舉例

我們已經知道了,使用這個 contain 屬性可以將一個元素標誌為和頁面上其它元素是相對獨立的元素。為了說明這個屬性的作用,下面舉幾個使用例子:

頁面小飾件(widgets)

通常在頁面上添加第三方小飾件時,我們幾乎對它們沒有什麼太多的控制,比如分享工具,它們可能會因為具有相當耗資源的布局、樣式、渲染操作等大幅度的降低整個頁面的執行效率。為了將它們同我們的網站隔離開來,使用 contain: strict; 將第三方的小飾件同頁面上的其它內容隔離開來。

屏幕外的內容

如果你有一個導航欄或其它類似的東西並不在屏幕可現實範圍內出現,瀏覽器同樣會為這些不可見的元素進行渲染。通過使用 contain: paint; 瀏覽器就會忽略渲染這些屏幕外不可見的元素,從而能更快的渲染其它內容。

什麼時候應該使用contain

如果你的頁面很簡單,沒有複雜的DOM節點和小飾件(widgets),那就沒必要考慮使用這種css的contain技術。而如果你開發的頁面非常複雜,那麼,這個css的contain技術可以幫助你優化頁面的性能。而對於第三方的小飾件,始終使用contain: strict;是很好的習慣,它可以保護你的頁面不受它們的干擾而出現性能問題。

contain 語法

看看它的語法:

{
  /* No layout containment. */
  contain: none;
  /* Turn on size containment for an element. */
  contain: size;
  /* Turn on layout containment for an element. */
  contain: layout;
  /* Turn on style containment for an element. */
  contain: style;
  /* Turn on paint containment for an element. */
  contain: paint;

  /* Turn on containment for layout, paint, and size. */
  contain: strict;
  /* Turn on containment for layout, and paint. */
  contain: content;
}

none | strict | layout | style | paint | size | contain

這個 contain 屬性可以有7種不同的值。

  • none 無
  • layout 開啟布局限制
  • style 開啟樣式限制
  • paint 開啟渲染限制
  • size 開啟size限制
  • content 開啟除了size外的所有限制
  • strict開啟 layout, style 和 paint 三種限制組合

除去 none,取值還有 6 個,我們一個一個來看看。

contain: size

contain: size: 設定了 contain: size 的元素的渲染不會受到其子元素內容的影響。

我開始看到這個定義也是一頭霧水,光看定義很難明白到底是什麼意思。還需實踐一番:

假設我們有如下簡單結構:

<div class="container">
   
</div>
.container {
    width: 300px;
    padding: 10px;
    border: 1px solid red;
}

p {
    border: 1px solid #333;
    margin: 5px;
    font-size: 14px;
}

並且,藉助 jQuery 實現每次點擊容器添加一個 <p>Coco</p> 結構:

$('.container').on('click', e => {
    $('.container').append('<p>Coco</p>')
})

那麼會得到如下結果:

可以看到,容器 .container 的高度是會隨着元素的增加而增加的,這是正常的現象。

此刻,我們給容器 .container 添加一個 contain: size,也就會出現上述說的:設定了 contain: size 的元素的渲染不會受到其子元素內容的影響

.container {
    width: 300px;
    padding: 10px;
    border: 1px solid red;
+   contain: size
}

再看看會發生什麼:

正常而言,父元素的高度會因為子元素的增多而被撐高,而現在,子元素的變化不再影響父元素的樣式布局,這就是 contain: size 的作用。

contain: style

接下來再說說 contain: style、contain: layout 、contain: paint。先看看 contain: style。

截止至本文書寫的過程中,contain: style 暫時被移除了。

嗯,官方說辭是因為存在某些風險,暫時被移除,可能在規範的第二版會重新定義吧,那這個屬性也暫且放一放。

contain: paint

contain: paint:設定了 contain: paint 的元素即是設定了布局限制,也就是說告知 User Agent,此元素的子元素不會在此元素的邊界之外被展示,因此,如果元素不在屏幕上或以其他方式設定為不可見,則還可以保證其後代不可見不被渲染。

這個稍微好理解一點,先來看第一個特性:

設定了 contain: paint 的元素的子元素不會在此元素的邊界之外被展示

設定了 contain: paint 的元素的子元素不會在此元素的邊界之外被展示

這個特點有點類似與 overflow: hidden,也就是明確告知用戶代理,子元素的內容不會超出元素的邊界,所以超出部分無需渲染。

簡單示例,假設元素結構如下:

<div class="container">
    <p>Coco</p>
</div>
.container {
    contain: paint;
    border: 1px solid red;
}

p{
    left: -100px;
}

我們來看看,設定了 contain: paint 與沒設定時會發生什麼:

設定了 contain: paint 的元素在屏幕之外時不會渲染繪製

通過使用 contain: paint, 如果元素處於屏幕外,那麼用戶代理就會忽略渲染這些元素,從而能更快的渲染其它內容。

contain: layout

contain: layout:設定了 contain: layout 的元素即是設定了布局限制,也就是說告知 User Agent,此元素內部的樣式變化不會引起元素外部的樣式變化,反之亦然。

This value turns on layout containment for the element. This ensures that the containing box is totally opaque for layout purposes; nothing outside can affect its internal layout, and vice versa.

啟用 contain: layout 可以潛在地將每一幀需要渲染的元素數量減少到少數,而不是重新渲染整個文檔,從而為瀏覽器節省了大量不必要的工作,並顯着提高了性能。

使用 contain:layout,開發人員可以指定對該元素任何後代的任何更改都不會影響任何外部元素的布局,反之亦然。

因此,瀏覽器僅計算內部元素的位置(如果對其進行了修改),而其餘DOM保持不變。因此,這意味着幀渲染管道中的布局過程將加快。

存在的問題

描述很美好,但是在實際 Demo 測試的過程中(截止至2021/04/27,Chrome 90.0.4430.85),僅僅單獨使用 contain:layout 並沒有驗證得到上述那麼美好的結果。

設定了 contain: layout 的指定元素,改元素的任何後代的任何更改還是會影響任何外部元素的布局,點擊紅框會增加一條 <p>Coco<p> 元素插入到 container 中:

簡單的代碼如下:

<div class="container">
    <p>Coco</p>
    ...
</div>
<div class="g-test"></div>
html,
body {
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    gap: 10px;
}

.container {
    width: 150px;
    padding: 10px;
    contain: layout;
    border: 1px solid red;
}

.g-test {
    width: 150px;
    height: 150px;
    border: 1px solid green;
}

目前看來,contain: layout 的實際作用不那麼明顯,更多的關於它的用法,你可以再看看這篇文章:CSS-tricks – contain

contain: strict | contain: content

這兩個屬性稍微有點特殊,效果是上述介紹的幾個屬性的聚合效果:

contain: strict:同時開啟 layout、style、paint 以及 size 的功能,它相當於 contain: size layout paint

contain: content:同時開啟 layout、style 以及 paint 的功能,它相當於 contain: layout paint

所以,這裏也提一下,contain 屬性是可以同時定義幾個的。

Can i Use — CSS Contain

截止至 2021-04-27,Can i Use 上的 CSS Contain 兼容性,已經可以開始使用起來:

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10267

沐鳴註冊_提高代碼質量的七大JavaScript優秀實踐

自2015年以來,隨着ECMAScript 6(簡稱ES6)的發布,每年都有新版本的ECMAScript規範面市。而每次迭代都為該語言添加新的功能、新的語法、以及新的質量改進。為此,大多數瀏覽器和Node.js中的JavaScript引擎都需要迎頭趕上。就連程序員的編程習慣、以及代碼組織方法,也需要與時俱進,以提高整體的代碼質量,並方便後期的維護。

為了便於您編寫出更簡潔、更易讀的程序代碼,本文將在總結和歸納ECMAScript最新功能的基礎上,為您提供提高JavaScript和Node.js代碼質量的七種優秀實踐。

1.塊作用域聲明(Block Scoped Declarations)

自從該語言問世以來,JavaScript開發人員一直使用var來聲明變量。不過,正如下面代碼段所展示的那樣,由關鍵字var所創建的變量,在其作用域上會出現問題。

var x = 10 
if (true) { 
  var x = 15     // inner declaration overrides declaration in parent scope 
  console.log(x) // prints 15 
} 
console.log(x)   // prints 15 

由於已定義的變量var並非塊作用域(block-scoped),因此如果在小作用域內被重新定義,它們就會影響到外部作用域的值。

然而,如果我們用let和const兩個新的關鍵字來替換var,便可以避免該缺陷(請參見如下代碼段)。

let y = 10 
if (true) { 
  let y = 15       // inner declaration is scoped within the if block 
  console.log(y)   // prints 15 
} 
console.log(y)     // prints 10 

當然,const與let在語義上有所不同,那些用const聲明的變量,無法在其作用域內被重新分配(如下代碼段所示)。不過,這並不意味着它們是不可改變的,而只是代表着它們的引用不能被更改。

const x = [] 
  
x.push("Hello", "World!") 
x // ["Hello", "World!"] 
  
x = [] // TypeError: Attempted to assign to readonly property. 

2.箭頭函數(Arrow Functions)

作為新近被引入JavaScript的一項重要功能,箭頭函數具有許多優點。首先,它們能夠讓JavaScript的函數看起來更加整潔,並且更便於開發者的編寫。

let x = [1, 2, 3, 4] 
  
x.map(val => val * 2)                // [2, 4, 6, 8] 
x.filter(val => val % 2 == 0)        // [2, 4] 
x.reduce((acc, val) => acc + val, 0) // 10 

如上述示例所示,“=>”後面的函數以一種簡潔的語法,替換了傳統函數。

  • 如果函數主體是單個表達式,則已經隱含了作用域括號{}和return關鍵字,所以無需額外寫入。
  • 如果函數只有一個參數,則也已經隱含了參數括號(),同樣無需額外寫入。
  • 如果函數體的表達式是一套字典(dictionary),則必須將其括入括號()中。

箭頭函數的另一個優勢在於:為了避免由於使用this關鍵字,而引起的諸多不便,箭頭函數並不會定義作用域,而是會存在於其父作用域中。也就是說,箭頭函數沒有任何針對this的綁定。在箭頭函數中,this的值與父作用域中的值是相同的。因此,箭頭函數不能被用作各種方法或構造函數。它們既不適用於apply、bind或call,也沒有針對super的綁定。

此外,箭頭函數還會受到諸如:缺少可供傳統功能訪問的arguments對象,以及缺少函數體中的yield等其他限制。

可以說,箭頭函數並非是與標準函數的1:1替代,而是向JavaScript中添加了額外的功能集。

3.可選鏈(Optional Chaining)

讓我們來試想一個類似person對象的、具有深層嵌套的數據結構。業務應用需要訪問到該對象的名字和姓氏。由此,我們可以編寫出如下JavaScript代碼:

public class HelloWorld { 
    public static void main(String[] args) { 
        System.out.println("Hello World"); 
    } 
} 

然而,如果person對象並不包含嵌套的name對象,則可能會出現如下錯誤。

person = { 
  age: 42 
} 
person.name.first // TypeError: Cannot read property 'first' of undefined 
person.name.last  // TypeError: Cannot read property 'last' of undefined 

對此,開發人員往往需要通過如下代碼,來予以解決。顯然,這些代碼不但冗長難寫,而且可讀性較差。

person && person.name && person.name.first // undefined 

而作為JavaScript的一項新功能,可選鏈的語法允許您訪問嵌套得更深的對象屬性,而不用擔心屬性是否真的存在。也就是說,如果可選鏈在挖掘過程遇到了null或undefined的值,那麼就會通過短路(short-circuit)計算,返回undefined,而不會報錯。

person?.name?.first // undefined 

如上述代碼所示,其結果代碼簡潔且明了。

4.空值合併(Null-ish Coalescing)

在引入空值合併運算符之前,在輸入為空的情況下,JavaScript開發人員需要使用OR運算符–||,以回退到默認值。這就會導致:即使出現合法的、但屬於虛假值(falsy values),也會被回退到默認值的情況。

function print(val) { 
    return val || 'Missing' 
} 
  
print(undefined) // 'Missing' 
print(null)      // 'Missing' 
  
print(0)         // 'Missing' 
print('')        // 'Missing' 
print(false)     // 'Missing' 
print(NaN)       // 'Missing' 

如今,JavaScript推出了null合併運算符–??。它能夠保證只有在前面的表達式為null-ish的情況下,才會觸發回退。值得注意的是,此處的空值是指null或undefined。

function print(val) { 
    return val ?? 'Missing' 
} 
  
print(undefined) // 'Missing' 
print(null)      // 'Missing' 
  
print(0)         // 0 
print('')        // '' 
print(false)     // false 
print(NaN)       // NaN 

如此,您可以確保自己的程序能夠接受虛假值作為合法輸入,而不會最終被回退。

5.邏輯賦值(Logical Assignment)

假設您需要先判斷是否為空,再為變量分配數值,那麼如下代碼便展示了這樣的基本邏輯:

if (x === null || x == undefined) { 
    x = y 
} 

如果您熟悉上面提到的短路計算的工作原理,則可能會使用null-ish合併運算符(coalescing operator),將上述三行代碼替換為如下更簡潔的版本。

x ?? (x = y) // x = y if x is nullish, else no effect 

由上述代碼可知,如果x為null-ish的話,我們可以使用null-ish合併運算符的短路功能,來執行第二部分(x = y)。這段代碼雖然非常簡潔,但是不太容易被閱讀或理解。而我們完全可以使用如下代碼,根據邏輯上的null-ish分配,來消除此類變通方法。

x ??= y // x = y if x is nullish, else no effect 

同樣,JavaScript還引入了邏輯AND賦值–&&=、邏輯OR賦值–||=的運算符。這些運算符僅在滿足特定條件時被執行賦值,否則並不起作用。

x ||= y // x = y if x is falsy, else no effect 
x &&= y // x = y if x is truthy, else no effect 

專家建議:如果您有過Ruby的編程經驗,那麼您會一眼識別出||=和&&=運算符。畢竟Ruby並沒有虛假值的概念。

6.已命名捕獲組(Named Capture Groups)

不知您是否知曉正則表達式中的“捕獲組”的相關概念?如下面的代碼段所示,它是與括號中的正則表達式部分匹配的字符串。

let re = /(\d{4})-(\d{2})-(\d{2})/ 
let result = re.exec('Pi day this year falls on 2021-03-14!') 
  
result[0] // '2020-03-14', the complete match 
result[1] // '2020', the first capture group 
result[2] // '03', the second capture group 
result[3] // '14', the third capture group 

一直以來,正則表達式都能夠支持已命名捕獲組。這是一種通過引用名稱、而非索引,來捕獲各個組的方式。目前,在ES9中,該功能已被JavaScript實現。正如下面的代碼段所示,其結果對象包含了一個嵌套的組對象,其中每個捕獲組的值都能夠映射到其名稱上。

let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/ 
let result = re.exec('Pi day this year falls on 2021-03-14!') 
  
result.groups.year  // '2020', the group named 'year' 
result.groups.month // '03', the group named 'month' 
result.groups.day   // '14', the group named 'day' 

而且,新的API與JavaScript的解構分配功能,也能夠完美地結合在一起(請參見下面的代碼段)。

let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/ 
let result = re.exec('Pi day this year falls on 2021-03-14!') 
let { year, month, day } = result.groups 
  
year  // '2020' 
month // '03' 
day   // '14' 

7.async和await

眾所周知,異步性是JavaScript的一項強大功能。許多可能長時間運行、或較為耗時的函數,能夠返回Promise,而不會被阻止運行。

const url = 'https://the-one-api.dev/v2/book' 
let prom = fetch(url) 
prom // Promise {<pending>} 
  
// wait a bit 
prom // Promise {<fullfilled>: Response}, if no errors 
// or 
prom // Promise {<rejected>: Error message}, if any error 

在上述代碼段中,針對fetch的調用返回了一個狀態為“待處理(pending)”的Promise。而當API返迴響應時,它將會轉換為“已實現(fulfilled)”的狀態。在Promises中,您可以執行如下操作,來通過API的調用,將響應解析成為JSON。

const url = 'https://the-one-api.dev/v2/book' 
let prom = fetch(url) 
prom                               // Promise {<fullfilled>: Response} 
  .then(res => res.json()) 
  .then(json => console.log(json)) // prints response, if no errors 
  .catch(err => console.log(err))  // prints error message, if any error 

2017年,JavaScript推出了兩個新的關鍵字async和await,來使得Promises的處理和使用變得更加容易和流暢。當然,它們並非Promises的替代,只是Promises概念之上的語法增強。

而且,並非讓所有的代碼里都出現在一系列的“then”函數,await旨在讓其更像同步的JavaScript。您可以使用帶有await的try…catch,來代替直接使用Promise的catch函數去處理錯誤。下面是具有同等效果的await代碼。

const url = 'https://the-one-api.dev/v2/book' 
let res = await fetch(url) // Promise {<fullfilled>: Response} -await-> Response 
try { 
    let json = await res.json() 
    console.log(json) // prints response, if no errors 
} catch(err) { 
  console.log(err)  // prints error message, if any error 
} 

當然,async關鍵字也有着“硬幣的另一面”,它會將任何待發送的數據封裝到一個Promise中。下面是一段旨在通過異步函數添加多個数字的程序代碼。在現實情況中,您的代碼可能會比它更加複雜。

async function sum(...nums) { 
    return nums.reduce((agg, val) => agg + val, 0) 
} 
sum(1, 2, 3)                    // Promise {<fulfilled>: 6} 
  .then(res => console.log(res) // prints 6 
let res = await sum(1, 2, 3)    // Promise {<fulfilled>: 6} -await-> 6 
console.log(res)                // prints 6 

小結

如您所見,JavaScript每年都會在其語言中加入新的功能。希望我們在上面介紹到的七項提到代碼質量的優秀實踐,能夠對您的日常編程提供幫助。

原文標題:7 JavaScript Best Practices to Improve Code Quality

作者:Dhruv Bhanushali

翻譯來自:http://developer.51cto.com/art/202105/661081.htm

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10274

沐鳴娛樂_公眾號網頁開發經驗總結

內網穿透

sunny-Ngrok:https://www.ngrok.cc (opens new window)

修改 webpack-dev-server 的 disableHostCheck: true

移動端調試神器

VConsole:https://github.com/Tencent/vConsole

微信公眾號開發文檔

公眾號開發文檔 (opens new window)

JS-WX-SDK 文檔 (opens new window)

如何讓網頁只在微信內打開

通過 userAgent 判斷

// 判斷是否是微信環境
function getIsWxClient() {
  var ua = navigator.userAgent.toLowerCase();
  if (ua.match(/MicroMessenger/i) == "micromessenger") {
    return true;
  }
  return false;
}

通過 WeixinjsBridge 判斷(推薦)

// 需要在js-SDK加載之後判斷
function getIsWxClient() {
  if (typeof WeixinJSBridge !== "undefined") {
    return true;
  } else {
    return false;
  }
}

公眾號內保持登錄狀態

微信內網頁不可使用 local/sessionStorage 儲存,因為它只是一個 webview 組件,並不是一個瀏覽器。 但是我們可以使用 cookie 儲存的方式

參考:關於微信中的 localStorage 及使用 cookie 的解決方案:https://my.oschina.net/crazymus/blog/425650

方法:

//設置cookie
function setCookie(c_name, value, expiredays) {
  var exdate = new Date();
  exdate.setDate(exdate.getDate() + expiredays);
  document.cookie =
    c_name +
    "=" +
    escape(value) +
    (expiredays == null ? "" : ";expires=" + exdate.toGMTString());
}

//取回cookie
function getCookie(c_name) {
  if (document.cookie.length > 0) {
    let c_start = document.cookie.indexOf(c_name + "=");
    if (c_start != -1) {
      c_start = c_start + c_name.length + 1;
      c_end = document.cookie.indexOf(";", c_start);
      if (c_end == -1) c_end = document.cookie.length;
      return unescape(document.cookie.substring(c_start, c_end));
    }
  }
  return "";
}

使用

//設置cookie,有效期為365天
setCookie("username", "123", 365);

//取回,若cookie失效,將返回空
getCookie("username");

同樣的,也可以做登錄狀態保存。

掃碼關注公眾號登錄網站

生成帶參數的二維碼

    生成二維碼(opens new window)

用戶掃描二維碼

    用戶掃碼后-獲取用戶 openid(opens new window)獲取

    用戶關注/取關推送(opens new window)獲取 openid 和關注狀況

關注后獲取用戶 openid

    通過 openid 獲取用戶基本信息(opens new window),可根據 openid 和 unionid 添加到用戶信息中。

    獲取關注者用戶列表(opens new window),可以判斷與用戶是否關注

總結邏輯:用戶通過掃碼生成帶有參數的二維碼后可以獲得微信的推送,拿到 openid 和訂閱狀態,然後通過 openid 可以獲取 unionid 和微信基本信息,在與我們的數據做一個綁定,然後訂閱之後可以獲得推送,判斷已經訂閱就給他返回登錄態(前端輪詢/webStoket)。

原文來自:https://www.zhangningle.top/articles/WeApp/公眾號網頁開發經驗總結.html

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10265

沐鳴平台註冊登錄_JavaScript中延遲加載屬性

改善性能的最好方法之一是避免重複兩次相同的工作。

因此,只要可以緩存結果供以後使用,就可以加快程序的速度。

諸如延遲加載屬性模式之類的技術使任何屬性都可以成為緩存層以提高性能。

這裏說到的延遲加載屬性模式就是利用的訪問器屬性,將計算昂貴的操作推遲到需要時再使用。

場景

有些時候,你會在JavaScript類內部創建一些屬性,它保存實例中可能需要的任何數據。

對於在構造函數內部隨時可用的小數據而言,這不是問題。

但是,如果需要在實例中可用之前計算一些大數據,則您可能需要執行昂貴的計算操作。例如,考慮此類:

class MyClass {
constructor() {
this.data = someExpensiveComputation();
}
}

在這裏,該data屬性是執行一些昂貴的計算而創建的。

如果您不確定將使用該屬性,則提前執行可能不太好,效率低。幸運的是,接下來介紹幾種方法可以將這些操作推遲。

接下來主要圍繞的訪問器屬性來展開的。

按需屬性模式

優化執行計算操作的最簡單方法是等到需要數據后再進行計算。

例如,您可以使用帶有getter的data屬性來按需進行計算,如下所示:

class MyClass {
get data() {
return someExpensiveComputation();
}
}

在這種情況下,直到有人第一次讀取該data屬性時,您的昂貴的計算操作才發生,這是一種改進。

但是,也是存在問題的,每次data讀取屬性時都會執行相同的昂貴計算操作,這比之前的示例(其中至少僅執行一次計算)差。

按照我們分析的情況來看,這不是一個好的解決方案,所以可以在此基礎上創建一個更好的解決方案。

延遲加載屬性模式

只有在訪問該屬性時才執行計算是一個好的開始。您真正需要的是在那之後緩存信息,然後僅使用該緩存的數據結果。

但是,有個問題需要我們考慮,您將這些信息緩存在何處以便於訪問呢?

最簡單的方法是定義一個具有相同名稱的屬性,並將其值設置為計算出的數據,如下所示:

class MyClass {
get data() {
const actualData = someExpensiveComputation();
Object.defineProperty(this, "data", {
value: actualData,
writable: false,
configurable: false,
enumerable: false
});
return actualData;
}
}

在這裏,該data屬性再次被定義為該類的getter,但是這一次它將緩存結果

調用Object.defineProperty()創建一個名為的新屬性data,該屬性的固定值為actualData,並且被設置為不可寫,不可配置和可枚舉。

下次data訪問該屬性時,它將從新創建的屬性中讀取而不是調用getter:

const object = new MyClass();
// calls the getter
const data1 = object.data;
// reads from the data property
const data2 = object.data;

實際上,所有計算僅在第一次讀取數據屬性時完成。數據屬性的每次後續讀取都將返回緩存的版本。這種模式的缺點是data屬性開始時是不可枚舉的原型屬性,最後是不可枚舉的自己的屬性:

const object = new MyClass();
console.log(object.hasOwnProperty("data")); // false
const data = object.data;
console.log(object.hasOwnProperty("data")); // true

儘管這種區別在許多情況下並不重要,但了解這種模式很重要,因為在傳遞對象時,這種模式可能會引起細微的問題。

幸運的是,我們可以使用接下來的模式很容易解決這個問題。

類的延遲加載屬性

如果您有一個實例,對於這個實例,延遲加載屬性存在很重要,那麼您可以使用Object.defineProperty()在類構造函數內部創建該屬性。

它比前面的示例有點混亂,但是它將確保該屬性僅存在於實例上。這是一個例子:

class MyClass {
constructor() {
Object.defineProperty(this, "data", {
get() {
const actualData = someExpensiveComputation();
Object.defineProperty(this, "data", {
value: actualData,
writable: false,
configurable: false
});
return actualData;
},
configurable: true,
enumerable: true
});
}
}

我們從這個例子中可以發現,構造函數使用創建data訪問器屬性Object.defineProperty()。該屬性是在實例上創建的(使用this),定義了一個getter並指定了可枚舉和可配置的屬性。

將data屬性設置為可配置尤其重要,這樣您可以Object.defineProperty()再次調用它。

然後,getter函數進行計算並再次調用Object.defineProperty()。對於data來說,將該屬性重新定義為具有特定值的數據屬性,並且將其變為不可寫且不可配置以保護最終數據。下次data讀取該屬性時,將從存儲的值中讀取該屬性。該data屬性現在僅作為自己的屬性存在,並且在第一次讀取之前和之後都具有相同的作用:

const object = new MyClass();
console.log(object.hasOwnProperty("data")); // true

const data = object.data;
console.log(object.hasOwnProperty("data")); // true

對於類,這很可能是您要使用的模式。另一方面,對象模式下可以使用更簡單的方法。

對象的延遲加載屬性

如果使用的是對象模式而不是類,則過程要簡單得多,因為在對象模式上定義的getter與數據屬性一樣被定義為可枚舉的自身屬性(而不是原型屬性)。這意味着您可以為類使用延遲加載屬性模式,而不會造成混亂:

const object = {
get data() {
const actualData = someExpensiveComputation();

Object.defineProperty(this, "data", {
value: actualData,
writable: false,
configurable: false,
enumerable: false
});

return actualData;
}
};
console.log(object.hasOwnProperty("data")); // true
const data = object.data;
console.log(object.hasOwnProperty("data")); // true

總結

通過從重新定義為數據屬性的訪問器屬性開始,您可以將計算推遲到第一次讀取該屬性時,然後將結果緩存起來以備後用。這種方法適用於類和對象文字,並且在對象模式中更簡單一些,因為您不必擔心getter最終會出現在原型上。

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10271

沐鳴登陸地址_ES6 exports 與 import 使用

在創建JavaScript模塊時,export 用於從模塊中導出實時綁定的函數、對象或原始值,以便其他程序可以通過 import使用它們。
被導出的綁定值依然可以在本地進行修改。
在使用import 進行導入時,這些綁定值只能被導入模塊所讀取,但在 export 導出模塊中對這些綁定值進行修改,所修改的值也會實時地更新。

exports

ES6模塊只支持靜態導出,只可以在模塊的最外層作用域使用export,不可在條件語句與函數作用域中使用。

Named exports (命名導出)

這種方式主要用於導出多個函數或者變量, 明確知道導出的變量名稱。
使用:只需要在變量或函數前面加 export 關鍵字即可。
使用場景:比如 utils、tools、common 之類的工具類函數集,或者全站統一變量等。

export 後面不可以是表達式,因為表達式只有值,沒有名字。

每個模塊包含任意數量的導出。

// lib.js
export const sqrt = Math.sqrt;
export function square(x) {
    return x * x;
}
export function diag(x, y) {
    return sqrt(square(x) + square(y));
}


// index.js 使用方式1
import { square, diag } from 'lib';
console.log(square(11)); // 121

// index.js 使用方式2
import * as lib from 'lib';
console.log(lib.square(11)); // 121

簡寫格式,統一列出需要輸出的變量,例如上面的lib.js可以改寫成:

// lib.js
const sqrt = Math.sqrt;
function square(x) {
    return x * x;
}
function add (x, y) {
    return x + y;
}
export { sqrt, square, add };

Default exports (默認導出)

這種方式主要用於導出類文件或一個功能比較單一的函數文件;
使用:只需要在變量或函數前面加 export default 關鍵字即可。

每個模塊最多只能有一個默認導出;

默認導出可以視為名字是default的模塊輸出變量;

默認導出後面可以是表達式,因為它只需要值。

導出一個值:

export default 123;

導出一個函數:

// myFunc.js
export default function () { ... };

// index.js
import myFunc from 'myFunc';
myFunc();

導出一個類:

// MyClass.js
class MyClass{
  constructor() {}
}
export default MyClass;
// 或者
export { MyClass as default, … };

// index.js
import MyClass from 'MyClass';

export default 與 export 的區別:

不需要知道導出的具體變量名;

導入【import】時不需要 { } 包裹;

Combinations exports (混合導出)

混合導出是 Named exports 和 Default exports 組合導出。

混合導出后,默認導入一定放在命名導入前面;

// lib.js
export const myValue = '';
export const MY_CONST = '';
export function myFunc() {
  ...
}
export function* myGeneratorFunc() {
  ...
}
export default class MyClass {
  ...
}

// index.js
import MyClass, { myValue, myFunc } from 'lib';

Re-exporting (別名導出)

一般情況下,export 導出的變量名是原文件中的變量名,但也可以用 as 關鍵字來指定別名。這樣做是為了簡化或者語義化 export 的函數名。

同一個變量允許使用不同名字輸出多次

// lib.js
function getName() {
   ...
};
function setName() {
  ...
};

export {
  getName as get,
  getName as getUserName,
  setName as set
}

Module Redirects (中轉模塊導出)

為了方便使用模塊導入,在一個父模塊中“導入-導出”不同模塊。簡單來說:創建單個模塊,集中多個模塊的多個導出。
使用:使用 export from 語法實現;

export * from 'lib'; // 沒有設置 export default
export * as myFunc2 from 'myFunc'; // 【ES2021】沒有設置 export default
export { default as function1, function2 } from 'bar.js';

上述例子聯合使用導入和導出:

import { default as function1, function2 } from 'bar.js';
export { function1, function2 };

儘管此時 export 與 import 等效,但以下語法在語法上無效:

import DefaultExport from 'bar.js'; // 有效的
export DefaultExport from 'bar.js'; // 無效的

正確的做法是重命名這個導出:

export { default as DefaultExport } from 'bar.js';

Import

// Named imports
import { foo, bar as b } from './some-module.mjs';

// Namespace import
import * as someModule from './some-module.mjs';

// Default import
import someModule from './some-module.mjs';

// Combinations:
import someModule, * as someModule from './some-module.mjs';
import someModule, { foo, bar as b } from './some-module.mjs';

// Empty import (for modules with side effects)
import './some-module.mjs';

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10264

沐鳴娛樂怎麼樣?_JavaScript中的類型檢查有點麻煩

js 的動態類型有好有壞。好的一面,不必指明變量的類型。不好的是,咱們永遠無法確定變量的類型。

typeof運算符可以確定 js 中的6種類型:

typeof 10;        // => 'number'
typeof 'Hello';   // => 'string'
typeof false;     // => 'boolean'
typeof { a: 1 };  // => 'object'
typeof undefined; // => 'undefined'
typeof Symbol();  // => 'symbol'

同樣,instanceof 運算符用於檢測構造函數的 prototype 屬性是否出現在某個實例對象的原型鏈上。

class Cat { }
const myCat = new Cat();

myCat instanceof Cat; // => true

但是typeof和instanceof的一些行為可能會令人混淆。防範於未然,咱們需要提前了解一些邊緣情況。

1. typeof null

typeof myObject === ‘object’會告知myObject是否是一個對象類型。舉個例子:

const person = { name: '前端小智' };

typeof person; // => 'object'

typeof person是’object’,因為person是一個普通的 JS 對象。

在某場景下,變量值可能需要指定為 null,下面是一些場景:

  • 可以使用null來跳過指示配置對象
  • 使用null初始化稍後要保存對象的變量
  • 當函數由於某種原因無法構造對象時,返回null

例如,如果不存在正則表達式匹配項,則str.match(regExp)方法返回null:

const message = 'Hello';
message.match(/Hi/); // => null

這裏引出一個問題,可以使用typeof 來區分有值的對象和具有 null 值的對象嗎?

let myObject = null;
typeof myObject; // => 'object'

myObject = { prop: 'Value' };
typeof myObject; // => 'object'

從上面可以看出,typeof 對象有值的對象和具有 null 值的對象,得到的結果都是’object’。

可以如下面方法來檢測變量是否有對象且不是null:

function isObject(value) {
  return typeof value === 'object' && value !== null;
}

isObject({});   // => true
isObject(null); // => false

除了檢查value是否為object: typeof value === ‘object’之外,還需要明確地驗證null: value !== null。

2. typeof array

如果試圖檢測一個變量是否包含一個數組,常見的錯誤就是使用typeof操作符:

const colors = ['white', 'blue', 'red'];

typeof colors; // => 'object'

檢測數組的正確方法是使用Array.isArray():

const colors = ['white', 'blue', 'red'];
const hero = { name: 'Batman' };

Array.isArray(colors); // => true
Array.isArray(hero);   // => false

Array.isArray(colors)返回一個布爾值true,表示colors是一個數組。

3.虛值類型檢查

JS中的undefined是一個特殊值,表示未初始化的變量。

如果試圖訪問未初始化的變量、不存在的對象屬性,則獲取到的值為 undefined :

let city;
let hero = { name: '前端小智', villain: false };

city;     // => undefined
hero.age; // => undefined

訪問未初始化的變量 city 和不存在的屬性hero.age的結果為undefined。

要檢查屬性是否存在,可以在條件中使用object[propName],這種遇到值為虛值或者undefined是不可靠的:

function getProp(object, propName, def) {
  // 錯誤方式
  if (!object[propName]) {
    return def;
  }
  return object[propName];
}

const hero = { name: '前端小智', villain: false };

getProp(hero, 'villain', true);  // => true
hero.villain;                    // => false

如果對象中不存在propName,則object [propName]的值為undefined。 if (!object[propName]) { return def }保護缺少的屬性。

hero.villain屬性存在且值為false。 但是,該函數在訪問villan值時錯誤地返回true:getProp(hero, ‘villain’, true)

undefined是一個虛值,同樣false、0和”和null。

不要使用虛值作為類型檢查,而是要明確驗證屬性是否存在於對象中:

  • typeof object[propName] === ‘undefined’
  • propName in object
  • object.hasOwnProperty(propName)

接着,咱們來改進getProp()函數:

function getProp(object, propName, def) {
  // Better
  if (!(propName in object)) {
    return def;
  }
  return object[propName];
}

const hero = { name: '前端小智', villain: false };

getProp(hero, 'villain', true);  // => false
hero.villain;                    // => false

if (!(propName in object)) { … }條件正確確定屬性是否存在。

邏輯運算符

我認為最好避免使用邏輯運算符||作為默情況,這個容易打斷閱讀的流程:

const hero = { name: '前端小智', villain: false };

const name = hero.name || 'Unknown';
name;      // => '前端小智'
hero.name; // => '前端小智'

// 不好方式
const villain = hero.villain || true;
villain;      // => true
hero.villain; // => false

hero 對象存在屬性villain,值為 false,但是表達式hero.villain || true結果為true。

邏輯操作符||用作訪問屬性的默認情況,當屬性存在且具有虛值時,該操作符無法正確工作。

若要在屬性不存在時默認設置,更好的選擇是使用新的雙問號(??)操作符,

const hero = { name: '前端小智', villan: false };

// 好的方式
const villain = hero.villain ?? true;
villain;      // => false
hero.villain; // => false

或使用解構賦值:

const hero = { name: '前端小智', villain: false };

// Good
const { villain = true } = hero;
villain;      // => false
hero.villain; // => false

4. typeof NaN

整數,浮點數,特殊数字(例如Infinity,NaN)的類型均為数字。

typeof 10;       // => 'number'
typeof 1.5;      // => 'number'
typeof NaN;      // => 'number'
typeof Infinity; // => 'number'

NaN是在無法創建数字時創建的特殊數值。NaN是not a number的縮寫。

在下列情況下不能創建数字:

Number('oops'); // => NaN

5 * undefined; // => NaN
Math.sqrt(-1); // => NaN

NaN + 10; // => NaN

由於NaN,意味着對数字的操作失敗,因此對数字有效性的檢查需要額外的步驟。

下面的isValidNumber()函數也可以防止NaN導致的錯誤:

function isValidNumber(value) {
  // Good
  return typeof value === 'number' && !isNaN(value);
}

isValidNumber(Number('Z99')); // => false
isValidNumber(5 * undefined); // => false
isValidNumber(undefined);     // => false

isValidNumber(Number('99'));  // => true
isValidNumber(5 + 10);        // => true

除了typeof value === ‘number’之外,還多驗證!isNaN(value)確保萬無一失。

5.instanceof 和原型鏈

JS 中的每個對象都引用一個特殊的函數:對象的構造函數。

object instanceof Constructor是用於檢查對象的構造函數的運算符:

const object = {};
object instanceof Object; // => true

const array = [1, 2];
array instanceof Array; // => true

const promise = new Promise(resolve => resolve('OK'));
promise instanceof Promise; // => true

現在,咱們定義一個父類Pet和它的子類Cat:

class Pet {
  constructor(name) {
    this.name;
  }
}

class Cat extends Pet {
  sound = 'Meow';
}

const myCat = new Cat('Scratchy');

現在,嘗試確定myCat的實例

myCat instanceof Cat;    // => true
myCat instanceof Pet;    // => true
myCat instanceof Object; // => true

instanceof運算符表示myCat是Cat,Pet甚至Object的實例。

instanceof操作符通過整個原型鏈搜索對象的構造函數。要準確地檢測創建對象的構造函數,需要檢測 constructor 屬性

myCat.constructor === Cat;    // => true
myCat.constructor === Pet;    // => false
myCat.constructor === Object; // => false

只有myCat.constructor === Cat的計算結果為true,表示 Cat 是 myCat實例的構造函數。

6. 總結

運算符typeof和instanceof 用於類型檢查。 它們儘管易於使用,但需要注意一些特殊情況。

需要注意的是:typeof null等於’object’。 要確定變量是否包含非null對象,需要显示指明null:

typeof myObject === 'object' && myObject !== null

檢查變量是否包含數組的最佳方法是使用Array.isArray(variable)內置函數。

因為undefined是虛值的,所以我們經常直接在條件句中使用它,但這種做法容易出錯。更好的選擇是使用prop in object來驗證屬性是否存在。

使用雙問號操作系符號object.prop ?? def 或者 { prop = def } = object 來訪問可能丟失的屬性。

NaN是一個類型為number的特殊值,它是由對数字的無效操作創建的。為了確保變量有正確的数字,最好使用更詳細的驗證:!isNaN(number) && typeof number === ‘number’。

最後,請記住instanceof通過prototype鏈搜索實例的構造函數。如果不知道這一點,那麼如果使用父類驗證子類實例,可能會得到錯誤的結果。

原文:https://dmitripavlutin.com/

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

2.廣告聯盟: 整理了目前主流的廣告聯盟平台,如果你有流量,可以作為參考選擇適合你的平台點擊進入

鏈接: http://www.fly63.com/article/detial/7403

沐鳴娛樂_JS變量存儲與深拷貝和淺拷貝

變量類型與存儲空間

棧內存和堆內存

基本數據類型

string、number、null、undefined、boolean、symbol(ES6新增) 變量值存放在棧內存中,可直接訪問和修改變量的值
基本數據類型不存在拷貝,好比如說你無法修改數值1的值

引用類型

Object Function RegExp Math Date 值為對象,存放在堆內存中
在棧內存中變量保存的是一個指針,指向對應在堆內存中的地址。
當訪問引用類型的時候,要先從棧中取出該對象的地址指針,然後再從堆內存中取得所需的數據

圖解存儲空間

let a1 = 0; // 棧內存
let a2 = "this is string" // 棧內存
let a3 = null; // 棧內存
let b = { x: 10 }; // 變量b存在於棧中,{ x: 10 }作為對象存在於堆中
let c = [1, 2, 3]; // 變量c存在於棧中,[1, 2, 3]作為對象存在於堆中

引用類型的賦值

let a = { x: 10, y: 20 }
let b = a;
b.x = 5;
console.log(a.x); // 5

深拷貝和淺拷貝

深拷貝

將一個對象從內存中完整的拷貝一份出來,從堆內存中開闢一個新的區域存放新對象,且修改新對象不會影響原對象

淺拷貝

淺拷貝是按位拷貝對象,它會創建一個新對象,這個對象有着原始對象屬性值的一份精確拷貝。如果屬性是基本類型,拷貝的就是基本類型的值;如果屬性是內存地址(引用類型),拷貝的就是內存地址

對象的賦值

當我們把一個對象賦值給一個新的變量時,賦的其實是該對象的在棧中的地址,而不是堆中的數據。也就是兩個對象指向的是同一個存儲空間,無論哪個對象發生改變,其實都是改變的存儲空間的內容,因此,兩個對象是聯動的。

三者對比

淺拷貝的常用的五種方法

Object.assign()

Object.assign() 方法可以把任意多個的源對象自身的可枚舉屬性拷貝給目標對象,然後返回目標對象。但是 Object.assign()進行的是淺拷貝

Object.assign 會從左往右遍歷源對象(sources)的所有屬性,然後用 = 賦值到目標對象(target)

        var obj = { a: {a: "kobe", b: 39},b:1 };
        var initalObj = Object.assign({}, obj);
        initalObj.a.a = "wade";
        initalObj.b = 2;
        console.log(obj.a.a); //wade
        console.log(obj.b); //1

擴展運算符

let obj = {a:1,b:{c:1}}
let obj2 = {...obj};
obj.a=2;
console.log(obj); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}

obj.b.c = 2;
console.log(obj); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}

Array.prototype.slice

slice() 方法返回一個新的數組對象,這一對象是一個由 begin和 end(不包括end)決定的原數組的淺拷貝。原始數組的基本類型不會被改變,引用類型會被改變。

let arr = [1, 3, {
    username: ' kobe'
    }];
let arr3 = arr.slice();
arr3[0]=0;
arr3[2].username = 'wade'
console.log(arr);

Array.prototype.concat()

let arr = [1, 3, {
    username: 'kobe'
    }];
let arr2=arr.concat();   
arr3[0]=0;
arr2[2].username = 'wade';
console.log(arr);

手寫淺拷貝

function shallowCopy(src) {
    var dst = {};
    for (var prop in src) {
        if (src.hasOwnProperty(prop)) {
            dst[prop] = src[prop];
        }
    }
    return dst;
}

深拷貝的常用方法

jsON.parse(jsON.stringify())

通過JSON.stringify實現深拷貝有幾點要注意

拷貝的對象的值中如果有函數,undefined,symbol則經過JSON.stringify()序列化后的JSON字符串中這個鍵值對會消失

無法拷貝不可枚舉的屬性,無法拷貝對象的原型鏈

拷貝Date引用類型會變成字符串

拷貝RegExp引用類型會變成空對象

對象中含有NaN、Infinity和-Infinity,則序列化的結果會變成null

無法拷貝對象的循環應用(即obj[key] = obj)

let arr = [1, 3, {
    username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan'; 
console.log(arr, arr4)

$.extend(true, object, object1,);

參考jQuery官方文檔

手寫乞丐版深拷貝

首先這個deepClone函數並不能複製不可枚舉的屬性以及Symbol類型

這裏只是針對Object引用類型的值做的循環迭代,而對於Array,Date,RegExp,Error,Function引用類型無法正確拷貝

對象成環,即循環引用 (例如:obj1.a = obj)

function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

皇帝版深拷貝

該實例來自ConardLi大佬的github,源地址:https://github.com/ConardLi/

    const mapTag = "[object Map]";
    const setTag = "[object Set]";
    const arrayTag = "[object Array]";
    const objectTag = "[object Object]";
    const argsTag = "[object Arguments]";

    const boolTag = "[object Boolean]";
    const dateTag = "[object Date]";
    const numberTag = "[object Number]";
    const stringTag = "[object String]";
    const symbolTag = "[object Symbol]";
    const errorTag = "[object Error]";
    const regexpTag = "[object RegExp]";
    const funcTag = "[object Function]";

    const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];

    function forEach(array, iteratee) {
      let index = -1;
      const length = array.length;
      while (++index < length) {
        iteratee(array[index], index);
      }
      return array;
    }

    function isObject(target) {
      const type = typeof target;
      return target !== null && (type === "object" || type === "function");
    }

    function getType(target) {
      return Object.prototype.toString.call(target);
    }

    function getInit(target) {
      const Ctor = target.constructor;
      return new Ctor();
    }

    function cloneSymbol(targe) {
      return Object(Symbol.prototype.valueOf.call(targe));
    }

    function cloneReg(targe) {
      const reFlags = /\w*$/;
      const result = new targe.constructor(targe.source, reFlags.exec(targe));
      result.lastIndex = targe.lastIndex;
      return result;
    }

    function cloneFunction(func) {
      const bodyReg = /(?<={)(.|\n)+(?=})/m;
      const paramReg = /(?<=\().+(?=\)\s+{)/;
      const funcString = func.toString();
      if (func.prototype) {
        const param = paramReg.exec(funcString);
        const body = bodyReg.exec(funcString);
        if (body) {
          if (param) {
            const paramArr = param[0].split(",");
            return new Function(...paramArr, body[0]);
          } else {
            return new Function(body[0]);
          }
        } else {
          return null;
        }
      } else {
        return eval(funcString);
      }
    }

    function cloneOtherType(targe, type) {
      const Ctor = targe.constructor;
      switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
          return new Ctor(targe);
        case regexpTag:
          return cloneReg(targe);
        case symbolTag:
          return cloneSymbol(targe);
        case funcTag:
          return cloneFunction(targe);
        default:
          return null;
      }
    }

    function clone(target, map = new WeakMap()) {
      // 克隆原始類型
      if (!isObject(target)) {
        return target;
      }

      // 初始化
      const type = getType(target);
      let cloneTarget;
      if (deepTag.includes(type)) {
        cloneTarget = getInit(target, type);
      } else {
        return cloneOtherType(target, type);
      }

      // 防止循環引用
      if (map.get(target)) {
        return map.get(target);
      }
      map.set(target, cloneTarget);

      // 克隆set
      if (type === setTag) {
        target.forEach(value => {
          cloneTarget.add(clone(value, map));
        });
        return cloneTarget;
      }

      // 克隆map
      if (type === mapTag) {
        target.forEach((value, key) => {
          cloneTarget.set(key, clone(value, map));
        });
        return cloneTarget;
      }

      // 克隆對象和數組
      const keys = type === arrayTag ? undefined : Object.keys(target);
      forEach(keys || target, (value, key) => {
        if (keys) {
          key = value;
        }
        cloneTarget[key] = clone(target[key], map);
      });

      return cloneTarget;
    }

    const map = new Map();
    map.set("key", "value");
    map.set("ConardLi", "code秘密花園");

    const set = new Set();
    set.add("ConardLi");
    set.add("code秘密花園");

    const target = {
      field1: 1,
      field2: undefined,
      field3: {
        child: "child"
      },
      field4: [2, 4, 8],
      empty: null,
      map,
      set,
      bool: new Boolean(true),
      num: new Number(2),
      str: new String(2),
      symbol: Object(Symbol(1)),
      date: new Date(),
      reg: /\d+/,
      error: new Error(),
      func1: () => {
        console.log("code秘密花園");
      },
      func2: function(a, b) {
        return a + b;
      }
    };

    const result = clone(target);

    console.log(target);
    console.log(result);

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

2.廣告聯盟: 整理了目前主流的廣告聯盟平台,如果你有流量,可以作為參考選擇適合你的平台點擊進入

鏈接: http://www.fly63.com/article/detial/7400