前端面試題(一)JS篇

来源:https://www.cnblogs.com/chuhui/archive/2018/12/03/10060071.html
-Advertisement-
Play Games

內置類型 JS 中分為七種內置類型,七種內置類型又分為兩大類型:基本類型和對象(Object)。 基本類型有六種: ,`undefined boolean number string symbol`。 其中 JS 的數字類型是浮點類型的,沒有整型。並且浮點類型基於 IEEE 754標準實現,在使用中 ...


內置類型

JS 中分為七種內置類型,七種內置類型又分為兩大類型:基本類型和對象(Object)。

基本類型有六種: nullundefinedbooleannumberstringsymbol

其中 JS 的數字類型是浮點類型的,沒有整型。並且浮點類型基於 IEEE 754標準實現,在使用中會遇到某些 BugNaN 也屬於 number 類型,並且 NaN 不等於自身。

對於基本類型來說,如果使用字面量的方式,那麼這個變數只是個字面量,只有在必要的時候才會轉換為對應的類型

let a = 111 // 這隻是字面量,不是 number 類型
a.toString() // 使用時候才會轉換為對象類型

對象(Object)是引用類型,在使用過程中會遇到淺拷貝和深拷貝的問題。

let a = { name: 'FE' }
let b = a
b.name = 'EF'
console.log(a.name) // EF

Typeof

typeof 對於基本類型,除了 null 都可以顯示正確的類型

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof b // b 沒有聲明,但是還會顯示 undefined

typeof 對於對象,除了函數都會顯示 object

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

對於 null 來說,雖然它是基本類型,但是會顯示 object,這是一個存在很久了的 Bug

typeof null // 'object'

PS:為什麼會出現這種情況呢?因為在 JS 的最初版本中,使用的是 32 位系統,為了性能考慮使用低位存儲了變數的類型信息,000 開頭代表是對象,然而 null 表示為全零,所以將它錯誤的判斷為 object 。雖然現在的內部類型判斷代碼已經改變了,但是對於這個 Bug 卻是一直流傳下來。

如果我們想獲得一個變數的正確類型,可以通過 Object.prototype.toString.call(xx)。這樣我們就可以獲得類似 [object Type] 的字元串。

let a
// 我們也可以這樣判斷 undefined
a === undefined
// 但是 undefined 不是保留字,能夠在低版本瀏覽器被賦值
let undefined = 1
// 這樣判斷就會出錯
// 所以可以用下麵的方式來判斷,並且代碼量更少
// 因為 void 後面隨便跟上一個組成表達式
// 返回就是 undefined
a === void 0

類型轉換

轉Boolean

在條件判斷時,除了 undefinednullfalseNaN''0-0,其他所有值都轉為 true,包括所有對象。

對象轉基本類型

對象在轉換基本類型時,首先會調用 valueOf 然後調用 toString。並且這兩個方法你是可以重寫的。

let a = {
    valueOf() {
        return 0
    }
}

當然你也可以重寫 Symbol.toPrimitive ,該方法在轉基本類型時調用優先順序最高。

let a = {
  valueOf() {
    return 0;
  },
  toString() {
    return '1';
  },
  [Symbol.toPrimitive]() {
    return 2;
  }
}
1 + a // => 3
'1' + a // => '12'

四則運算符

只有當加法運算時,其中一方是字元串類型,就會把另一個也轉為字元串類型。其他運算只要其中一方是數字,那麼另一方就轉為數字。並且加法運算會觸發三種類型轉換:將值轉換為原始值,轉換為數字,轉換為字元串。

1 + '1' // '11'
2 * '2' // 4
[1, 2] + [2, 1] // '1,22,1'
// [1, 2].toString() -> '1,2'
// [2, 1].toString() -> '2,1'
// '1,2' + '2,1' = '1,22,1'

對於加號需要註意這個表達式 'a' + + 'b'

'a' + + 'b' // -> "aNaN"
// 因為 + 'b' -> NaN
// 你也許在一些代碼中看到過 + '1' -> 1

== 操作符

image

上圖中的 toPrimitive 就是對象轉基本類型。

這裡來解析一道題目 [] == ![] // -> true ,下麵是這個表達式為何為 true 的步驟

// [] 轉成 true,然後取反變成 false
[] == false
// 根據第 8 條得出
[] == ToNumber(false)
[] == 0
// 根據第 10 條得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根據第 6 條得出
0 == 0 // -> true

比較運算符

  1. 如果是對象,就通過 toPrimitive 轉換對象
  2. 如果是字元串,就通過 unicode 字元索引來比較

原型

prototype

每個函數都有 prototype 屬性,除了 Function.prototype.bind(),該屬性指向原型。

每個對象都有 __proto__ 屬性,指向了創建該對象的構造函數的原型。其實這個屬性指向了 [[prototype]],但是 [[prototype]] 是內部屬性,我們並不能訪問到,所以使用 _proto_ 來訪問。

對象可以通過 __proto__ 來尋找不屬於該對象的屬性,__proto__ 將對象連接起來組成了原型鏈。

如果你想更進一步的瞭解原型,可以仔細閱讀 深度解析原型中的各個難點

new

  1. 新生成了一個對象
  2. 鏈接到原型
  3. 綁定 this
  4. 返回新對象

在調用 new 的過程中會發生以上四件事情,我們也可以試著來自己實現一個 new

function create() {
    // 創建一個空的對象
    let obj = new Object()
    // 獲得構造函數
    let Con = [].shift.call(arguments)
    // 鏈接到原型
    obj.__proto__ = Con.prototype
    // 綁定 this,執行構造函數
    let result = Con.apply(obj, arguments)
    // 確保 new 出來的是個對象
    return typeof result === 'object' ? result : obj
}

對於實例對象來說,都是通過 new 產生的,無論是 function Foo() 還是 let a = { b : 1 }

對於創建一個對象來說,更推薦使用字面量的方式創建對象(無論性能上還是可讀性)。因為你使用 new Object() 的方式創建對象需要通過作用域鏈一層層找到 Object,但是你使用字面量的方式就沒這個問題。

function Foo() {}
// function 就是個語法糖
// 內部等同於 new Function()
let a = { b: 1 }
// 這個字面量內部也是使用了 new Object()

對於 new 來說,還需要註意下運算符優先順序。

function Foo() {
    return this;
}
Foo.getName = function () {
    console.log('1');
};
Foo.prototype.getName = function () {
    console.log('2');
};

new Foo.getName();   // -> 1
new Foo().getName(); // -> 2       

image

從上圖可以看出,new Foo() 的優先順序大於 new Foo ,所以對於上述代碼來說可以這樣劃分執行順序

new (Foo.getName());   
(new Foo()).getName();

對於第一個函數來說,先執行了 Foo.getName() ,所以結果為 1;對於後者來說,先執行 new Foo() 產生了一個實例,然後通過原型鏈找到了 Foo 上的 getName 函數,所以結果為 2。

instanceof

instanceof 可以正確的判斷對象的類型,因為內部機制是通過判斷對象的原型鏈中是不是能找到類型的 prototype

我們也可以試著實現一下 instanceof

function instanceof(left, right) {
    // 獲得類型的原型
    let prototype = right.prototype
    // 獲得對象的原型
    left = left.__proto__
    // 判斷對象的類型是否等於類型的原型
    while (true) {
        if (left === null)
            return false
        if (prototype === left)
            return true
        left = left.__proto__
    }
}

this

this 是很多人會混淆的概念,但是其實他一點都不難,你只需要記住幾個規則就可以了。

function foo() {
    console.log(this.a)
}
var a = 1
foo()

var obj = {
    a: 2,
    foo: foo
}
obj.foo()

// 以上兩者情況 `this` 只依賴於調用函數前的對象,優先順序是第二個情況大於第一個情況

// 以下情況是優先順序最高的,`this` 只會綁定在 `c` 上,不會被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)

// 還有種就是利用 call,apply,bind 改變 this,這個優先順序僅次於 new

以上幾種情況明白了,很多代碼中的 this 應該就沒什麼問題了,下麵讓我們看看箭頭函數中的 this

function a() {
    return () => {
        return () => {
            console.log(this)
        }
    }
}
console.log(a()()())

箭頭函數其實是沒有 this 的,這個函數中的 this 只取決於他外面的第一個不是箭頭函數的函數的 this。在這個例子中,因為調用 a 符合前面代碼中的第一個情況,所以 thiswindow。並且 this 一旦綁定了上下文,就不會被任何代碼改變。

執行上下文

當執行 JS 代碼時,會產生三種執行上下文

  • 全局執行上下文
  • 函數執行上下文
  • eval 執行上下文

每個執行上下文中都有三個重要的屬性

  • 變數對象(VO),包含變數、函數聲明和函數的形參,該屬性只能在全局上下文中訪問
  • 作用域鏈(JS 採用詞法作用域,也就是說變數的作用域是在定義時就決定了)
  • this
var a = 10
function foo(i) {
  var b = 20
}
foo()

對於上述代碼,執行棧中有兩個上下文:全局上下文和函數 foo 上下文。

stack = [
    globalContext,
    fooContext
]

對於全局上下文來說,VO 大概是這樣的

globalContext.VO === globe
globalContext.VO = {
    a: undefined,
    foo: <Function>,
}

對於函數 foo 來說,VO 不能訪問,只能訪問到活動對象(AO)

fooContext.VO === foo.AO
fooContext.AO {
    i: undefined,
    b: undefined,
    arguments: <>
}
// arguments 是函數獨有的對象(箭頭函數沒有)
// 該對象是一個偽數組,有 `length` 屬性且可以通過下標訪問元素
// 該對象中的 `callee` 屬性代表函數本身
// `caller` 屬性代表函數的調用者

對於作用域鏈,可以把它理解成包含自身變數對象和上級變數對象的列表,通過 [[Scope]] 屬性查找上級變數

fooContext.[[Scope]] = [
    globalContext.VO
]
fooContext.Scope = fooContext.[[Scope]] + fooContext.VO
fooContext.Scope = [
    fooContext.VO,
    globalContext.VO
]

接下來讓我們看一個老生常談的例子,var

b() // call b
console.log(a) // undefined

var a = 'Hello world'

function b() {
    console.log('call b')
}

想必以上的輸出大家肯定都已經明白了,這是因為函數和變數提升的原因。通常提升的解釋是說將聲明的代碼移動到了頂部,這其實沒有什麼錯誤,便於大家理解。但是更準確的解釋應該是:在生成執行上下文時,會有兩個階段。第一個階段是創建的階段(具體步驟是創建 VO),JS 解釋器會找出需要提升的變數和函數,並且給他們提前在記憶體中開闢好空間,函數的話會將整個函數存入記憶體中,變數只聲明並且賦值為 undefined,所以在第二個階段,也就是代碼執行階段,我們可以直接提前使用。

在提升的過程中,相同的函數會覆蓋上一個函數,並且函數優先於變數提升

b() // call b second

function b() {
    console.log('call b fist')
}
function b() {
    console.log('call b second')
}
var b = 'Hello world'

var 會產生很多錯誤,所以在 ES6中引入了 letlet 不能在聲明前使用,但是這並不是常說的 let 不會提升,let 提升了聲明但沒有賦值,因為臨時死區導致了並不能在聲明前使用。

對於非匿名的立即執行函數需要註意以下一點

var foo = 1
(function foo() {
    foo = 10
    console.log(foo)
}()) // -> ƒ foo() { foo = 10 ; console.log(foo) }

因為當 JS 解釋器在遇到非匿名的立即執行函數時,會創建一個輔助的特定對象,然後將函數名稱作為這個對象的屬性,因此函數內部才可以訪問到 foo,但是這個值又是只讀的,所以對它的賦值並不生效,所以列印的結果還是這個函數,並且外部的值也沒有發生更改。

specialObject = {};
 
Scope = specialObject + Scope;
 
foo = new FunctionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}
 
delete Scope[0]; // remove specialObject from the front of scope chain

閉包

閉包的定義很簡單:函數 A 返回了一個函數 B,並且函數 B 中使用了函數 A 的變數,函數 B 就被稱為閉包。

function A() {
  let a = 1
  function B() {
      console.log(a)
  }
  return B
}

你是否會疑惑,為什麼函數 A 已經彈出調用棧了,為什麼函數 B 還能引用到函數 A 中的變數。因為函數 A 中的變數這時候是存儲在堆上的。現在的 JS 引擎可以通過逃逸分析辨別出哪些變數需要存儲在堆上,哪些需要存儲在棧上。

經典面試題,迴圈中使用閉包解決 var 定義函數的問題

for ( var i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
}

首先因為 setTimeout 是個非同步函數,所有會先把迴圈全部執行完畢,這時候 i 就是 6 了,所以會輸出一堆 6。

解決辦法兩種,第一種使用閉包

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}

第二種就是使用 setTimeout 的第三個參數

for ( var i=1; i<=5; i++) {
    setTimeout( function timer(j) {
        console.log( j );
    }, i*1000, i);
}

第三種就是使用 let 定義 i

for ( let i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
}

因為對於 let 來說,他會創建一個塊級作用域,相當於

{ // 形成塊級作用域
  let i = 0
  {
    let ii = i
    setTimeout( function timer() {
        console.log( ii );
    }, i*1000 );
  }
  i++
  {
    let ii = i
  }
  i++
  {
    let ii = i
  }
  ...
}

深淺拷貝

let a = {
    age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2

從上述例子中我們可以發現,如果給一個變數賦值一個對象,那麼兩者的值會是同一個引用,其中一方改變,另一方也會相應改變。

通常在開發中我們不希望出現這樣的問題,我們可以使用淺拷貝來解決這個問題。

淺拷貝

首先可以通過 Object.assign 來解決這個問題。

let a = {
    age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1

當然我們也可以通過展開運算符(…)來解決

let a = {
    age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // 1

通常淺拷貝就能解決大部分問題了,但是當我們遇到如下情況就需要使用到深拷貝了

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = {...a}
a.jobs.first = 'native'
console.log(b.jobs.first) // native

淺拷貝只解決了第一層的問題,如果接下去的值中還有對象的話,那麼就又回到剛開始的話題了,兩者享有相同的引用。要解決這個問題,我們需要引入深拷貝。

深拷貝

這個問題通常可以通過 JSON.parse(JSON.stringify(object)) 來解決。

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

但是該方法也是有局限性的:

  • 會忽略 undefined
  • 會忽略 symbol
  • 不能序列化函數
  • 不能解決迴圈引用的對象
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)

如果你有這麼一個迴圈引用對象,你會發現你不能通過該方法深拷貝

image

在遇到函數、 undefined 或者 symbol 的時候,該對象也不能正常的序列化

let a = {
    age: undefined,
    sex: Symbol('male'),
    jobs: function() {},
    name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}

你會發現在上述情況中,該方法會忽略掉函數和 undefined

但是在通常情況下,複雜數據都是可以序列化的,所以這個函數可以解決大部分問題,並且該函數是內置函數中處理深拷貝性能最快的。當然如果你的數據中含有以上三種情況下,可以使用 lodash 的深拷貝函數

如果你所需拷貝的對象含有內置類型並且不包含函數,可以使用 MessageChannel

function structuralClone(obj) {
  return new Promise(resolve => {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
  });
}

var obj = {a: 1, b: {
    c: b
}}
// 註意該方法是非同步的
// 可以處理 undefined 和迴圈引用對象
(async () => {
  const clone = await structuralClone(obj)
})()

模塊化

在有 Babel 的情況下,我們可以直接使用 ES6 的模塊化

// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}

import {a, b} from './a.js'
import XXX from './b.js'

CommonJS

CommonJs 是 Node 獨有的規範,瀏覽器中使用就需要用到 Browserify 解析了。

// a.js
module.exports = {
    a: 1
}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1

在上述代碼中,module.exportsexports 很容易混淆,讓我們來看看大致內部實現

var module = require('./a.js')
module.a
// 這裡其實就是包裝了一層立即執行函數,這樣就不會污染全局變數了,
// 重要的是 module 這裡,module 是 Node 獨有的一個變數
module.exports = {
    a: 1
}
// 基本實現
var module = {
  exports: {} // exports 就是個空對象
}
// 這個是為什麼 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
    // 導出的東西
    var a = 1
    module.exports = a
    return module.exports
};

再來說說 module.exportsexports,用法其實是相似的,但是不能對 exports 直接賦值,不會有任何效果。

對於 CommonJS 和 ES6 中的模塊化的兩者區別是:

  • 前者支持動態導入,也就是 require(${path}/xx.js),後者目前不支持,但是已有提案
  • 前者是同步導入,因為用於服務端,文件都在本地,同步導入即使卡住主線程影響也不大。而後者是非同步導入,因為用於瀏覽器,需要下載文件,如果也採用同步導入會對渲染有很大影響

  • 前者在導出時都是值拷貝,就算導出的值變了,導入的值也不會改變,所以如果想更新值,必須重新導入一次。但是後者採用實時綁定的方式,導入導出的值都指向同一個記憶體地址,所以導入值會跟隨導出值變化
  • 後者會編譯成 require/exports 來執行的

AMD

AMD 是由 RequireJS 提出的

// AMD
define(['./a', './b'], function(a, b) {
    a.do()
    b.do()
})
define(function(require, exports, module) {   
    var a = require('./a')  
    a.doSomething()   
    var b = require('./b')
    b.doSomething()
})

防抖

你是否在日常開發中遇到一個問題,在滾動事件中需要做個複雜計算或者實現一個按鈕的防二次點擊操作。

這些需求都可以通過函數防抖動來實現。尤其是第一個需求,如果在頻繁的事件回調中做複雜計算,很有可能導致頁面卡頓,不如將多次計算合併為一次計算,只在一個精確點做操作。

PS:防抖和節流的作用都是防止函數多次調用。區別在於,假設一個用戶一直觸發這個函數,且每次觸發函數的間隔小於wait,防抖的情況下只會調用一次,而節流的 情況會每隔一定時間(參數wait)調用函數。

我們先來看一個袖珍版的防抖理解一下防抖的實現:

// func是用戶傳入需要防抖的函數
// wait是等待時間
const debounce = (func, wait = 50) => {
  // 緩存一個定時器id
  let timer = 0
  // 這裡返回的函數是每次用戶實際調用的防抖函數
  // 如果已經設定過定時器了就清空上一次的定時器
  // 開始一個新的定時器,延遲執行用戶傳入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
// 不難看出如果用戶調用該函數的間隔小於wait的情況下,上一次的時間還未到就被清除了,並不會執行函數

這是一個簡單版的防抖,但是有缺陷,這個防抖只能在最後調用。一般的防抖會有immediate選項,表示是否立即調用。這兩者的區別,舉個慄子來說:

  • 例如在搜索引擎搜索問題的時候,我們當然是希望用戶輸入完最後一個字才調用查詢介面,這個時候適用延遲執行的防抖函數,它總是在一連串(間隔小於wait的)函數觸發之後調用。
  • 例如用戶給interviewMap點star的時候,我們希望用戶點第一下的時候就去調用介面,並且成功之後改變star按鈕的樣子,用戶就可以立馬得到反饋是否star成功了,這個情況適用立即執行的防抖函數,它總是在第一次調用,並且下一次調用必須與前一次調用的時間間隔大於wait才會觸發。

下麵我們來實現一個帶有立即執行選項的防抖函數

// 這個是用來獲取當前時間戳的
function now() {
  return +new Date()
}
/**
 * 防抖函數,返回函數連續調用時,空閑時間必須大於或等於 wait,func 才會執行
 *
 * @param  {function} func        回調函數
 * @param  {number}   wait        表示時間視窗的間隔
 * @param  {boolean}  immediate   設置為ture時,是否立即調用函數
 * @return {function}             返回客戶調用函數
 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args
  
  // 延遲執行函數
  const later = () => setTimeout(() => {
    // 延遲函數執行完畢,清空緩存的定時器序號
    timer = null
    // 延遲執行的情況下,函數會在延遲函數中執行
    // 使用到之前緩存的參數和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 這裡返回的函數是每次實際調用的函數
  return function(...params) {
    // 如果沒有創建延遲執行函數(later),就創建一個
    if (!timer) {
      timer = later()
      // 如果是立即執行,調用函數
      // 否則緩存參數和調用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 如果已有延遲執行函數(later),調用的時候清除原來的並重新設定一個
    // 這樣做延遲函數會重新計時
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}

整體函數實現的不難,總結一下。

  • 對於按鈕防點擊來說的實現:如果函數是立即執行的,就立即調用,如果函數是延遲執行的,就緩存上下文和參數,放到延遲函數中去執行。一旦我開始一個定時器,只要我定時器還在,你每次點擊我都重新計時。一旦你點累了,定時器時間到,定時器重置為 null,就可以再次點擊了。
  • 對於延時執行函數來說的實現:清除定時器ID,如果是延遲調用就調用函數

節流

防抖動和節流本質是不一樣的。防抖動是將多次執行變為最後一次執行,節流是將多次執行變成每隔一段時間執行。

/**
 * underscore 節流函數,返回函數連續調用時,func 執行頻率限定為 次 / wait
 *
 * @param  {function}   func      回調函數
 * @param  {number}     wait      表示時間視窗的間隔
 * @param  {object}     options   如果想忽略開始函數的的調用,傳入{leading: false}。
 *                                如果想忽略結尾函數的調用,傳入{trailing: false}
 *                                兩者不能共存,否則函數不能執行
 * @return {function}             返回客戶調用函數   
 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 之前的時間戳
    var previous = 0;
    // 如果 options 沒傳則設為空對象
    if (!options) options = {};
    // 定時器回調函數
    var later = function() {
      // 如果設置了 leading,就將 previous 設為 0
      // 用於下麵函數的第一個 if 判斷
      previous = options.leading === false ? 0 : _.now();
      // 置空一是為了防止記憶體泄漏,二是為了下麵的定時器判斷
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      // 獲得當前時間戳
      var now = _.now();
      // 首次進入前者肯定為 true
      // 如果需要第一次不執行函數
      // 就將上次時間戳設為當前的
      // 這樣在接下來計算 remaining 的值時會大於0
      if (!previous && options.leading === false) previous = now;
      // 計算剩餘時間
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 如果當前調用已經大於上次調用時間 + wait
      // 或者用戶手動調了時間
      // 如果設置了 trailing,只會進入這個條件
      // 如果沒有設置 leading,那麼第一次會進入這個條件
      // 還有一點,你可能會覺得開啟了定時器那麼應該不會進入這個 if 條件了
      // 其實還是會進入的,因為定時器的延時
      // 並不是準確的時間,很可能你設置了2秒
      // 但是他需要2.2秒才觸發,這時候就會進入這個條件
      if (remaining <= 0 || remaining > wait) {
        // 如果存在定時器就清理掉否則會調用二次回調
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // 判斷是否設置了定時器和 trailing
        // 沒有的話就開啟一個定時器
        // 並且不能不能同時設置 leading 和 trailing
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };

繼承

在 ES5 中,我們可以使用如下方式解決繼承的問題

function Super() {}
Super.prototype.getNumber = function() {
  return 1
}

function Sub() {}
let s = new Sub()
Sub.prototype = Object.create(Super.prototype, {
  constructor: {
    value: Sub,
    enumerable: false,
    writable: true,
    configurable: true
  }
})

以上繼承實現思路就是將子類的原型設置為父類的原型

在 ES6 中,我們可以通過 class 語法輕鬆解決這個問題

class MyDate extends Date {
  test() {
    return this.getTime()
  }
}
let myDate = new MyDate()
myDate.test()

但是 ES6 不是所有瀏覽器都相容,所以我們需要使用 Babel 來編譯這段代碼。

如果你使用編譯過得代碼調用 myDate.test() 你會驚奇地發現出現了報錯

image

因為在 JS 底層有限制,如果不是由 Date 構造出來的實例的話,是不能調用 Date 里的函數的。所以這也側面的說明瞭:ES6 中的 class 繼承與 ES5 中的一般繼承寫法是不同的

既然底層限制了實例必須由 Date 構造出來,那麼我們可以改變下思路實現繼承

function MyData() {

}
MyData.prototype.test = function () {
  return this.getTime()
}
let d = new Date()
Object.setPrototypeOf(d, MyData.prototype)
Object.setPrototypeOf(MyData.prototype, Date.prototype)

以上繼承實現思路:先創建父類實例 => 改變實例原先的 _proto__ 轉而連接到子類的 prototype => 子類的 prototype__proto__ 改為父類的 prototype

通過以上方法實現的繼承就可以完美解決 JS 底層的這個限制。

call, apply, bind 區別

首先說下前兩者的區別。

callapply 都是為瞭解決改變 this 的指向。作用都是相同的,只是傳參的方式不同。

除了第一個參數外,call 可以接收一個參數列表,apply 只接受一個參數數組。

let a = {
    value: 1
}
function getValue(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value)
}
getValue.call(a, 'yck', '24')
getValue.apply(a, ['yck', '24'])

模擬實現 call 和 apply

可以從以下幾點來考慮如何實現

  • 不傳入第一個參數,那麼預設為 window
  • 改變了 this 指向,讓新的對象可以執行該函數。那麼思路是否可以變成給新的對象添加一個函數,然後在執行完以後刪除?
Function.prototype.myCall = function (context) {
  var context = context || window
  // 給 context 添加一個屬性
  // getValue.call(a, 'yck', '24') => a.fn = getValue
  context.fn = this
  // 將 context 後面的參數取出來
  var args = [...arguments].slice(1)
  // getValue.call(a, 'yck', '24') => a.fn('yck', '24')
  var result = context.fn(...args)
  // 刪除 fn
  delete context.fn
  return result
}

以上就是 call 的思路,apply 的實現也類似

Function.prototype.myApply = function (context) {
  var context = context || window
  context.fn = this

  var result
  // 需要判斷是否存儲第二個參數
  // 如果存在,就將第二個參數展開
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }

  delete context.fn
  return result
}

bind 和其他兩個方法作用也是一致的,只是該方法會返回一個函數。並且我們可以通過 bind 實現柯里化。

同樣的,也來模擬實現下 bind

Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  var _this = this
  var args = [...arguments].slice(1)
  // 返回一個函數
  return function F() {
    // 因為返回了一個函數,我們可以 new F(),所以需要判斷
    if (this instanceof F) {
      return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat(...arguments))
  }
}

Promise 實現

Promise 是 ES6 新增的語法,解決了回調地獄的問題。

可以把 Promise 看成一個狀態機。初始是 pending 狀態,可以通過函數 resolvereject ,將狀態轉變為 resolved 或者 rejected 狀態,狀態一旦改變就不能再次變化。

then 函數會返回一個 Promise 實例,並且該返回值是一個新的實例而不是之前的實例。因為 Promise 規範規定除了 pending 狀態,其他狀態是不可以改變的,如果返回的是一個相同實例的話,多個 then 調用就失去意義了。

對於 then 來說,本質上可以把它看成是 flatMap

// 三種狀態
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一個函數參數,該函數會立即執行
function MyPromise(fn) {
  let _this = this;
  _this.currentState = PENDING;
  _this.value = undefined;
  // 用於保存 then 中的回調,只有當 promise
  // 狀態為 pending 時才會緩存,並且每個實例至多緩存一個
  _this.resolvedCallbacks = [];
  _this.rejectedCallbacks = [];

  _this.resolve = function (value) {
    if (value instanceof MyPromise) {
      // 如果 value 是個 Promise,遞歸執行
      return value.then(_this.resolve, _this.reject)
    }
    setTimeout(() => { // 非同步執行,保證執行順序
      if (_this.currentState === PENDING) {
        _this.currentState = RESOLVED;
        _this.value = value;
        _this.resolvedCallbacks.forEach(cb => cb());
      }
    })
  };

  _this.reject = function (reason) {
    setTimeout(() => { // 非同步執行,保證執行順序
      if (_this.currentState === PENDING) {
        _this.currentState = REJECTED;
        _this.value = reason;
        _this.rejectedCallbacks.forEach(cb => cb());
      }
    })
  }
  // 用於解決以下問題
  // new Promise(() => throw Error('error))
  try {
    fn(_this.resolve, _this.reject);
  } catch (e) {
    _this.reject(e);
  }
}

MyPromise.prototype.then = function (onResolved, onRejected) {
  var self = this;
  // 規範 2.2.7,then 必須返回一個新的 promise
  var promise2;
  // 規範 2.2.onResolved 和 onRejected 都為可選參數
  // 如果類型不是函數需要忽略,同時也實現了透傳
  // Promise.resolve(4).then().then((value) => console.log(value))
  onResolved = typeof onResolved === 'function' ? onResolved : v => v;
  onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

  if (self.currentState === RESOLVED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      // 規範 2.2.4,保證 onFulfilled,onRjected 非同步執行
      // 所以用了 setTimeout 包裹下
      setTimeout(function () {
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === REJECTED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      setTimeout(function () {
        // 非同步執行onRejected
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === PENDING) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      self.resolvedCallbacks.push(function () {
        // 考慮到可能會有報錯,所以使用 try/catch 包裹
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });

      self.rejectedCallbacks.push(function () {
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });
    }));
  }
};
// 規範 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
  // 規範 2.3.1,x 不能和 promise2 相同,避免迴圈引用
  if (promise2 === x) {
    return reject(new TypeError("Error"));
  }
  // 規範 2.3.2
  // 如果 x 為 Promise,狀態為 pending 需要繼續等待否則執行
  if (x instanceof MyPromise) {
    if (x.currentState === PENDING) {
      x.then(function (value) {
        // 再次調用該函數是為了確認 x resolve 的
        // 參數是什麼類型,如果是基本類型就再次 resolve
        // 把值傳給下個 then
        resolutionProcedure(promise2, value, resolve, reject);
      }, reject);
    } else {
      x.then(resolve, reject);
    }
    return;
  }
  // 規範 2.3.3.3.3
  // reject 或者 resolve 其中一個執行過得話,忽略其他的
  let called = false;
  // 規範 2.3.3,判斷 x 是否為對象或者函數
  if (x !== null && (typeof x === "object" || typeof x === "function")) {
    // 規範 2.3.3.2,如果不能取出 then,就 reject
    try {
      // 規範 2.3.3.1
      let then = x.then;
      // 如果 then 是函數,調用 x.then
      if (typeof then === "function") {
        // 規範 2.3.3.3
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            // 規範 2.3.3.3.1
            resolutionProcedure(promise2, y, resolve, reject);
          },
          e => {
            if (called) return;
            called = true;
            reject(e);
          }
        );
      } else {
        // 規範 2.3.3.4
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 規範 2.3.4,x 為基本類型
    resolve(x);
  }
}

以上就是根據 Promise / A+ 規範來實現的代碼,可以通過 promises-aplus-tests 的完整測試

image

Generator 實現

Generator 是 ES6 中新增的語法,和 Promise 一樣,都可以用來非同步編程

// 使用 * 表示這是一個 Generator 函數
// 內部可以通過 yield 暫停代碼
// 通過調用 next 恢復執行
function* test() {
  let a = 1 + 2;
  yield 2;
  yield 3;
}
let b = test();
console.log(b.next()); // >  { value: 2, done: false }
console.log(b.next()); // >  { value: 3, done: false }
console.log(b.next()); // >  { value: undefined, done: true }

從以上代碼可以發現,加上 * 的函數執行後擁有了 next 函數,也就是說函數執行後返回了一個對象。每次調用 next 函數可以繼續執行被暫停的代碼。以下是 Generator 函數的簡單實現

// cb 也就是編譯過的 test 函數
function generator(cb) {
  return (function() {
    var object = {
      next: 0,
      stop: function() {}
    };

    return {
      next: function() {
        var ret = cb(object);
        if (ret === undefined) return { value: undefined, done: true };
        return {
          value: ret,
          done: false
        };
      }
    };
  })();
}
// 如果你使用 babel 編譯後可以發現 test 函數變成了這樣
function test() {
  var a;
  return generator(function(_context) {
    while (1) {
      switch ((_context.prev = _context.next)) {
        // 可以發現通過 yield 將代碼分割成幾塊
        // 每次執行 next 函數就執行一塊代碼
        // 並且表明下次需要執行哪塊代碼
        case 0:
          a = 1 + 2;
          _context.next = 4;
          return 2;
        case 4:
          _context.next = 6;
          return 3;
        // 執行完畢
        case 6:
        case "end":
          return _context.stop();
      }
    }
  });
}

Map、FlatMap 和 Reduce

Map 作用是生成一個新數組,遍歷原數組,將每個元素拿出來做一些變換然後 append 到新的數組中。

[1, 2, 3].map((v) => v + 1)
// -> [2, 3, 4]

Map 有三個參數,分別是當前索引元素,索引,原數組

['1','2','3'].map(parseInt)
//  parseInt('1', 0) -> 1
//  parseInt('2', 1) -> NaN
//  parseInt('3', 2) -> NaN

FlatMapmap 的作用幾乎是相同的,但是對於多維數組來說,會將原數組降維。可以將 FlatMap 看成是 map + flatten ,目前該函數在瀏覽器中還不支持。

[1, [2], 3].flatMap((v) => v + 1)
// -> [2, 3, 4]

如果想將一個多維數組徹底的降維,可以這樣實現

const flattenDeep = (arr) => Array.isArray(arr)
  ? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , [])
  : [arr]

flattenDeep([1, [[2], [3, [4]], 5]])

Reduce 作用是數組中的值組合起來,最終得到一個值

function a() {
    console.log(1);
}

function b() {
    console.log(2);
}

[a, b].reduce((a, b) => a(b()))
// -> 2 1

async 和 await

一個函數如果加上 async ,那麼該函數就會返回一個 Promise

async function test() {
  return "1";
}
console.log(test()); // -> Promise {<resolved>: "1"}

可以把 async 看成將函數返回值使用 Promise.resolve() 包裹了下。

await 只能在 async 函數中使用

function sleep() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('finish')
      resolve("sleep");
    }, 2000);
  });
}
async function test() {
  let value = await sleep();
  console.log("object");
}
test()

上面代碼會先列印 finish 然後再列印 object 。因為 await 會等待 sleep 函數 resolve ,所以即使後面是同步代碼,也不會先去執行同步代碼再來執行非同步代碼。

async 和 await 相比直接使用 Promise 來說,優勢在於處理 then 的調用鏈,能夠更清晰準確的寫出代碼。缺點在於濫用 await 可能會導致性能問題,因為 await 會阻塞代碼,也許之後的非同步代碼並不依賴於前者,但仍然需要等待前者完成,導致代碼失去了併發性。

下麵來看一個使用 await 的代碼。

var a = 0
var b = async () => {
  a = a + await 10
  console.log('2', a) // -> '2' 10
  a = (await 10) + a
  console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1

對於以上代碼你可能會有疑惑,這裡說明下原理

  • 首先函數 b 先執行,在執行到 await 10 之前變數 a 還是 0,因為在 await 內部實現了 generatorsgenerators 會保留堆棧中東西,所以這時候 a = 0 被保存了下來
  • 因為 await 是非同步操作,遇到await就會立即返回一個pending狀態的Promise對象,暫時返回執行代碼的控制權,使得函數外的代碼得以繼續執行,所以會先執行 console.log('1', a)
  • 這時候同步代碼執行完畢,開始執行非同步代碼,將保存下來的值拿出來使用,這時候 a = 10
  • 然後後面就是常規執行代碼了

Proxy

Proxy 是 ES6 中新增的功能,可以用來自定義對象中的操作

let p = new Proxy(target, handler);
// `target` 代表需要添加代理的對象
// `handler` 用來自定義對象中的操作

可以很方便的使用 Proxy 來實現一個數據綁定和監聽

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      setBind(value);
      return Reflect.set(target, property, value);
    }
  };
  return new Proxy(obj, handler);
};

let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
  value = v
}, (target, property) => {
  console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2

為什麼 0.1 + 0.2 != 0.3

因為 JS 採用 IEEE 754 雙精度版本(64位),並且只要採用 IEEE 754 的語言都有該問題。

我們都知道電腦表示十進位是採用二進位表示的,所以 0.1 在二進位表示為

// (0011) 表示迴圈
0.1 = 2^-4 * 1.10011(0011)

那麼如何得到這個二進位的呢,我們可以來演算下

image

小數算二進位和整數不同。乘法計算時,只計算小數位,整數位用作每一位的二進位,並且得到的第一位為最高位。所以我們得出 0.1 = 2^-4 * 1.10011(0011),那麼 0.2 的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)

回來繼續說 IEEE 754 雙精度。六十四位中符號位占一位,整數位占十一位,其餘五十二位都為小數位。因為 0.10.2 都是無限迴圈的二進位了,所以在小數位末尾處需要判斷是否進位(就和十進位的四捨五入一樣)。

所以 2^-4 * 1.10011...001 進位後就變成了 2^-4 * 1.10011(0011 * 12次)010 。那麼把這兩個二進位加起來會得出 2^-2 * 1.0011(0011 * 11次)0100 , 這個值算成十進位就是 0.30000000000000004

下麵說一下原生解決辦法,如下代碼所示

parseFloat((0.1 + 0.2).toFixed(10))

正則表達式

元字元

元字元 作用
. 匹配任意字元除了換行符和回車符
[] 匹配方括弧內的任意字元。比如 [0-9] 就可以用來匹配任意數字
^ ^9,這樣使用代表匹配以 9 開頭。[^9],這樣使用代表不匹配方括弧內除了 9 的字元
{1, 2} 匹配 1 到 2 位字元
(yck) 只匹配和 yck 相同字元串
| 匹配 | 前後任意字元
\ 轉義
* 只匹配出現 0 次及以上 * 前的字元
+ 只匹配出現 1 次及以上 + 前的字元
? ? 之前字元可選

修飾語

修飾語 作用
i 忽略大小寫
g 全局搜索
m 多行

字元簡寫

簡寫 作用
\w 匹配字母數字或下劃線
\W 和上面相反
\s 匹配任意的空白符
\S 和上面相反
\d 匹配數字
\D 和上面相反
\b 匹配單詞的開始或結束
\B 和上面相反

V8 下的垃圾回收機制

V8 實現了準確式 GC,GC 演算法採用了分代式垃圾回收機制。因此,V8 將記憶體(堆)分為新生代和老生代兩部分。

新生代演算法

新生代中的對象一般存活時間較短,使用 Scavenge GC 演算法。

在新生代空間中,記憶體空間分為兩部分,分別為 From 空間和 To 空間。在這兩個空間中,必定有一個空間是使用的,另一個空間是空閑的。新分配的對象會被放入 From 空間中,當 From 空間被占滿時,新生代 GC 就會啟動了。演算法會檢查 From 空間中存活的對象並複製到 To 空間中,如果有失活的對象就會銷毀。當複製完成後將 From 空間和 To 空間互換,這樣 GC 就結束了。

老生代演算法

老生代中的對象一般存活時間較長且數量也多,使用了兩個演算法,分別是標記清除演算法和標記壓縮演算法。

在講演算法前,先來說下什麼情況下對象會出現在老生代空間中:

  • 新生代中的對象是否已經經歷過一次 Scavenge 演算法,如果經歷過的話,會將對象從新生代空間移到老生代空間中。
  • To 空間的對象占比大小超過 25 %。在這種情況下,為了不影響到記憶體分配,會將對象從新生代空間移到老生代空間中。

老生代中的空間很複雜,有如下幾個空間

enum AllocationSpace {
  // TODO(v8:7464): Actually map this space's memory as read-only.
  RO_SPACE,    // 不變的對象空間
  NEW_SPACE,   // 新生代用於 GC 複製演算法的空間
  OLD_SPACE,   // 老生代常駐對象空間
  CODE_SPACE,  // 老生代代碼對象空間
  MAP_SPACE,   // 老生代 map 對象
  LO_SPACE,    // 老生代大空間對象
  NEW_LO_SPACE,  // 新生代大空間對象

  FIRST_SPACE = RO_SPACE,
  LAST_SPACE = NEW_LO_SPACE,
  FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
  LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};

在老生代中,以下情況會先啟動標記清除演算法:

  • 某一個空間沒有分塊的時候
  • 空間中被對象超過一定限制
  • 空間不能保證新生代中的對象移動到老生代中

在這個階段中,會遍歷堆中所有的對象,然後標記活的對象,在標記完成後,銷毀所有沒有被標記的對象。在標記大型對記憶體時,可能需要幾百毫秒才能完成一次標記。這就會導致一些性能上的問題。為瞭解決這個問題,2011 年,V8 從 stop-the-world 標記切換到增量標誌。在增量標記期間,GC 將標記工作分解為更小的模塊,可以讓 JS 應用邏輯在模塊間隙執行一會,從而不至於讓應用出現停頓情況。但在 2018 年,GC 技術又有了一個重大突破,這項技術名為併發標記。該技術可以讓 GC 掃描和標記對象時,同時允許 JS 運行,你可以點擊 該博客 詳細閱讀。

清除對象後會造成堆記憶體出現碎片的情況,當碎片超過一定限制後會啟動壓縮演算法。在壓縮過程中,將活的對象像一端移動,直到所有對象都移動完成然後清理掉不需要的記憶體。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • In August 2015, Google announced the release of Android Mobile Vision API. At that time this API had mainly three components Face Detection, Barcode s... ...
  • 一、修改點擊的動畫 函數: 這是預設的點擊的動畫 我們用代碼修改一下: 這是之後的效果: 二、設置下劃線指示器的寬度不要填充完(動態的根據TabView的寬度來設置自身的寬度) 函數: 這是預設的效果 我們用代碼修改一下: 這是之後的效果: 三、設置下劃線指示器的樣式 函數: 這是預設的效果 我們用 ...
  • "覓知音"這個APP的第一個版本從提交審核到上架,歷時三個星期,其中遇到一些審核上的問題,它的處理或許能幫助到遇到同樣問題的小伙伴們,所以這裡列舉出來,這三個星期如何跟蘋果的審核團隊“鬥智鬥勇”。 ...
  • 力有不逮的對象 眾所周知,在 中,直接修改對象屬性的值無法觸發響應式。當你直接修改了對象屬性的值,你會發現,只有數據改了,但是頁面內容並沒有改變。 這是什麼原因? 原因在於: 的響應式系統是基於 這個方法的,該方法可以監聽對象中某個元素的獲取或修改,經過了該方法處理的數據,我們稱其為響應式數據。但是 ...
  • 聲明 本系列文章內容全部梳理自以下幾個來源: 《JavaScript權威指南》 "MDN web docs" "Github:smyhvae/web" "Github:goddyZhao/Translation/JavaScript" 作為一個前端小白,入門跟著這幾個來源學習,感謝作者的分享,在其基 ...
  • 方法一:footer高度固定+絕對定位 HTML代碼: CSS代碼: 實現的效果: 首先,設置body的高度至少充滿整個屏幕,並且body作為footer絕對定位的參考節點; 其次,設置main(footer前一個兄弟元素)的padding-bottom值大於等於footer的height值,以保證 ...
  • 學習Vue的一些總結,第一次寫博客,文筆實在是很差 不過我會不斷寫的。 這裡只寫了一部分常用的vue的指令,後面還會有的。 ...
  • 12.3事件對象 1.每個元素身上的事件都是天生自帶的,不需要我們去定義,只需要我們給這個事件綁定一個方法, 2.事件綁定的寫法 1.div.onclick=function(){} DOM 0級事件綁定 2.div.addEventListener()或div.attachEvent() DOM2 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...