GithubHelp home page GithubHelp logo

blog's Introduction

Hi there 👋

leowangJ

blog's People

Contributors

leowangj avatar

Stargazers

 avatar

Watchers

 avatar  avatar

blog's Issues

費波那契數列實作及優化

費波那契數列實作

先看一下維基百科的費波那契解釋:

F0 = 0
F1 = 1
Fn = F(n-1) +F(n-2)(n≧2)
用文字來說,就是費氏數列由0和1開始,之後的斐波那契系數就是由之前的兩數相加而得出。
ex: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233.......
0代表第0項
簡單來說就是第一項與第二項相加就為第三項的值。
讓我們來看程式是怎麼寫的:

let fib = (n) => 
  n === 0 ? 0 : 
  n === 1 ? 1  :
  fib(n-1) + fib(n-2)

fib(4) // 3 

上面的例子意思是說, 假如n是0則返回0,若是1則返回1,若都不是則返回下兩項的相加。
但是使用遞迴有一個問題就是當傳入的參數越大則計算時間會變非常的久。

console.time('時間1')
fib(40)  // 花了2230.783935546875ms
console.timeEnd('時間1')

console.time('時間2')
fib(45) //花了25279.218017578125ms
console.timeEnd('時間2')

fib(40與fib(45)的計算足足差了23秒,這是因為遞迴雖然裡面有一些函數我們已經計算過了但是電腦沒有那麼聰明,所以還是會重複計算,所以有人發明尾遞迴來優化。

尾遞迴優化

尾遞迴的思路是我們從前面第二項開始計算到要計算的那一個項目,並且將計算完的第一項與第二項函式的參數一起傳到執行的參數中。

fib = (n) => fib_inner(2,n,1,0)
/*
 * start - 起始項目
 * end - 結束項目
 * prev1 - 計算的第一項
 * prev2 - 計算的第二項
 */
fib_inner = (start, end , prev1, prev2) => 
    start === end ? prev1 + prev2 :
           fib_inner(start + 1, end , prev1 + prev2, prev1 )

上面fib_inner 的意思就是當起始項目與最後要計算的項目相等時,就將計算好的第一項與第二項的值相加,否則就繼續執行下一個起始項目並且將下一個起始項目的第一項改成上一個第一項與第二項相加,第二項變成上一個第一項。

用文字敘述可能看不太懂,我們用數字n= 3 帶入進去:

fib(3) // 呼叫
fib = (3) => fib_inner(2,3,1,0) // n = 3

fib_inner = (2, 3 , 1, 0) => 
    2 === 3 ? 1 + 0 :  // 不相等
           fib_inner(2 + 1, 3 , 1 + 0, 1 ) //  此時fib(2+1)的prev1 已經變成 1 + 0, prev2 變成 1

又在執行  fib_inner(2 + 1, 3 , 1 + 0, 1 )

 fib_inner(2 + 1, 3 , 1 + 0, 1 ) => 
       ( 2 +1) === 3 ?  (1 + 0) + 1 :  // 相等
           fib_inner(3 + 1, 3 , 1 + 1, 1 ) // 不執行

將數值帶入進去後應該就會明白他的意思是什麼了。
那麼真的有比較快嗎?

console.time('時間2')
fib(45) // 0.07421875ms
console.timeEnd('時間2')

比我們剛剛用遞迴快了超級多!

modules自動導入(require.context)

modules自動導入

一般我們在導入vuex的module時是使用import的方式一個一個去手動導入,但當每次新增一個module我們就必須記得去import,對於工程的角度來說能夠自動導入的話是最方便的,也不會忘記去導入。

手動添加module:

//moduleA.js
const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

//moduleB.js
const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

// index.js
import moduleA from './moduleA'
import moduleB from './moduleB'

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

require.context

所幸webpack中這個方法能夠讓我們自動導入檔案

require.context接受三個參數

  1. directory(string) - 要搜索的目錄
  2. useSubdirectories(boolean) - 是否要搜索子目錄
  3. regExp - 匹配文件的正則

usage:

//將遍歷當前目錄下的modules文件夾下的所有js檔案,包含子目錄
const modulesFiles = require.context('./modules', true, /\.js$/)

將modulesFiles打印出來後會發現requrie.context返回的是一個函數,這函數可以接受一個參數request且該函數包含了三個屬性resolve、keys、id,而此地方只會用到keys。

  1. keys - 返回匹配成功的模組名稱所組成的陣列
// index.js
const modulesFiles = require.context('./modules', true, /\.js$/)

// 將匹配成功的模組名稱陣列組合成我們想要的物件
// modulesFiles.keys() =>  ["./moduleA.js","./moduleB.js"]
const API = modulesFiles.keys().reduce((modules, modulePath) => {
  // 將模組路徑名稱正則成想要的名稱
  // ex: ./moduleA.js => moduleA
  const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
  // 取得模組資料
  const value = modulesFiles(modulePath)
  modules[moduleName] = value.default
  return modules
}, {})
export default API

此時index.js 格式將會是

{
  moduleA:{
    state: { ... },
    mutations: { ... },
    actions: { ... },
    getters: { ... }
  },
  moduleB:{
    state: { ... },
    mutations: { ... },
    actions: { ... },
    getters: { ... }
  }
}

參考:

vue data 代理

let vm = new Vue({
  el: '#app',
  mounted () {
   console.log(this)
   console.log(this.message)
  },
 data () {
  return {
   message: 'hello'
  }
 }
})

為什麼我們呼叫this.message時,可以直接取得data中的message資料呢? 而console.log(this) 卻又找不到這個message屬性? Vue到底在這個地方做了什麼處理?

vue的資料都是儲存在this._data中,而為了使用者方便取用,且避免訪問私有變數,Vue在初始化data時有替data中每個key做getter與setter,所以當我們在調用this.message時,其實getter就觸發並返回this._data.message,導致我們能夠取得到值
以下是源碼:

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
// vm是Vue元件 , _data是要代理的路徑, key是要代理的變數
proxy(vm, `_data`, key)

node mysql sequlize 出現 Client does not support authentication protocol requested by server; consider upgrading MySQL client

google後發現是mysql版本密碼算法不同,導致會出現這個錯誤!
解決方法:
進入mysql cmd
/usr/local/mysql/bin/mysql -u root -p

輸入mysql password

修改mysql密碼

USE mysql;
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密碼';
FLUSH PRIVILEGES;

修改完成後再重新開啟node服務器連接mysql 就成功了

基本類型怎麼取的toString方法

先看一個例子:

let n = 1
n.toString() // 印出 "1"

直接印出n的值時也沒有看到有任何的prototype可以使用,那為什麼n可以使用到toString方法呢?
而且n是一個基本類型怎麼能夠使用物件的方法呢?

首先我們要先說明let n = new Number(1)與 let n = 1 的差別。

使用new Number 會創建一個物件,而這個物件會擁有Number的prototype且Number的prototype又指向Object的prototype,所以使得我們可以同時使用Number與Object的prototype。

let n = new Number(1)

n -----> Number.prototype -----> Object.prototype

而使用let n = 1 時,他並不會生成一個物件,所以JS作者就用了一個方法解決這個問題。
就是當基本類型要使用prototype的東西時,會去new一個對應類型的物件,這個物件會提供n取得protoype的方法,等到取用結束後這個物件就會釋放掉避免佔太多記憶體。

再看一個例子:

let n = 1
n.xxx = 2
console.log(n.xxx) // undefined

由於上面我們已經說過當要對基本類型進行物件的操作時,會先創建一個暫時的物件且當操作結束後該物件就會被釋放掉,所以要再次調用n.xxx時就會是undefined。

淺拷貝與深拷貝

淺拷貝: 僅能拷貝物件中第一層的資料,第二層以後的資料一樣會與舊的物件共用相同參考(reference)。

深拷貝:複製一個物件,使得兩個物件的參考完全沒有相關(不共用相同記憶體)。

  1. Object.assign : 屬於淺拷貝
var obj = { 
    name:'leo',
    child:{
         name:'jack'
    }
}

var obj2 = Object.assign({},obj)
console.log(obj2) // 與obj值相同,代表成功複製

obj2.name = 'change' 
console.log(obj.name)  // leo,obj值未被修改

obj2.child.name = 'fake' 
console.log(obj.child.name) // fake,修改obj2的值但obj的值也一起被修改了 
  1. 使用展開運算子複製物件 : 屬於淺拷貝
var obj = { 
    name:'leo',
    child:{
         name:'jack'
    }
}

var obj2 = {...obj} // 展開運算子複製物件
console.log(obj2) // 與obj值相同,代表成功複製

obj2.name = 'change' 
console.log(obj.name)  // leo,obj值未被修改

obj2.child.name = 'fake' 
console.log(obj.child.name) // fake,修改obj2的值但obj的值也一起被修改了 
  1. 使用JSON.parse(JSON.stringify())複製 :屬於深拷貝
var obj = { 
    name:'leo',
    child:{
         name:'jack'
    }
}

var obj2 =JSON.parse(JSON.stringify(obj))// JSON.parse與JSON.stringify進行複製
console.log(obj2) // 與obj值相同,代表成功複製

obj2.name = 'change' 
console.log(obj.name)  // leo,obj值未被修改

obj2.child.name = 'fake' 
console.log(obj.child.name) // jack,obj值未被修改,是我們想要的效果

但這種方式進行深拷貝的話,有些資料是無法複製的ex: function,Date,正則 ...等
原因是JSON格式沒有提供上述方式,所以在轉換時會被忽略,導致無法複製。
那麼要怎麼解決無法複製的那些值呢?
可以使用lodash提供的_. cloneDeep或者自行實作(敬請期待)

柯里化(curry)

把支持多個參數的函式轉成只支持單一參數的函式形式,大多應用在函數式編程。

假設我們目前有一個相加函式

let add = (a,b) => a + b
add(1,2) // return 3

使用curry方式來表達的話則是

let add = a => b => a + b
add(1)(2)

使用閉包來記錄第一個參數使得執行返回的函數時能夠調用第一個參數來相加。

那假如我們希望使用一個函式能夠將多個參數形式的函式轉變成curry函式時該怎麼做呢?

let currify = (fn,params = []){
    let newParams = params.conact(arg)
    return (arg) = >{
       if(newParams.length === fn.length){
           return fn(...newParams)
       } else {
           return currify(fn, newParams)
       }
    }
}

addTwo = (a,b)=>a+b
addThree = (a,b,c)=>a+b+c

let newAddTwo = currify(addTwo)
newAddTwo(1)(2) // return 3

let newAddThree = currify(addThree)
newAddThree(1)(2)(3) // return 6

上面currify的思路是

  1. 若當前參數數量為傳入函式的形參則執行該函式
  2. 若不是則代表還沒到最後一個參數,所以繼續執行currify
  3. newParams則代表有執行過的參數

vue 元素為何不能掛載在html與body上?

<body>
   <div id="app">
   </div>
</body>

let vm = new Vue({
  el: '#app',
  template: '<p>hello</p>'
})

通常我們在掛載Vue元素時會這樣寫,但為什麼我們不能將el掛載在body或html元素上呢?
原因是你的template裡面的html會替換掉

這一個元素,所以假設你在body上掛載元件時,會使得body被替換掉了,雖然省略body tag在html5是允許的,但省略可能會導致各家瀏覽器出現不明確的風險,所以還是盡量避免。

而且在vue源碼中也有做相關判斷,避免將el掛載在html與body上

if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

Vue中的數據data

為什麼使用new 創建一個vue實例時data可以直接使用物件來定義,而在組件中的data卻必須使用function來return 一個物件呢?

原因是物件是引用類型,每個組件中的data都是共用相同內存,所以一個數值改變時,其他組件就會跟著變。

先看一下下面的範例

var component = function(){}
component.prototype.data = {
  a:1,
  b:2
}

var test1 = new component()
var test2 = new component()

test1.prototype.data == test2.prototype.data // true
test1.prototype.data = 10;
console.log(test2.prototype.data) // 10

上述data就是類似我們傳入vue組件的data,所以當創建組件時你傳入是物件時,會將每個組件的data指向同一個地址。
若使用function return 一個物件時,則會造成作用域,來確保每個組建都用自己的物件,而不會共用。

參考

package-lock.json

由於模組可能在你npm install 之後有在更新過版本,這樣可能會導致你的專案在每個時期安裝到的模組版本都是不同的,所以package-lock.json是為了解決這個問題,確保你之後clone或deploy專案時,模組版本是固定的(package-lock.json會紀錄)

CommonJS、AMD、UMD 差別

CommonJS : 同步加載模塊。由於在服務器端中,模塊載入的速度取決於硬碟速度,所以以同步來加載模塊的話並沒什麼大礙。

AMD: 非同步加載模塊。AMD的出現主要是要解決在瀏覽器上載入模塊的問題,因為假如是同步的話,會使得畫面卡死,直到加載完成。

UMD: 主要是解決跨平台問題。 會判斷目前環境是否支持CommonJS或AMD,而去使用其中一種方法

手寫eventEmitter

eventBus又稱eventHub,中文意思為訂閱發布模式

 class eventEmitter {
     private cache = {}
     on (eventName, fn) {
         this.cache[eventName] = this.cache[eventName] ||  []
         this.cache[eventName].push(fn)
     }
     emit (eventName, data) {
         let array = this.cache[eventName] || []
         array.forEach(fn=>{
            fn(data)
         })
     }
     off(eventName, fn){
           this.cache[eventName] = this.cache[eventName] || []
           let index = this.cache[eventName].indexOf(fn)
           if(index === -1) return 
           this.cache[eventName].splice(index, 1)
     }
 } 

let event = new eventEmitter()
let fn1 =(data) => console.log(data)

event.on('test', fn1)  // 訂閱
event.emit('test','你好')  // 發布

event.off('test', fn1) // 取消訂閱
event.emit('test','你好') 

主要思路是

  1. on方法將傳入的方法名稱與執行函式存進cache物件中,而執行函式使用陣列來儲存, 方便調用不同的訂閱要執行的方法。
  2. emit方法則是執行所有訂閱該方法名稱的函式。
  3. off方法則是取消該項方法名稱中要取消的函數(訂閱的函式與取消的函式不能使用匿名函式,否則不會知道要取消哪個方法)。

七種水平垂直置中的css排版

兩個div元素,parent代表父層,child代表子層

<div id="parent">
  <div id="child">
  </div>
</div>

預設的css

body{
  height:300px;
}
#child{
  background-color:#111;
  width:50px;
  height:50px;
  
}
#parent{
  height:100%;
  background-color:#d5d7de;
}
  1. flex水平垂直置中
#parent{
   display:flex;
   justify-content:center;
  align-items:center;
  1. 已知父層寬高的情況下,父層設relative,子層設置absolute/fixed,並且將top,left設成50%
    位子已經取得到中心點,但中心點的位子是子層的左上角。
    所以必須移動子層寬高的一半,才會是置中。使用margin-top/left去調整位子。
    ex:子層寬高各為50px; 所以需要將元素移動25px
#parent{
 position:relative;
 width:300px;
}

#child{
 position:absolute;
 top:50%;
 left:50%;
 margin-top:-25px;
 margin-left:-25px;
}

節流(throttle) 實現

節流

持續觸發某個任務, 每隔一段時間觸發一次。

簡單來說 就是多久執行一次

立即執行

開始執行時會先呼叫一次, 接下來依照時間戳來判斷是否重新觸發。

 throttle (fn, wait) {
    var prev = 0
    return function () {
      var now = +new Date()
      if (now - prev > wait) {
        fn.apply(this, arguments)
        prev = now
      }
    }
  }

非立即執行

經過設定的時間數後才會執行,我們透過setTimeout來判斷隔了多久觸發一次。

throttle (fn, wait) {
    var timeout
    return function () {
      var args = arguments
      if (timeout) {
        return false
      }

      timeout = setTimeout(() => {
        fn.apply(this, args)
        timeout = null
      }, wait)
    }
  }

立即執行與非立即執行區別

立即執行: 執行任務時會先執行一次,但當事件停止觸發時任務就會立即停止。
例如: 在輸入註冊帳號時,設置每秒執行一次驗證註冊帳號是否符合資格,若在第2.5秒停止輸入這時只會執行兩次驗證。

非立即執行: 執行任務時會先等到指定時間到才會執行,但當事件停止觸發時依舊還會執行最後一次事件。
意思為2.5秒停止輸入等到第三秒時還會再執行一次驗證

整合兩種方式的優點

我們想要實現出可以立即執行並且在停止輸入時還會觸發最後一次事件的方法

throttle (fn, wait) {
    var timeout, remaining, context, args
    var prev = 0
    // 執行最後一次事件
    var later = function () {
      prev = +new Date()
      timeout = null
      fn.apply(context, args)
      console.log('最後執行')
    }

    var throttled = function () {
      context = this
      args = arguments
      var now = +new Date()
      // 下次觸發任務(fn)的剩餘時間
      remaining = wait - (now - prev)
      // 剩餘時間小於0,代表可以觸發事件
      if (remaining <= 0) {
        // 確保下次還會進入setTimeout
        if (timeout) {
          clearTimeout(timeout)
          timeout = null
        }
        prev = now
        fn.apply(context, args)
      } else if (!timeout) {
        timeout = setTimeout(later, remaining)
      }
    }

    return throttled
  }

優化

有時候並不想要上面的版本, 只想要原先的非立即執行的版本, 為了兼容這個問題我們想到了使用參數來控制想要使用哪種版本。
我們使用options作為第三個參數,而options中有兩個key:

  1. leading:false 表示禁用第一次執行
  2. trailing: false 表示禁用停止觸發的回調
throttle (fn, wait, options) {
    var timeout, remaining, context, args
    var prev = 0

    if (!options) options = {}
    // 執行最後一次事件
    var later = function () {
      prev = options.leading === false ? 0 : +new Date()
      timeout = null
      fn.apply(context, args)
      console.log('最後執行')
    }

    var throttled = function () {
      context = this
      args = arguments
      var now = +new Date()
      if (!prev && options.leading === false) prev = now
      // 下次觸發任務(fn)的剩餘時間
      remaining = wait - (now - prev)
      // 剩餘時間小於0,代表可以觸發事件
      if (remaining <= 0) {
        // 確保下次還會進入setTimeout
        if (timeout) {
          clearTimeout(timeout)
          timeout = null
        }
        prev = now
        fn.apply(context, args)
      } else if (!timeout && options.trailing !== false) {
        timeout = setTimeout(later, remaining)
      }
    }

    return throttled
  }

回收變量

由於我們在閉包中儲存了許多變量都沒有回收, 所以在結束這個事件時順便回收閉包變量

throttle (fn, wait, options) {
    var timeout, remaining, context, args
    var prev = 0

    if (!options) options = {}
    // 執行最後一次事件
    var later = function () {
      prev = options.leading === false ? 0 : +new Date()
      timeout = null
      fn.apply(context, args)
      if (!timeout) context = args = null
      console.log('最後執行')
    }

    var throttled = function () {
      context = this
      args = arguments
      var now = +new Date()
      if (!prev && options.leading === false) prev = now
      // 下次觸發任務(fn)的剩餘時間
      remaining = wait - (now - prev)
      // 剩餘時間小於0,代表可以觸發事件
      if (remaining <= 0) {
        // 確保下次還會進入setTimeout
        if (timeout) {
          clearTimeout(timeout)
          timeout = null
        }
        prev = now
        fn.apply(context, args)
        if (!timeout) context = args = null
      } else if (!timeout && options.trailing !== false) {
        timeout = setTimeout(later, remaining)
      }
    }

    return throttled
  }

參考

从搜索系统来聊聊防抖和节流
JavaScript专题之跟着underscore学防抖

函數劫持

函數劫持意思是在要調用某個函數時,額外的添加一些我們想要執行的功能後才會執行調用的函數,

用意是你依舊可以使用原先函數的功能,並且在此功能上再添加一些想要做的事情。

函數劫持的原理:

  1. 使用變量先保存原先的函式

  2. 重新複寫要劫持的函式

  3. 在覆寫的函式內重新呼叫使用變量儲存的原先函式

例子:

var test = function() { console.log('原本的函式')}

var temp = test;
test = function() {
  console.log('添加的判斷')
  temp()
}

test()

通過上面例子,我們會打印出兩個console.log,證明了我們會執行原有的函式,以及後來添加的方法

HTTP header

HTTP header

使用HTTP header主要是給瀏覽器與服務器提供報文主體大小、所使用的語言、驗證訊息等。

4種HTTP header 類型

  1. 通用首部字段(general header fields)
  2. 請求首部字段(request header fields)
  3. 回應首部字段(response header fields)
  4. 實體首部字段(entity header fields)

通用首部字段(general header fields)

指的是請求與回應的報文雙方都會使用的首部

  1. Cache-ontrol - 操作緩存的工作機制
  2. Connection - 管理連接
  3. Date - 創建報文的日期與時間

請求首部字段(request header fields)

從客戶端項服務端發送請求報文時使用的首部。補充了請求的附加內容、客戶端訊息

  1. Accept 用來告知服務端, 客戶端可以處理的內容類型

待完成...

new 操作符

上一篇介紹構造函數時有提到必須使用new 來呼叫,那麼為什麼需要new呢? new 到底做了什麼事情?

new 的四件事情

new是一種語法糖,它幫我們做了四件事情,讓我們先看代碼

let polyfillNew = (constructor,...args)=>{
   let obj = {} 
   Object.setPrototypeOf(obj, constructor.prototype) // 等同於 obj.__proto__ = constructor.prototype
   let result = constructor.call(obj,...args)
   return typeof result === 'object' ? result : obj
}
  1. 創建一個物件
  2. 將構造函數的原型指定到物件原型
  3. 構造函數中的this指向到新的物件
  4. 返回創建的物件, 若構造函數本身有返回物件類型的話就返回物件, 若不是則返回創建的物件

總結

其實new 所做的事情很簡單,拆解new所做的事情之後就清楚為什麼我們要創建實例時必須使用new操作符,它幫我們處理原型鏈以及this的指向,最後返回我們所需要的實例。

函數與閉包

函數的返回值的影響因素有兩個:

  • 傳入時的參數
  • 定義時環境的env
let a = 1
function add(x){
   return x + a
}
{
   let a = 2
   add(2)  // 是 3 or 4 ?
}

此時add(2)函式的2就是我們傳入的參數,return 的 a 就是由我們在環境中所定義的變數。

那麼答案是多少呢?

我們說影響的因素有兩個,其中有一個是"定義時"的環境env。
這個"定義時"指的是函數在定義時的地方,那麼跟我們函數在相同環境的a為1, 所以答案是3。
假如我們把add函數移至a=2 的區塊環境內,則答案為4。

其實在函數外面我們能夠訪問函數外面的變數,就是我們常見的閉包

閉包就是該函數與在函數外面被訪問的變數

那閉包有什麼用呢? 他能夠幫我們儲存環境變數的值,使得能夠重複使用該參數。

先看一個經典的題目:

for(var i = 0; i < 6; i++){
   setTimeout(()=>console.log(i))
}

答案是什麼?

有一定經驗的前端一定知道setTimeout是非同步的,等到印出console.log(i)時i 已經都增加到6了,而且setTimeout六個函式中的i 都是共用相同的環境變數i,所以此時答案就是6個6。

那要怎麼正常打印出012345呢?

可以使用立即執行函數或者ES6的let。

for(let i = 0; i < 6; i++){
   setTimeout(()=>console.log(i))
}

由於let是區域環境也就是在{ }那就是一個環境,所以我們所宣告的i會在該環境下保持住當前i的值,所以當setTimeout的函數執行時能夠取得到i = 0,1,2,3,4,5的值。

手寫bind

在開發時多少有使用過bind這個方法,但是卻沒有想過他是怎麼實現的,今天就要來研究一下。
先來看mdn 的定義:

bind() 方法,會建立一個新函式。該函式被呼叫時,會將 this 關鍵字設為給定的參數,並在呼叫時,帶有提供之前,給定順序的參數。

polyfill

var slice = Array.prototype.slice
function bind(){
 var that = arguments[0]
 var thatArgs = slice.call(arguments, 1)
 var fn = this
  
  if(typof fn !== function){
    throw new Error('bind 必須調用在函數上')
  }
  
  return function(){
      var funcArgs = slice.call(arguments,0)
      return fn.apply(that, thatArgs.concat(funcArgs))
  }
}

我們來一次拆分函數:

  1. 我們先將Array.slice的方法存到變數slice中,方便之後要處理arguments的值。
  2. 接著將arguments的第一個參數,也就是傳入要修改的this值存到that變數中。
  3. 將剩餘的參數存至thatArgs中,再將當前this存到fn變數中。
  4. 這個fn就是我們bind的函數,所以若bind的不是函數則會拋出錯誤。
  5. 因為bind會返回一個函數,所以我們這邊也會回傳函數。
  6. 函數中使用funcArgs用來儲存返回的函數中所傳的參數。
  7. 接著使用apply將傳入的this以及將全部參數加入綁定到fn函數中。

但是這個bind還有一個問題是當我們使用new時會出現錯誤。

var slice = Array.prototype.slice
function bind(){
 var that = arguments[0]
 var thatArgs = slice.call(arguments, 1)
 var fn = this
  
  if(typof fn !== function){
    throw new Error('bind 必須調用在函數上')
  }
  
  function resultsFn(){
      var funcArgs = slice.call(arguments,0)
      return fn.apply(
       resultsFn.prototype.isPrototypeOf(this) ? this : that, 
       thatArgs.concat(funcArgs)
       )
  }
  resultsFn.prototype = fn.prototype
  return resultsFn
}

上面程式碼就使我們支援new 方法呼叫bind

原型prototype

上一篇物件導向與構造函數(constructor)中提到構造函數,但由於構造函數創造出來的實例中方法與屬性無法共用。所以當如果有一個方法需要公用時,只能重複創建相同方法,這樣很耗費記憶體空間。而原型則是解決無法共用屬性的問題。

原型(prototype)

是儲存公用方法與屬性的地方,只需要創建一次即可使子類使用。

let Car = function(name,color,amount){
    this.name = name
    this.color = color
    this.amount = amount
}

Car.prototype.tax =  function(countryTax){
     return this.amount * countryTax
}

let car1 = new Car('toyota','black',700000)
let car2 = new Car('bmw','black',1400000)
car1.tax(1.2)   // 840000
car2.tax(1.3)  // 1820000
car1.tax === car2.tax // true, 說明方法來自同一個地方

proto 是什麼 ?

物件導向與構造函數(constructor)

物件導向(Object Oriented Programming 簡稱OOP)的概念是將現實生活中的各種複雜的情況,抽象成各個物件,然後由這些物件進行分工與合作來模擬現實生活的情況。

對於物件的描述

  1. 物件是單個現實例子的抽象
  2. 物件是一個容器,封裝了屬性與方法(function)

例如: 一輛車變成物件的樣子

const car = {
    name: 'toyota',
    color: 'black',
    amount: 700000
    tax: function(countryTax) {
      return  amount * countryTax
   }
}

那我們想要造一台黃色的bmw的車子就必須又要重複定義這個結構

 const bmwCar = {
    name: 'bmw',
    color: 'yellow',
    amount: 1400000
    tax: function(countryTax) {
      return  amount * countryTax
   }
}

那有什麼辦法能夠解決重複結構的問題呢?

構造函數(constructor)使我們不需要再重複定義物件,是製造物件的模板

let Car = function(name,color,amount,countryTax){
    this.name = name
    this.color = color
    this.amount = amount
    this.tax = function(){
        return amount * countryTax
    }
}

構造函數與一般函數來說是沒什麼差別的,只是呼叫方式的不同,構造函數必須使用new來調用一個實例

let toyotaCar = new Car('toyota','black',700000,1.2)

JS的falsy值

falsy的意思是在Boolean中可以轉變為false的值,總共有五個。

  1. 0
  2. NaN
  3. null
  4. undefined
  5. ""
console.log(!!0) // false
console.log(!!NaN) //false
console.log(!!null) //false
console.log(!!undefined) //false
console.log(!!"") //false

防抖(debounce) 實現

防抖(debounce)

在功能頻繁觸發的情況下,當只有在功能觸發的時間間隔超過我們所指定的時間,才會觸發此功能

簡單來說就是某個函數在某個指定的時間內不管觸發多少次, 都只會執行最後一次的觸發。

舉例來說: 我們設了一個五秒的指定時間, 則在這五秒內每觸發了一次函式,就會重新計算這五秒, 直到最後一次五秒內都沒有重新觸發函式時就會執行任務。

常用場景:

  1. window的resize,scroll 事件
  2. input的keyup等其他事件
  3. 滑鼠拖曳的mousemove事件

有時候我們只是想知道最後的結果,但上述這些方法在發生變動時會頻繁觸發,因此我們需要使用防抖來節省資源。

例如註冊帳號時,我們需要去檢驗此帳號是否已經被註冊過了,但需求方希望可以在使用者打字時就可以監測,而不是輸入框失去焦點時才判斷。

這時我們可能會這樣寫

$(".register").on("input", function(){
   // 請求後端來判斷是否使用者已經有被註冊,但這個函式不是我們主要探討的部分
  handleRegisterName(this.value)
})

當今天我想要將帳號名稱取為leowang時,會發現我們每次輸入文字都會觸發handleRegisterName這個方法,這樣就多請求許多次不必要的資源。

這時我們不就可以使用防抖來解決這個問題了嗎? 那防抖怎麼寫呢?
你想到了上面的那行 "當只有在功能觸發的時間間隔超過我們所指定的時間,才會觸發此功能"。
這時需要setTimeout來判斷時間是否有超過間隔以及定義兩個參數,分別是要觸發的函式以及指定的時間

 // 防抖函式
 function debounce(fn , wait){
     var timeout
     return function(){
        if(timeout) clearTimeout(timeout)
        timeout = setTimeout(fn, wait)
    }
 }
 
$("#register").on("input", debounce(function(){
  console.log(this.value)
  handleRegisterName(this.value)
}, 300))

修正this問題與傳遞參數

完成後發現了this.value 怎麼是undefined? 檢查一下發現this被指向了window, 這是因為是setTimeout呼叫了這個回呼函式,除了這個問題之外,我們還想要傳遞一些參數進去函式中,例如event。所以我們必須調整一下debounce函式。

透過apply方式綁定上下文情境以及使用arguments取得函數參數。

 function debounce(fn, wait) {
   var timeout, args
   return function(){
      args = arguments
      if(timeout) clearTimeout(timeout)
      timeout = setTimeout(() => {
          fn.apply(this, args)  
      }, wait)
  }
 }

立即執行功能

這時需求方又說希望一輸入文字時,就先執行一次。所以我們就當時間到時將timeout 設成null,使得可以直接執行fn方法

 function debounce(fn, wait, imd) {
   var timeout,args
   return function(){
     args = arguments
     if(timeout) clearTimeout(timeout)
     if(imd) {
       var callNow = !timeout;
       timeout = setTimeout(function(){
         timeout = null;
       }, wait)
       if (callNow) fn.apply(this, args)

     } else {
        timeout = setTimeout(() => {
            fn.apply(this, args)  
        }, wait)
     }
  } 
 }

返回值

但上面還有一個問題, 就是我們的fn假如有回傳值時, 上面那一個版本是無法接收的, 所以我們必須修正這個問題。

但要注意的是, 回傳值版本只提供給立即執行, 因為在非立即執行中fn是在setTimeout內, 導致我們的return的值永遠都是undefined 。

  var debounce = function(fn, wait, immediate){
      var timeout,result,args
      return fuction() {
          args = arguments
          if(timeout) clearTimeout(timeout)
          if(immediate) {
              var callNow = !timeout
              timeout = setTimeout(function(){
                  timeout = null;
              }, wait) 
              if(callNow) result = fn.apply(this, args)
          } else {
              timeout = setTimeout(() => {
                   fn.apply(this, args)
             }, wait)
          }
          return result
      }
  }
參考

从搜索系统来聊聊防抖和节流
JavaScript专题之跟着underscore学防抖

高階函式

把函式當作參數或者返回函式的函式, 就是為高階函式。

let add = function(a){
   return function(b){
      return a+b
   }
}

add(2)(3)  // return 5

JS原生的高階函數有bind,call,apply,

Array的sort,reduce,map,filter..等等

let newArr = [1,2,3,4].map(function(item){
   return item+2
})
console.log(newArr) //[3,4,5,6]

單頁面應用(SPA)

由於AJAX的出現,使得我們在同一個頁面上不用刷新就能夠處理資料(CRUD),來進行畫面的互動效果,進而達到單頁面的效果。

但是透過AJAX只能達到單頁面的操作,切換到別的路徑時畫面還是會重整一次才顯示,這樣會照成使用者的體驗不好,那現今各大框架的路由是怎麼解決這個問題的呢?

透過兩種方式來解決:

  1. 路徑hash的判斷。由於路徑後面加上#之後,不會再隨著路徑不同而去向後端發出請求,也不會重新加載頁面,透過onhashchange方法可以監聽hash值有沒有發生改變。

  2. 由於HTML5 histroy API 多了pushState,replaceState這兩個方法,可以改變當前的url,但是不會向後端發出請求。

從URL輸入到頁面渲染結束發生了什麼?

讓我們先將全部過程列出來,在一個一個去解析過程。

  1. DNS 解析:將域名解析成IP
  2. TCP 三次握手
  3. 瀏覽器HTTP請求
  4. 服務端HTTP返回請求
  5. 瀏覽器渲染畫面
  6. TCP 四次揮手

DNS 解析

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.