《你不知道的JavaScript 上》讀書筆記

標簽: js 還有原型鏈要更新呀


作用域

  • 作用域是查找變量的一套規則
  • 如果查找的目的是對變量進行賦值,則進行LHS查詢;如果查找的目的是獲取變量的值,則進行RHS查詢。Left/Right Hand Sidei,變量在等號左邊或等號右邊
  • LHS查詢:賦值操作,如=操作符、調用函數時傳參
 function foo(e) {
    var b = a;
    return a + b;
}
var c = foo(2);
  • 例子中LHS查詢有三處:b=a c=foo(2) 把2賦給a(隱式變量分配)
  • RHS查詢有四處:foo(2) =a a b
  • 異常
    • ReferenceError,引用一個未被定義的變量。
    • TypeError,對結果的操作不合理,如引用一個undefined類型的值的屬性
    • SyntaxError,語法錯誤
  • 如何判斷當前是否在嚴格模式下:函數直接調用時this指向運行時環境的全局對象。在非嚴格模式下,瀏覽器中全局對象為Window,Nodejs控制臺中全局對象為Global;在嚴格模式下,全局對象是undefined。因此可判斷如下
var isStrict = (function(){return !this;}());
console.log('isStrict ' + isStrict);
  • 編譯。傳統編譯語言,代碼執行前需要編譯,包括三個過程:詞法分析(將代碼拆分為詞法單元)、語法解析(將詞法單元數組轉換為語法樹)、代碼生成(生成可執行代碼)

詞法作用域VS動態作用域

  • 編譯的詞法分析階段知曉各標識符及如何聲明的,因此可以在執行過程中進行查找。詞法作用域意味著作用域在書寫代碼時函數聲明的位置決定。
  • eval with
  • 詞法作用域又叫靜態作用域,和動態作用域的區別是動態作用域是在運行時而非定義時確定的。詞法作用域關注函數在何處聲明,動態作用域關注函數從何處調用,其作用域鏈是基于運行時的調用棧
  • wiki:大多數現在程序設計語言都是采用靜態作用域規則,如C/C++、C#、Python、Java、JavaScript;采用動態作用域的語言有Pascal、Emacs Lisp、Common Lisp(兼有靜態作用域)、Perl(兼有靜態作用域)。C/C++是靜態作用域語言,但在宏中用到的名字,也是動態作用域。
// 詞法作用域:
function foo() {
    print a;    // 輸出2
}

function bar() {
    var a = 3;
    foo();
}

var a = 2;
bar();
// 動態作用域
function foo() {
    print a;    // 輸出3而不是2
}

function bar() {
    var a = 3;
    foo();
}

var a = 2;
bar();

函數作用域

  • 最小特權原則
  • 在軟件設計中,應該最小限度地暴露必要內容,而將其隱藏起來,比如某個模塊或對象的API設計。
  • 避免暴露私有的變量或函數,避免其被有意或無意地以非預期的方式使用。
function doSomething(a){
    b = a + doSomethingElse(a * 2);
    console.log(b * 3);
}
function doSomethingElse(e){
    return e - 1;
}
var b;
doSomething(2); // 15
  • 上例中b doSomethisElse均應是doSomething的內部內容,改進:
function doSomething(a){
    function doSomethingElse(e){
        return e - 1;
    }
    var b;
    b = a + doSomethingElse(a * 2);
    console.log(b * 3);
}
doSomething(2); // 15           
  • 立即執行函數表達式IIFE 的兩種形式
(function(){console.log('a');})() // a
// 第一個()將函數變成表達式,第二個()立即執行這個函數

+function(){console.log('a');}() // a
// + 也會將函數變成一個表達式,js會解析成運算(bootstrap的js源碼采用此種方式),第二個()立即執行這個函數

js中神奇的 +
new Date(); // Fri Aug 05 2016 16:58:08 GMT+0800 (CST)
+ new Date; // 1470387494773 (一個時間戳)

// 時間戳:unix時間,從`協調時間時`1970年1月1日0時0分0秒起至現在的總秒數
  • IIFE進階1:當作函數調用并傳參,提高可讀性,改進代碼風格。
var a = 2;
(function IIFE(global){
    var a = 1;
    console.log(a); // 1
    console.log(global.a); // 2
})(window);
  • IIFE進階2:倒置代碼的運行順序,將需要運行的代碼放在第二位,在IIFE執行后當作參數傳遞進去
var a = 2;
(function IIFE(def){
    def(window);
    console.log(a); // 2
}(function def(global){    
    var a = 1;
    console.log(a); // 1
    console.log(global.a); // 2
}));

塊級作用域

let ES6變量聲明

  • 隱式綁定在所在塊級作用域
var foo = true;
if (foo) {
    var bar = foo * 2;
    console.log(bar); // 2
}
console.log(bar); // 2
var foo = true;
if (foo) {
    let bar = foo * 2;
    console.log(bar); // 2
}
console.log(bar); // ReferenceError: bar is not defined
  • 顯式創建一個塊級作用域
var foo = true;
if (foo) {
    {
        let bar = foo * 2;
        console.log(bar); // 2
    } // 一個顯式的塊
    console.log(bar); // ReferenceError: bar is not defined
}
console.log(bar); // ReferenceError: bar is not defined
  • let聲明的變量在塊級作用域中不會提升
  • 垃圾回收,let塊級作用域中的內容執行完后被銷毀
  • let循環
for(let i=0;1<10;i++) {
    console.log(i);
}
console.log(i); // ReferenceError: i is not defined
  • let將i綁定在for循環的塊中,不會污染全局變量,并且將其重新綁定在循環的每一次迭代中。

const ES6變量聲明

  • const聲明的變量只在聲明時賦值,不可修改,否則會報錯
  • const聲明的變量必須在聲明時就賦值。
var foo = true;
if(foo) {
    var a = 2;
    const b = 3;
    const c = {};
    const d; // SyntaxError: Missing initializer in const declaration
    a = 3;
    b = 4; // TypeError: Assignment to constant variable
    c.a = 1; 
    console.log(c); // {a: 1}
    console.log(c.a); // 1
}
console.log(a); // 3
console.log(b); // ReferenceError: b is not defined

提升(hoisting)

  • 理解提升:引擎會在解釋js代碼前首先進行編譯,編譯的一部分工作就是找到所有聲明,并用合適的作用域將他們包裹起來。所有聲明會在代碼執行前被處理。
  • 編譯階段聲明會被提升,賦值或其他運算邏輯留在原地在執行過程中被處理。
  • 函數聲明提升的同時,包含了實際函數的隱藏值。
foo(); // undefined;
function foo() {
    console.log(a);
    var a = 1;
}
// 此時foo()可正常調用,foo()函數內部也有變量提升
  • 函數表達式和變量聲明類似,變量標識符會提升,賦值沒有被提升
foo(); // TypeError: foo is not a function
var foo = function() {
    console.log(a);
    var a = 2;
}
// 此時報錯不是ReferenceError,因為此時變量標識符foo被提升到了頂部,var foo;但賦值未被提升,此時foo的值是undefined,對一個undefined值進行函數調用是非法操作,因此拋出TypeError異常
  • 提升中函數優先
foo(); // 1
var foo;
function foo() { 
    console.log(1); 
} 
foo = function() {
    console.log(2);
}

引擎解釋:

function foo() { 
    console.log(1); 
} 
foo(); 
foo = function() {
    console.log(2);
}
// var foo;是重復聲明被忽略。
foo(); // 此時再調用foo()值為2,第二次定義的函數表達式覆蓋了前面的函數聲明。
  • 一個函數聲明會被提升到所在作用域的頂部
foo(); //  TypeError: foo is not a function
var a = true;
if (a) {
    function foo(){console.log(11);} 
} else {
    function foo(){console.log(22);} // 
}
// 老版本中兩個foo函數會被提升,第二個foo函數會覆蓋第一個,因此foo()執行結果是22;新版本中if塊內僅提升函數聲明,但未包含實際函數的隱藏值,因此foo()執行結果是TypeError

作用域閉包

  • 閉包:內部函數可以訪問外部函數作用域,并持有對原始詞法作用域的引用。
  • 經典例子:循環與閉包
for(var i=0;i<5;i++) {
    setTimeout(function timer() {
        console.log(i);
    }, i*1000);
}
  • 輸出五次5的原因:循環中的五個函數在每次迭代中分別定義,但它們被封閉在一個共享的全局作用域,因此共享一個i的引用。
  • 解決方法是給每次循環的函數創建一個單獨的作用域
for(var i=0;i<5;i++) {
    (function() {
        var j = i;
        setTimeout(function timer() {
            console.log(j);
        }, j*1000);
    })()
}
// IIFE為每次迭代創建一個新的作用域,延遲函數的回調可以將新的作用域封閉在每個迭代內部,每個迭代都有一個正確的變量。

or

for(let i=0;i<5;i++) { // i在每次迭代都會聲明
    setTimeout(function timer() {
        console.log(i);
    }, i*1000);
}

or

for(var i=0;i<5;i++) {
    let j = i; // 創建塊級作用域
    setTimeout(function timer() {
        console.log(i);
    }, i*1000);
}

this

  • 隱式傳遞一個對象引用
  • 一個函數被調用時,會創建一個執行上下文,包含函數在哪里被調用,函數的調用方式,傳入的參數等,this是其中的一個屬性,是函數被調用時發生的綁定。this既不指向函數本身也不指向函數的詞法作用域。
  • 直接調用 new dot(.) call apply bind
function fo(){this.x='x';};
// 直接調用
fo(); // ()代表立即執行
x;    // 'x' this指向全局對象window
var fn = function(){'use strict';this.x='x';};
x;    // ReferenceError: x is not defined,此時全局對象是undefined;

// new調用
var foo = new fo();
foo.x; // 'x'

// dot(.)調用
var aa = {x: 'aaa',fo: fo};
aa.x; // 'aaa'
aa.fo; // function fo(){this.x='x';};
aa.fo();
aa.x; // 'x'

// call
var b = {x: 'bb'};
b.x; // 'bb'
b.fo(); // TypeError: b.fo is not a function
fo.call(b);
b.x; // 'x'

// apply bind 用法和call相似,this均指向fo.call(args),args的第一個參數

對象

  • javascript六種類型:string number boolean null undefined object
  • 內置對象,對象子類型:string number boolean object function array date regexp error
  • ES6增加了可計算屬性名,用[]包裹,eg:
var prefix = foo; 
var myObject = {[prefix + 'bar']: 'hello', [prefix + 'baz']: 'world'};
myObject['foobar']; // hello
myObject['foobaz']; // world
  • 數組也是對象,但是最好用對象存儲鍵值對,用數組存儲數組下標值對;
// 給數組定義數字形式的屬性名的bug
var myArray = ['hello', 'world', 42];
myArray.baz = 'bar';
myArray.length; // 3
myArray.baz; // 'bar'
myArray['3'] = 'test'; 
myArray.length; // 4 
myArray; // ['hello', 'world', 42, 'test']
  • 檢查屬性是否存在
var myObject = { a: 2 };
myObject['b']; // 2
myObject.b; // 2
'a' in myObject; // true
myObject.hasOwnProperty('a'); // true
// in操作符檢查屬性是否在對象中或prototype原型鏈中;hasOwnProperty僅檢查屬性是否在對象中。
  • 屬性描述符 ES5
  • getOwnpropertyDescriptor查詢對象的屬性的屬性描述符,格式:Object.getOwnpropertyDescriptor(對象名,'屬性名');
Object.getOwnPropertyDescriptor(myObject,'a');
// Object {
//    value: 2, 
//    writable: true,     可寫,可修改屬性的值value
//    enumerable: true,   可枚舉,出現在對象屬性的遍歷中
//    configurable: true  可配置,可用defineProperty修改屬性描述符,刪除一個屬性delete myObject.a; if false,除value和writable外的特性不能被改變。
// }
  • defineProperty添加一個新屬性或修改一個已有屬性(的屬性描述符),格式:Object.defineProperty(對象名,'屬性名',{});
Object.defineProperty(myObject,'b',{
       value: 3,
       writable: true,
       configurable: true,
       enumerable: true
}); // Object {a: 2, b: 3}
Object.getOwnPropertyDescriptor(myObject,'b');
// Object {value: 3, writable: true, enumerable: true, configurable: true}
  • 枚舉屬性 enumerable
var myObject = {};

Object.defineProperty(myObject,'a',{
      value: 2,
      enumerable: true
});

Object.defineProperty(myObject,'b',{
      value: 3,
      enumerable: false
});

myObject.b; // 3
'b' in myObject; // true
myObject.hasOwnProperty('b'); // true

for(var k in myObject) {
      console.log(k+': '+myObject[k]);
} // a: 2
// myObject.b存在且可正常訪問,但不出現在屬性的遍歷中

myObject.propertyIsEnumerable('b'); // false
// propertyIsEnumerable檢查給定的屬性名存在于對象中(不包括原型鏈中)&& 可枚舉的

Object.keys(myObject); // ['a'] 返回可枚舉屬性

Object.getOwnPropertyNames(myObject); // ['a','b'] 返回所有屬性
  • 數組遍歷注意事項:數組上應用for..in循環時不僅包含所有數值索引,且包含所有可枚舉屬性;因此遍歷數組最好使用傳統for循環或forEach循環。ES6中新增for..of循環遍歷數據結構。數組有內置的@@iterator,因此for..of可直接遍歷數組,而不能直接遍歷對象。P122
var myArray = ['hello','world',42];
myArray.baz = 'bar';
myArray['3']='test';
myArray; // ["hello", "world", 42, "test"]

for(var k in myArray) {console.log(k,myArray[k]);}
//  0 hello  1 world  2 42  3 test  baz bar

for(vari=0;i<myArray.length;i++){console.log(i,myArray[i])}; 
//  0 hello  1 world  2 42  3 test 

myArray.forEach(function(e){console.log(e)}); 
// hello world 42 test

for(var v of myArray){console.log(v);};
// hello world 42 test

for(var v of myObject){console.log(v);};
// TypeError: myObject[Symbol.iterator] is not a function

原型prototype


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容