前言
文章只記錄理解以及容易遺忘的知識點,
詞法作用域、塊作用域
詞法作用域
詞法作用域:簡單的說,詞法作用域就是定義在詞法階段的作用域,換句話說,詞法作用域就是在你寫代碼時將變數和塊作用域寫在哪里來決定的,因此在詞法分析器處理代碼時會保持作用域不變(大部分情況是這樣的),
當然有一些欺騙詞法作用域的方法,這些方法在詞法分析器處理后依然可以改變作用域,
欺騙詞法作用域的方法有:
- 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:hippofoo.awesome(); //LET ME INTRODUCE:HIPPO“foo”和“bar”模塊都是通過一個回傳公共API的函式來定義的,“foo”甚至接受“bar”的示例作為依賴引數,并能相應地使用它,
未來的模塊機制
bar.jsfunction 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:hippofoo.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: undefinedvar 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); //5delete 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); //2console.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) } } } }});//手動遍歷myObjectvar 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遍歷myObjectfor(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')); //falseconsole.log(myObject.hasOwnProperty('b')); //trueconsole.log(myObject.hasOwnProperty('c')); //trueconsole.log(myObject.a); //2console.log(myObject.b); //3console.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);}//讓子類“繼承”WidgetButton.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)原則,創建和初始化并不需要合并成一個步驟,
<style></style><style></style><style></style>
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/83658.html
標籤:JavaScript
下一篇:JavaScript箭頭函式
