[javascript]關於javascript的那些奇形怪狀的事

[javascript]關於javascript的那些奇形怪狀的事

前言

最近在協助團隊寫vue.js的時候,因為看到很多人對javascript的坑不理解,或者是不熟悉導致問題連連,在jquery的世界很多情境可能是取取dom改一下元素,或者不明究理的硬改讓效果能出來就好了,但當你要使用前端框架的時候,良好的javascript基礎是很重要的,而javascript太自由了,雖然寫起來很方便,但也會遇到很多坑啊雷的地方,雖然在babel和es6或typescript的協助下,已經為此語言改善了非常多,但是人在江湖上誰能不挨刀,就是會遇到很老舊的網站得維護,所以什麼奇怪不明究理的程式碼到處充斥著也不足為奇了,筆者就來記錄一下有關javascript的一些比較少用的用法,或者一些奇怪的雷和觀念,希望能幫助到各位和自己。

導覽

  1. 關於true或false的一些秘密
  2. &&或||的運用
  3. 不傳參數的function也能成立
  4. 物件和陣列判斷出來都是物件
  5. 如何deep clone一個by reference的物件呢
  6. 關於this的一些bug
  7. IIFE是什麼鬼東西
  8. call,bind,apply
  9. 建立一個不同reference新物件
  10. javascript的hosting
  11. 結論

關於true或false的一些秘密

在javascript裡面,其實我們常常在判斷值的時候,會用如下語法來做判斷,如果你還不是這樣子做判斷的話,那請記得此規則囉。

if (!a) console.log(a)
a = null
if (!a) console.log(a)
a = ''
if (!a) console.log(a)
a=0
if (!a) console.log(a)
a=false
if (!a) console.log(a)
a=123
if (!a) console.log(a)

除了最後兩個之外,全部都會印出來,從此實驗我們可以得到一個結論,有值的就是為true,除了兩個特例,一是a=false的話會為false,二是a=0的話會為false,所以如果我們的值為數字的話,就要明確判斷數字是否大於0比較保險哦。

&&或||的運用

這兩個比較常用到的是||,有時候我們希望為一個不存在的值給一個預設值,就可以用如下的方式去做

var a = undefined
a = a || 123
console.log(a)

最後a會等於123,可以想像成a如果不成立的話,就把123給予a變數,或者用更白話的意思,如果a沒有值的話,就給予123,接著我們來看一下&&又代表什麼呢?

var a = 123
a = a && 'abc'
console.log(a)

a會等於abc,因為a有值所以判斷成功,所以把'abc'給予a變數,但其實我們還可以當成判斷條件式的應用,比如下方的例子

var a = undefined
a || console.log('is false') //等同於 if(!a)console.log('is false')
a = 123
a && console.log(a) //等同於 if(a) console.log(a)

不傳參數的function也能成立

在javascript的世界裡面,你定義了一個method,就算你不傳參數也能呼叫,只是參數會變成undefined而已,如下例子

     callMethodWithoutParam()

      function callMethodWithoutParam(msg){
        console.log(msg)
      }

會得到一個undefined,並不會報錯哦,這跟c#明顯是不一樣的事,所以在沒有es6的時候,我們如果要預防空值的話,會用如下的方式去做

     callMethodWithoutParam()

      function callMethodWithoutParam(msg){
        msg=msg||'default value'
        console.log(msg)
      }

在es6就更簡便了,為參數給一個default value就好了

     callMethodWithoutParam()

      function callMethodWithoutParam(msg='default value'){
        console.log(msg)
      }

很多東西其實都是物件

在javascript建立一個物件有很多方法,但是最直接也最好的方法就是使用{}來定義,如下方式

    var a={
      id:1,
      name:'kin'
    }

而如果要定義陣列的話,可以如下方式

var b=[1,2,3]

但是事實上這兩種都是物件,如果我們用typeof來看的話,就會呈現出來,不過實際上array是繼承自Array.prototyte,而object則是繼承自Object.prototyte,所以可以使用的api就不一樣了,那我們即然用typeof判斷不出來的話,我們得怎麼去察覺呢?可以用如下的方式。

      var a = {
        id: 1,
        name: 'kin'
      }

      var b = [1, 2, 3]
      console.log(a.constructor === Array)
      console.log(b.constructor === Array)

如何deep clone一個by reference的物件呢

首先解釋一下什麼叫做by reference,看一下下面的程式碼

      var a = {
        id: 1,
        name: 'kin'
      }

      var b = a

      a.id = 2

      console.log(a.id, b.id)

結果就是a和b的id都改成2了,因為共用同一份記憶體了,所以改a就等於改到b,那要怎麼clone一份物件出來呢?我們可以使用如下兩種方式

在es6或較新的瀏覽器,其實有兩種語法可以直接clone一個新的reference,如下方式

      var b = Object.assign({},a)
      var b = {...a}

相信如果有寫react的人對上述的語法一定不陌生,但是這個clone只是淺層的,深層的話還是沒有用的,有一種更簡單的方式,使用如下方式,先轉成字串再轉回來,就可以了

      var b = JSON.stringify(a)
      b = JSON.parse(b)

關於this的一些bug

相信很常有看到程式是這樣寫如下的方式來指派this

var vm=this

先來看一下下面的情境

      var a = {
        id: 1,
        name: 'kin',
        getName: function () {
          console.log(this)
        }
      }

這時候的this會是代表著a這個物件

但如果我們在getName裡面在寫一個方法並把this印出來會變成什麼狀況呢?

      var a = {
        id: 1,
        name: 'kin',
        getName: function () {
          console.log(this)          
          function setName(){
            console.log(this)
          }
          setName()
        }
      }


第二個this卻是指到了window物件了,所以我們必須把程式碼加上vm=this的語法,以讓setName裡面可以取到a物件

      var a = {
        id: 1,
        name: 'kin',
        getName: function () {
          console.log(this)
          var vm = this;          
          function setName() {
            console.log(vm)
          }
          setName()
        }
      }

不過如果你是寫es6的話,善用arrow function的語法就不會遇到這個問題了

      var a = {
        id: 1,
        name: 'kin',
        getName() {
          console.log(this)
          const setName = () => {
            console.log(this)
          }
          setName()
        }
      }

IIFE是什麼鬼東西

IIFE全名為(Immediately Invoked Functions Expressions),翻白話文就是建立function的時候馬上執行的意思,如下範例就會直接執行函式了

var test=function(){
  console.log('test')
}()

如果我們想要可以傳參數的話,也可以這樣子寫

var test=function(msg){
  console.log(msg)
}('test')

當我們使用這種語法來定義函數的話,就不能再呼叫了,不過蠻少看人會這樣子用的,但是我們常常會看到為了避免全局汙染,而使用如下的方式,這就是為何會有如下的語法出現囉

(function (msg) {
  console.log(msg)
}('test'))

但是即然我們希望不要全局汙染,為何還要丟參數進來呢?有時候為了一些莫名奇妙的理由,所以需要把外面的東西丟進來讓裡面變更,比如丟一個對象進去,改完之後全局的變數也會跟著變更。

<body onload="init()">
  <script>
    var globalVar = {
      id: 1,
      name: 'kin',
      getName() {
        console.log(this)
        const setName = () => {
          console.log(this)
        }
      }
    }

    function init() {
      (function (obj) {
        obj.name = 'kinanson'
      }(globalVar))
      globalVar.getName()
    }

  </script>
</body>

call,bind,apply

這個是我記得以前有在javascript的考題看過,所以特別記錄下來的,這三個api都是在解決this這個bug,以之前this的例子來看,setName應該會是印出window而不是a這個物件

      var a = {
        id: 1,
        name: 'kin',
        getName: function () {
          console.log(this)          
          function setName(){
            console.log(this)
          }
          setName()
        }
      }

如果我們用bind來處理的話,就會這樣處理this的bug

      var a = {
        id: 1,
        name: 'kin',
        getName: function () {
          console.log(this)
          function setName() {
            console.log(this)
          }

          var setUserName=setName.bind(a)
          setUserName()
        }

      }
        a.getName()

而call的話則是可以多放入方法的參數,並且直接呼叫此方法使用

      var a = {
        id: 1,
        name: 'kin',
        getName: function () {
          console.log(this)
          function setName(userName) {
            this.name = userName
            console.log(this)
          }

          setName.call(a, 'kinanson')
        }

      }
      a.getName()

apply的話跟call很像,差異在後面的參數是用陣列帶入而已

setName.apply(a, ['kinanson'])

當然如果你寫的是es6的話,善用arrow function就不會再遇到this的坑了。

建立一個不同reference新物件

之前有提到,在javascript建立物件比較好的方法是使用如下語法

 let obj={
   id:1,
   name:'anson'
 }

但是這種方法卻是singleton的方法,也就是全局共用了這個物件的屬性,那我們如何建立一個全新的物件,然後不是全局共用的呢?其實使用function就可以了,請見如下例子

function obj() {
  this.id = 1
  this.name = 'kin'
}
var a = new obj()
var b = new obj()
b.id = 2
console.log(`a物件為${a.id},b物件為${b.id}` )


但是請注意一點很重要的事情,因為我們每次new一個物件,就是佔用了一個記憶體,所以如果我們在這個物件有共用方法的話,可以使用為物件的prototype的方式,讓每個new出來的物件,共享同一個記憶體的方法,以避免不必要的記憶體浪費

function obj() {
  this.id = 1
  this.name = 'kin'
}
obj.prototype.getProperty = function () {
  console.log(this.id, this.name)
}
var a = new obj()
var b = new obj()
b.id = 2
a.getProperty()
b.getProperty()

而善用prototype的特性,我們就很容易替一個物件新增新特性,比如說vue我們想要新增一個全局的alert方法,我們就只要寫如下語法就行了

Vue.prototype.alert=()=>{
  toastr('something')
}

當然es6已經有了跟我們寫class很類似的語法了,所以如果你是寫es6的話,還是直接善用新特性吧。

javascript的hosting

在javascript裡面,定義了var永遠都會被自動放在當前函數的最前面,但如下代碼會是undefind,因為使用了立即執行函數的原因


var   v = "hello" ;
( function (){
   console.log(v);
   var   v = "world" ;
})();

我們也可以先不使用var,然後直接給變數值,在後面才使用var,此答案為印出hello

( function (){
  v="hello";
   console.log(v);
  var v = "world" ;
})();

但是執行函數則不行,我們可以使用一個變數代表一個方法,此段示例則會顯示報錯

( function (){
foo()
      var foo=function(){
console.log('hi');
    }
})();

有鑑於var有很多陷阱,所以在es6之後,我們應該使用let來代替var,以防止一些奇奇怪怪的bug問題

結論

這篇並不能包含全部的問題,日後有發覺自己沒看到的,或者是有學習到新的再來補一下,有關於另外一些memory leak的問題,也可以看一下筆者以前的文章(https://dotblogs.com.tw/kinanson/2017/05/11/140022)