『读书笔记』你不知道的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)原则,创建和初始化并不需要合并成一个步骤。