[筆記]你不知道的JavaScript(上)


前言

文章只記錄理解以及容易遺忘的知識點。

詞法作用域、塊作用域

詞法作用域

詞法作用域:簡單的說,詞法作用域就是定義在詞法階段的作用域。換句話說,詞法作用域就是在你寫代碼時將變量和塊作用域寫在哪里來決定的,因此在詞法分析器處理代碼時會保持作用域不變(大部分情況是這樣的)。

當然有一些欺騙詞法作用域的方法,這些方法在詞法分析器處理后依然可以改變作用域。

欺騙詞法作用域的方法有:

  • eval():可以接受一個字符串作為參數。
  • with:通常被當作重復引用同一個對象中的多個屬性的快捷方式,可以不需要重復引用對象本身。
var obj = { a:1, b:2, c:3 }; //單調乏味的重復"obj" obj.a=2; obj.b=3; obj.c=4; //簡單的快捷方式 with(obj){ a=2; b=3; c=4; }

塊作用域

  • with
  • try/catch
  • let
  • const

簡單解釋下箭頭函數:簡單來說,箭頭函數在涉及this綁定時的行為和普通函數的行為完全不一致。它放棄了所有普通this綁定規則,取而代之的是用當前的詞法作用域覆蓋了this本來的值。

作用域閉包

現代的模塊機制

大多數模塊依賴加載器/管理器本質上都是將這種模塊定義封裝進一個友好的API。這里並不會研究某個具體的庫,為了宏觀了解簡單介紹一些核心概念:

var MyModules = (function Manager(){
    var modules = {};
    function define(name,deps,impl){
        for(var i = 0; i < deps.length; i++){
            deps[i] = modules[deps[i]];
        }
        modules[name] = impl.apply(impl,deps);
    }
    
    function get(name){
        return modules[name];
    }
    return {
        define:define,
        get:get
    }
})();

這段代碼的核心是modules[name] = impl.apply(impl,deps)。為了模塊的定義引入了包裝函數(可以傳入任何依賴),並且將返回值,也就是模塊的API,存儲在一個根據名字來管理的模塊列表中。

下面用它來如何定義模塊:

MyModules.define("bar",[],function(){
    function hello(who){
        return "Let me introduce:" + who;
    }
    return {
        hello:hello
    }
});

MyModules.define("foo",['bar'],function(bar){
    var hungry = "hippo";
    function awesome(){
        console.log(bar.hello(hungry).toUpperCase());
    }
    return {
        awesome:awesome
    }
});

var bar = MyModules.get("bar");
var foo = MyModules.get("foo");

console.log(bar.hello("hippo")); //Let me introduce:hippo
foo.awesome();  //LET ME INTRODUCE:HIPPO

 “foo”和“bar”模塊都是通過一個返回公共API的函數來定義的。“foo”甚至接受“bar”的示例作為依賴參數,並能相應地使用它。

未來的模塊機制

bar.js
function hello(who){
    return "Let me introduce:" + who;
}
export hello;

foo.js
//僅從"bar"模塊導入hello()
import hello from "bar";
var hungry = "hippo";
function awesome(){
    console.log(hello(hungry).toUpperCase());
}
export awesome;

baz.js
//導入完整的"foo"和"bar"模塊
module foo from "foo";
module bar from "bar";
console.log(bar.hello("hippo")); //Let me introduce:hippo
foo.awesome();  //LET ME INTRODUCE:HIPPO

 import可以將一個模塊中的一個或多個API導入到當前作用域中,並分別綁定在一個變量上(在我們的例子里是hello)。module會將整個模塊的API導入並綁定到一個變量上(在我們的例子里是foo和bar).export會將當前模塊的一個標識符(變量、函數)導出為公共API。這些操作可以在模塊定義中根據需要使用任意多次。

動態作用域

function foo(){
    console.log(a); //2
}
function bar(){
    var a = 3;
    foo();
}
var a = 2;
bar(); 

如果JS具有動態作用域,那么打印的值就是3,而不是2了。需要明確的是,事實上JS並不具有動態作用域。它只有詞法作用域,簡單明了。但是this機制某種程度上很像動態作用域。

主要區別:詞法作用域是在寫代碼或者說定義時確定的,而動態作用域是在運行時確定的。(this也是!)詞法作用域關注函數在何處聲明,而動態作用域關注函數從何處調用。最后,this關注函數如何調用,這就表明了this機制和動態作用域之間的關系那么緊密。

this解析

JS有許多的內置函數,都提供了一個可選的參數,通常被成為“上下文”(context),其作用和bind(...)一樣,確保你的回調函數使用指定的this。如下例子:

function foo(el){
    console.log(el,this.id);
}

var obj = {
    id:"awesome"
};

//調用foo(...)時把this綁定到obj
[1,2,3].forEach(foo,obj); //結果:1 "awesome" 2 "awesome"  3 "awesome"

 bind()

bind()方法創建一個新的函數,在調用時設置this關鍵字為提供的值。並在調用新函數時,將給定參數列表作為原函數的參數序列的前若干項。

語法:function.bind(thisArg[, arg1[, arg2[, ...]]])

簡單例子:

var module = {
  x: 42,
  getX: function() {
    return this.x;
  }
}

var unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// expected output: undefined

var boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// expected output: 42

你可以將下面這段代碼插入到你的腳本開頭,從而使你的 bind() 在沒有內置實現支持的環境中也可以部分地使用bind。

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    var aArgs   = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP    = function() {},
        fBound  = function() {
          // this instanceof fBound === true時,說明返回的fBound被當做new的構造函數調用
          return fToBind.apply(this instanceof fBound
                 ? this
                 : oThis,
                 // 獲取調用時(fBound)的傳參.bind 返回的函數入參往往是這么傳遞的
                 aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    // 維護原型關系
    if (this.prototype) {
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype; 
    }
    // 下行的代碼使fBound.prototype是fNOP的實例,因此
    // 返回的fBound若作為new的構造函數,new生成的新對象作為this傳入fBound,新對象的__proto__就是fNOP的實例
    fBound.prototype = new fNOP();

    return fBound;
  };
}

 詳細參考地址:《MDN:Function.prototype.bind()》

對象

對象鍵只能是字符串

symbol 出現之前,對象鍵只能是字符串,如果試圖使用非字符串值作為對象的鍵,那么該值將被強制轉換為字符串,如下:

const obj = {};
obj.foo = 'foo';
obj['bar'] = 'bar';
obj[2] = 2;
obj[{}] = 'someobj';
console.log(obj);

結果:

2:2
[object Object]:"someobj"
bar:"bar"
foo:"foo"

屬性描述符

從ES5開始,所有的屬性都具備了屬性描述符。

思考如下代碼,使用Object.getOwnPropertyDescriptor()

var myObject = {
    a:2
};
var result = Object.getOwnPropertyDescriptor(myObject,"a");
console.log(result);

得到的結果如下:

{
    configurable:true,
    enumerable:true,
    value:2,
    writable:true
}

這個普通的對象屬性對應的屬性描述符除了有value值為2,還有另外三個特性:writable(可寫)enumerable(可枚舉)configurable(可配置)

使用Object.defineProperty()來添加一個新屬性或者修改一個已有屬性(如果它是configurable)並對特性進行設置。

writable

如下代碼:

var myObject = {}
Object.defineProperty(myObject,"a",{
    value:2,
    writable:false, //不可寫
    configurable:true,
    enumerable:true
});
myObject.a = 3;
console.log(myObject.a); //2

如果在嚴格模式下,上面這寫法報錯:

"use strict";
var myObject = {}
Object.defineProperty(myObject,"a",{
    value:2,
    writable:false, //不可寫
    configurable:true,
    enumerable:true
});
myObject.a = 3; //Uncaught TypeError: Cannot assign to read only property 'a' of object '#<Object>'

configurable

var myObject = {}
Object.defineProperty(myObject,"a",{
    value:2,
    writable:true, 
    configurable:false, //不可配置
    enumerable:true
});
myObject.a = 5;
console.log(myObject.a); //5
delete myObject.a;
console.log(myObject.a); //configurable:false,禁止刪除這個屬性

Object.defineProperty(myObject,"a",{
    value:6,
    writable:true, 
    configurable:true, 
    enumerable:true
}); //TypeError: Cannot redefine property: a

上面代碼可以看出,設置configurable為false是單向操作,無法撤銷。同時還會禁止刪除這個屬性。

注意:要注意一個小小的例外,即使屬性configurable:false,我們還是可以把writable的狀態有true改為false,但是無法由false改為true。

enumerable

從名字可以看出來,這個描述符控制的是屬性是否出現在對象的屬性枚舉中,比如for...in循環。

不變性

有時候我們希望屬性或者對象是不可改變的。ES5中有很多方法可以實現。

對象常量

結合writable:false和configurable:false就可以真正的創建一個常量屬性(不可修改、重定義或者刪除)。

var myObject = {}
Object.defineProperty(myObject,"a",{
    value:2,
    writable:false, 
    configurable:false
});

禁止擴展Object.preventExtensions()

如果你想禁止一個對象添加新的屬性並且保留已有屬性,可以使用Object.preventExtensions()

var myObject = {
    a:2
};
Object.preventExtensions(myObject);
myObject.b = 3;
console.log(myObject.b); //undefined

在嚴格模式下,將會拋出TypeError錯誤。

密封Object.seal()

Object.seal()會創建一個“密封”的對象,這個方法實際上會在現有對象上調用Object.preventExtensions()並把所有現有屬性標記為configurable:false。

所以,密封之后不僅不能添加新的屬性,也不能重新配置或者刪除任何屬性(雖然可以修改屬性的值)。

凍結Object.freeze()

Object.freeze()會創建一個凍結對象,這個方法實際上會在一個現有對象上調用Object.seal()並把所有“數據訪問”屬性標記為writable:false,這樣就無法修改它們的值。

這個方法是你可以應用在對象上的級別最高的不可變性,它會禁止對於對象本身及其任意直接屬性的修改(這個對象引用的其它對象是不受影響的)。

你可以“深度凍結”一個對象,具體方法為,首先在這個對象上調用Object.freeze(),然后遍歷它所有引用的所有對象並在這些對象上調用Object.freeze()。但你一定要小心,因為這樣做,你可能會在無意中凍結其它(共享)對象。

Getter和Setter

對象默認的[[Put]]和[[Get]]操作分別可以控制屬性值的設置和獲取。

當你給一個屬性定義getter、setter或者兩者都有時,這個屬性會被定義為“訪問描述符”(和“數據描述符”相對的)。對於訪問描述符來說,JS會忽略它們的value和writable特性,取而代之的是關心set和get(還有configurable和enumerable)特性。

思考如下代碼:

var myObject = {
    get a(){
        return 2;
    }
};

Object.defineProperty(myObject,"b",{
    get:function(){
        return this.a * 2;
    },
    enmuerable:true
})
console.log(myObject.a); //2
console.log(myObject.b); //4

為了讓屬性更合理,還應該定義setter,setter會覆蓋單個屬性默認的[[Put]](也被稱為賦值)操作。通常來說getter和setter是成對出現的。

var myObject = {
    get a(){
        return this._a_;
    },
    set a(val){
        this._a_ = val * 2;
    }
};
myObject.a = 2;
console.log(myObject.a); //4

遍歷

for...in循環可以用來遍歷對象的可枚舉屬性列表(包括[[Prototype]]鏈)。

ES5增加了一些數組的輔助迭代器,包括forEach()、every()和some()。每種迭代器都可以接受一個回調函數並把它應用到數組的每個元素上,唯一的區別就是它們對於回調函數返回值的處理方式不同。

  • forEach():會遍歷數組中的所有值並忽略回調函數的返回值。
  • every():會一直運行直到回調函數返回false(或者“假”值)。
  • some():會一直運行直到回調函數返回true(或者“真”值)。

注:every()和some()中特殊的返回值和普通for循環中的break語句相似,他們會提前終止遍歷。

使用for...in遍歷對象是無法直接獲得屬性值的 ,它只是遍歷了對象中所有可以枚舉的屬性,你需要手動獲取屬性值。

ES6增加了一種用來遍歷數組的for...of循環語法(如果對象本身定義了迭代器的話也可以遍歷對象):

var myArray = [1,2,3];
for(var v of myArray){
    console.log(v); //1 2 3
};

for...of循環首先會向被訪問對象請求一個迭代器對象,然后通過調用迭代器對象的next()方法來遍歷所有返回值。

數組有內置的@@iterator,因此for...of可以直接應用在數組上。我們使用內置的@@iterator來手動遍歷數組,看看它是怎么工作的:

var myArray = [1,2,3];
var it = myArray[Symbol.iterator]();
var next1 = it.next();
var next2 = it.next();
var next3 = it.next();
var next4 = it.next();
console.log(next1); //{value: 1, done: false}
console.log(next2); //{value: 2, done: false}
console.log(next3); //{value: 3, done: false}
console.log(next4); //{value: undefined, done: true}

注:我們使用ES6中的符號Symbol.iterator來獲取對象的@@iterator內部屬性。@@iterator本身並不是一個迭代器對象,而是一個返回迭代器對象的函數--這一點非常精妙並且非常重要。

普通的對象並沒有內置的@@iterator,所以無法自動完成for...of遍歷。當然,你也可以給任何想遍歷的對象定義@@iterator,如下代碼:

var myObject = {
    a:2,
    b:3
};
Object.defineProperty(myObject,Symbol.iterator,{
    enumerable:false,
    writable:false,
    configurable:true,
    value:function(){
        var o = this,
            idx = 0,
            ks = Object.keys(o);
        return {
            next:function(){
                return {
                    value:o[ks[idx++]],
                    done:(idx > ks.length)
                }
            }
        }
    }
});

//手動遍歷myObject
var it = myObject[Symbol.iterator]();
var next1 = it.next();
var next2 = it.next();
var next3 = it.next();
console.log(next1); //{value: 2, done: false}
console.log(next2); //{value: 3, done: false}
console.log(next3); //{value: undefined, done: true}

//用for...of遍歷myObject
for(var v of myObject){
    console.log(v);
}
//2
//3

注:我們使用Object.defineProperty()定義了我們自己的@@iterator(主要是為了讓它不可枚舉),不過注意,我們把符號當做可計算屬性名。此外,也可以直接在定義對象時進行聲明,比如:

var myObject = {
    a:2,
    b:3,
    [Symbol.iterator]:function(){
        /*..*/
    }
};

對於用戶定義的對象來說,結合for...of和用戶自定義的迭代器可以組成非常強大的對象操作工具。

再看一個例子,寫一個迭代器生成“無限個”隨機數,我們添加一條break語句,防止程序被掛起,代碼如下:

var randoms = {
    [Symbol.iterator]:function(){
        return {
            next:function(){
                return {
                    value:Math.random()
                }
            }
        }
    }
};

var random_pool = [];
for(var n of randoms){
    random_pool.push(n);
    console.log(n);
    //防止無限運行
    if(random_pool.length === 10) break;
}

 constructor 屬性

語法:object.constructor

返回值:對象的constructor屬性返回創建該對象的函數的引用。

// 字符串:String() var str = "張三"; alert(str.constructor); // function String() { [native code] } alert(str.constructor === String); // true // 數組:Array() var arr = [1, 2, 3]; alert(arr.constructor); // function Array() { [native code] } alert(arr.constructor === Array); // true // 數字:Number() var num = 5; alert(num.constructor); // function Number() { [native code] } alert(num.constructor === Number); // true // 自定義對象:Person() function Person(){ this.name = "CodePlayer"; } var p = new Person(); alert(p.constructor); // function Person(){ this.name = "CodePlayer"; } alert(p.constructor === Person); // true // JSON對象:Object() var o = { "name" : "張三"}; alert(o.constructor); // function Object() { [native code] } alert(o.constructor === Object); // true // 自定義函數:Function() function foo(){ alert("CodePlayer"); } alert(foo.constructor); // function Function() { [native code] } alert(foo.constructor === Function); // true // 函數的原型:bar() function bar(){ alert("CodePlayer"); } alert(bar.prototype.constructor); // function bar(){ alert("CodePlayer"); } alert(bar.prototype.constructor === bar); // true

原型

對象關聯

使用Object.create()可以完美的創建我們想要的關聯關系。

var foo = {
    something:function(){
        console.log("tell me something");
    }
};

var bar = Object.create(foo);
bar.something(); //tell me something

Object.create()的polyfill代碼,由於Object.create()是在ES5中新增的函數,所以在舊版瀏覽器中不支持,使用下面這段代碼兼容:

if(!Object.create){
    Object.create = function(o){
        function F(){};
        F.prototype = o;
        return new F();
    }
}

標准ES5中內置的Object.create()函數還提供了一系列的附加功能。如下代碼:

var anotherObject= {
    a:2
};
var myObject = Object.create(anotherObject,{
    b:{
        enumerable:false,
        writable:true,
        configurable:false,
        value:3
    },
    c:{
        enumerable:true,
        writable:false,
        configurable:false,
        value:4
    }
});
console.log(myObject.hasOwnProperty('a')); //false
console.log(myObject.hasOwnProperty('b')); //true
console.log(myObject.hasOwnProperty('c')); //true

console.log(myObject.a); //2
console.log(myObject.b); //3
console.log(myObject.c); //4

Object.create(..)第二個參數指定了需要添加到新對象中的屬性名以及這些屬性的屬性描述符。

關聯關系是備用

下面代碼可以讓你的API設計不那么“神奇”,同時仍然能發揮[[Prototype]]關聯的威力:

var anotherObject= {
    cool:function(){
        console.log('cool!');
    }
};
var myObject = Object.create(anotherObject);
myObject.deCool = function(){
    this.cool();
}
myObject.deCool();

 行為委托

面向委托的設計:比較思維模型

下面比較下這兩種設計模式(面向對象和對象關聯)具體的實現方法。下面典型的(“原型”)面向對象風格:

function Foo(who){
    this.me = who;
}
Foo.prototype.identify = function(){
    return "I am " + this.me;
}

function Bar(who){
    Foo.call(this,who);
}
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.speak = function(){
    console.log("hello, " + this.identify() + ".");
}
var b1 = new Bar("b1");
var b2 = new Bar("b2");

b1.speak(); //hello, I am b1.
b2.speak(); //hello, I am b2.

子類Bar繼承了父類Foo,然后生成了b1和b2兩個實例,b1委托了Bar.prototype,后者委托了Foo.prototype。這種風格很常見。

對象關聯風格實現相同的功能:

var Foo = {
    init:function(who){
        this.me = who;
    },
    identify:function(){
        return "I am " + this.me;
    }
};

var Bar = Object.create(Foo);

Bar.speak = function(){
    console.log("hello, " + this.identify() + ".");
}

var b1 = Object.create(Bar);
b1.init("b1");
var b2 = Object.create(Bar);
b2.init("b2");

b1.speak(); //hello, I am b1.
b2.speak(); //hello, I am b2.

這段代碼同樣利用[[Prototype]]把b1委托給Bar並把Bar委托給Foo,和上一段代碼一模一樣。我們仍然實現了三個對象直接的關聯。

類與對象

web開發一種典型的前端場景:創建UI控件(按鈕,下拉列表等等)。

控件“類”

下面代碼是在不使用任何“類”輔助庫或者語法的情況下,使用純JavaScript實現類風格的代碼:

//父類
function Widget(width,height){
    this.width = width || 50;
    this.height = height || 50;
    this.$elem = null;
};
Widget.prototype.render = function($where){
    if(this.$elem){
        this.$elem.css({
            width:this.width + "px",
            height:this.height + "px"
        }).appendTo($where);
    }
};

//子類
function Button(width,height,label){
    //調用"super"構造函數
    Widget.call(this,width,height);
    this.label = label || "Default";
    this.$elem = $("<button>").text(this.label);
}

//讓子類“繼承”Widget
Button.prototype = Object.create(Widget.prototype);

//重寫render()
Button.prototype.render = function($where){
    Widget.prototype.render.call(this,$where);
    this.$elem.click(this.onClick.bind(this));
}
Button.prototype.onClick = function(evt){
    console.log("Button '"+this.label+"'clicked! ");
};

$(document).ready(function(){
    var $body = $(document.body);
    var btn1 = new Button(125,30,"Hello");
    var btn2 = new Button(150,40,"World");
    btn1.render($body);
    btn2.render($body);
});

ES6的class語法糖

class Widget {
    constructor(width,height) {
        this.width = width || 50;
        this.height = height || 50;
        this.$elem = null;
    }
    render($where){
        if(this.$elem){
            this.$elem.css({
                width:this.width + "px",
                height:this.height + "px"
            }).appendTo($where);
        }
    }
}

class Button extends Widget {
    constructor(width,height,label){
        super(width,height);
        this.label = label || "Default";
        this.$elem = $("<button>").text(this.label);
    }
    render($where){
        super($where);
        this.$elem.click(this.onClick.bind(this));
    }
    onClick(evt){
        console.log("Button '"+this.label+"'clicked! ");
    }
}

$(document).ready(function(){
    var $body = $(document.body);
    var btn1 = new Button(125,30,"Hello");
    var btn2 = new Button(150,40,"World");
    btn1.render($body);
    btn2.render($body);
});

委托控件對象

下面例子使用對象關聯風格委托來更簡單地實現Wiget/Button:

var Widget = {
    init:function(width,height){
        this.width = width || 50;
        this.height = height || 50;
        this.$elem = null;
    },
    insert:function($where){
        if(this.$elem){
            this.$elems.css({
                width:this.width + "px",
                height:this.height + "px"
            }).appendTo($where);
        }
    }
}

var Button = Object.create(Widget);
Button.setup = function(width,height,label){
    //委托調用
    this.init(width,height);
    this.label = label || "Default";
    this.$elem = $("<button>").text(this.label);
}
Button.build = function($where){
    //委托調用
    this.insert($where);
    this.$elem.click(this.onClick.bind(this));
}
Button.onClick = function(evt){
    console.log("Button '"+this.label+"'clicked! ");
}

$(document).ready(function(){
    var $body = $(document.body);
    
    var btn1 = Object.create(Button);
    btn1.setup(125,30,"Hello");
    
    var btn2 = Object.create(Button);
    btn2.setup(150,40,"World");
    
    btn1.build($body);
    btn2.build($body);
})

對象關聯可以更好的支持關注分離(separation of concerns)原則,創建和初始化並不需要合並成一個步驟。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM