本书是读完 Effective JavaScript之后开的进行的阅读,整理自己需要注意的点。方便以后巩固。
作用域
JavaScript没有块级作用域的相关功能。但是使用with、try...catch可以创建块级作用域
变量提升
提升时函数优先
1 2 3 4 5 6 7 8 9 10 11 12 13
| foo();
var foo;
foo = function() { console.log(2); }
function foo() { console.log(1); }
|
输入结果是1,函数声明优先,同名的var变量会被忽略,但是后面的重新赋值仍然有效,第二次执行foo得到的结果就是2了。
作用域闭包
1 2 3 4 5 6 7 8 9 10 11 12 13
| function foo() { var a = 2;
function bar() { console.log(a); }
bar(); }
foo();
|
这个是闭包吗? 从技术上来讲,也许是。但是确切的说不是。准确的说bar()对a的引用方法是词法作用域的查找规则,这些规则只是闭包的一部分。但确实是非常重要的一部分。
来看一个清晰的闭包例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function foo() { var a = 2;
function bar() { console.log(a); }
return bar; }
var baz = foo();
baz();
|
函数 bar() 的词法作用域能够访问foo() 的内部作用域。然后我们将bar()函数本身当作一个值类型进行传递, 在这个例子中,我们将bar所引用的函数对象本身当作返回值。
动态作用域
JavaScript只有词法作用域,并没有动态作用域
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function foo() { console.log(a); }
function bar() { var a = 3; foo(); }
var a = 2;
bar();
|
词法作用域,让foo在查找a时,只在全局作用域找到 var a = 2, 所以这里显示2。 这里可能会疑惑为什么没有找到bar方法内部, 因为这里只是执行foo方法,如果像下面这样写,才会读到a = 3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function bar() { var a = 3;
function foo() { console.log(a); }
foo(); }
var a = 2;
bar();
|
块级作用域的替代方案
ES5之前块级作用域的解决方案catch或with
1 2 3 4 5 6
| try{ throw 2 } catch(a){ console.log(a); }
console.log(a);
|
let作用域或着let声明还可以这样用
1 2 3 4 5 6
| let(a = 2) { console.log(a); }
console.log(a);
|
与隐式的劫持一个已经存在的作用域不同, let声明会显示的创建一个作用域并与其进行绑定。显示作用域不仅更突出,在代码重构时也表现更加健壮。
但是ES6语法不包含这个我们可以选择,合法的ES6语法,
1 2 3 4 5
| { let a = 2 console.log(a) }
|
还可以选择使用bable转换器 let-er这个转换器然后开启 这个设置项 。这样就可以直接使用了。
关于this
为什么要使用this? this提供了一种更优雅的方式来隐式的传递对象引用,因此可以将API设计的更加简洁并且易于复用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function foo(num) { console.log("foo:" + num); this.count++; }
foo.count = 0;
var i;
for(i = 0; i < 10; i++) { if(i > 5) { foo(i); } }
console.log(foo.count);
|
这里的this并没有指向自身, foo被当作方法调用时,this代表的是函数的接收者。 这里非严格模式下指向的是全局对象,然而全局对象进行++运算结果是NaN,foo.count作为foo这个函数对象的一个属性从来没有改变过。
我们可以这样来改进x
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| function foo(num) { console.log("foo:" + num); data.count++; }
var data = { count: 0; } var i;
for(i = 0; i < 10; i++) { if(i > 5) { foo(i); } }
console.log(foo.count);
|
这样貌似解决了问题,但是忽略了真正的问题,无法理解this的含义和工作原理而是返回了舒适区。
另一种结局方案是使用foo标识符来替代this引用函数对象,但是同样回避了this的问题,并且完全依赖于变量foo的词法作用域
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function foo(num) { console.log("foo:" + num); foo.count++; }
foo.count = 0;
var i;
for(i = 0; i < 10; i++) { if(i > 5) { foo(i); } }
console.log(foo.count);
|
我们可以强制使用this执行foo函数对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function foo(num) { console.log("foo:" + num); data.count++; }
var data = { count: 0; } var i;
for(i = 0; i < 10; i++) { if(i > 5) { foo.call(foo,i); } }
console.log(foo.count);
|
this的绑定规则
默认绑定
这是最常用的函数调用类型,独立函数调用。 这种情况this默认指向了全局对象
1 2 3 4 5 6 7 8
| function foo() { console.log(this.a); }
var a = 2;
foo();
|
隐式绑定
1 2 3 4 5 6 7 8 9 10 11
| function foo() { console.log(this.a); }
var obj = { a: 2, foo: foo }
obj.foo();
|
这种方式加上了对于obj对象的引用,拥有了上下文,当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上细纹对象。
对象属性的引用链只有上一层或者说最后一层在调用位置中起作用。举例来说:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function foo() { console.log(this.a); }
var obj2 = { a: 42, foo: foo }
var obj1 = { a: 2, obj2: obj2 }
obj1.obj2.foo();
|
一个最常见的this对象绑定问题就是隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而吧this绑定到全局对象或着undefined这取决于是否是严格模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function foo() { console.log(this.a); }
var obj = { a: 2, foo: foo }
var bar = obj.foo;
var a = "oops, global";
bar();
|
还有一种,参数传递也是一种隐式赋值,因此我们穿日函数时也会被隐式赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function foo() { console.log(this.a); }
function doFoo(fn) { fn(); }
var obj = { a: 2, foo: foo }
var bar = obj.foo;
var a = "oops, global";
doFoo(obj.foo);
|
同理如果向setTimeout中传递函数对象,同样会丢失this。
显示绑定
显示绑定通常使用 call、apply、bind,这些方式来实现。
1 2 3 4 5 6 7 8 9 10 11
| function foo() { console.log(this.a); }
var obj = { a: 2 }
foo.call(obj);
|
foo.call(…), 我们可以调用foo时强制把它的this绑定到obj上。
可惜的是显示绑定仍然无法解决我们之前提出的丢失绑定的问题。但是显示绑定的一个变种可以帮我们解决。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function foo() { console.log(this.a); }
var obj = { a: 2 }
var bar = function() { foo.call(obj); }
bar();
setTimeout(bar, 100);
bar.call(window);
|
硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function foo(something) { console.log(this.a, something); }
var obj = { a: 2 }
var bar = function() { return foo.apply(obj, arguments); }
var b = bar(3);
console.log(b);
|
另一种使用方法是创建一个可以重复使用的辅助函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function foo(something) { console.log(this.a); return this.a + something; }
function bind(fn, obj) { return function() { fn.apply(obj, arguments); } }
var obj = { a: 2 }
var bar = bind(foo, obj);
var b = bar(3);
console.log(b);
|
由于硬绑定是一种非常常用的模式,所以ES5提供了内置的方法Function.ptototype.bind,它的用法如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function foo(something) { console.log(this.a); return this.a + something; }
var obj = { a: 2 }
var bar = foo.bind(obj);
var b = bar(3);
console.log(b);
|
new绑定
虽然JavaScript中也有new操作符,使用方法看起来跟那些面向类的语言意义,绝大多数开发者都认为JavaScript中的new的机制也跟那些语言一样,然而JavaScript中的new机制实际上跟面向类的语言完全不同。
- 构造函数 首先JavaScript中的构造函数只是使用new操作符调用的函数,它们并不会属于某个类,也不会实例化一个类,实际上它们甚至不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。 函数被使用new操作符调用成为构造函数调用,实际上不存在所谓的构造函数,只有对于函数的构造调用。
1 2 3 4 5 6 7 8
| function foo() { this.a = a; }
var bar = new foo(2);
console.log(bar.a);
|
我们创造了一个新的对象,并把它绑定到foo调用中的this上。
优先级
显示绑定比隐式绑定优先级要高, new绑定比隐式绑定优先级要高, 但是new绑定会得到一个新的对象。
判断this
函数是否在new中调用, 如果是的话this绑定的是新创建的对象。
函数是否通过call,apply,或者硬绑定调用,如果是的话this是指定的对象
函数是否在某个上下文对象中(隐式绑定)如果是的话,this绑定的是上下文对象。
如果都不是的话,使用默认绑定。如果在严格模式下绑定到undefined, 否则绑定到全局对象。
被忽略的this
如果你把null或undefined作为this绑定对象传入call,apply或者bind,这些值会在调用时被忽略,实际应用的是默认绑定规则
1 2 3 4 5 6 7 8 9
| function foo() { console.log(this.a); }
var a = 2;
foo.call(null);
|
使用null忽略this可能产生一些副作用,如果某个函数确实使用了this(比如第三方库的一个函数), 那默认绑定规则会把this绑定到全局对象。这将导致不可预计的后果。
所以我们使用一个更安全的空对象
在JavaScript中创建一个空对象最简单的方法是Obejct.create(null), 它和{}很像,但是不会创建Object.ptototype,所以它比{}更空
1 2 3 4
| option + o var ø = Object.create(null);
|
间接引用
还有一个需要注意的情况你可能创建一个函数的间接引用,调用这个函数会应用默认绑定规则
间接引用最容易在赋值时发生
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function foo() { console.log(this.a); }
var a = 2;
var o = { a: 3, foo: foo }
var p = { a: 4 }
o.foo();
(p.foo = o.foo)();
|
赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo() 而不是p.foo()或者o.foo();
this词法
self = this和箭头函数看起来都可以取代bind但本质上想取代的是this机制。如果你经常编写this风格的代码,但是绝大部份都会是用self = this,或者箭头函数,那么你或许应该
- 只使用此法作用域并且完全抛弃错误this风格代码;
- 完全采用this风格,在必要时使用bind,避免使用self = this 和 箭头函数。
对象
在对象中,属性名永远都是字符串。使用非String作为属性名它首先会被转成一个字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13
| var myObject = {}
myObject[true] = "foo" myObject[3] = "bar" myObject[myObject] = "baz"
myObject["true"] = "foo" myObject["3"] = "bar" myObject["[object OBject]"] = "baz"
|
对象复制
1 2 3 4 5 6 7 8
| 浅复制 var newObj = Object.assign({}, myObject);
深复制 对于JSON安全的对象来说有一种巧妙的复制方法
var newObj = JSON.parse(JSON.stringify(someObj));
|
属性描述符
1 2 3 4 5 6 7 8 9 10
| var myObject = { a: 2 }
Object.getOwnPropertyDescriptor(myObject, "a");
// {value: 2, writable: true, enumerable: true, configurable: true}
如果configurable: false, 则不可以再配置,也不可以被删除
|
对象常量
结合writable: false 和configurable: false就可以创建一个真正的常量属性(不可以重新定义,修改,删除)
1 2 3 4 5 6 7 8
| var myObject = {};
Object.defineProterty(myObject, "FAVORITE_NUMBER", { value: 42, writable: false, configurable: false });
|
禁止扩展
如果你想禁止一个对象添加新属性并且保留已有属性,可以使用Object.preventExtensions(…)
1 2 3 4 5 6 7 8 9 10
| var myObject = { a: 2 }
Object.preventExtensions(myObject);
myObject.b = 3;
myObject.b;
|
在非严格模式下,创建属性b会静默失败,在严格模式下,将会抛出TyperError错误。
密封
Object.seal(…)会创建一个”密封”对象,这个方法实际会在一个现有对象上调用Object.preventExtensions并把所有现有属性标记为configurable: false.
所有密封后不能添加新属性,也不能重新配置和删除任何现有属性(虽然可以修改属性的值)
冻结
Object.freeze(...),会创建一个冻结对象,这个方法是在现有对象调用了Object.seal并把所有数据访问属性标记为 writable: false, 这样就无法修改它们的值
Getter和Setter
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| var myObject = { get a() { return 2; } }
Object.defineProperty(myObject, "b", { get: function() { return this.a * 2; }, enumerable: true
});
|
通常get和set是成对出现的,只定义一个的话通常会产生意料之外的行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| var myObject = { get a() { return this._a_; }
set a(val) { return this._a_ = val * 2; } }
myObject.a = 2;
myObject.a;
|
存在性
前面说过 如果本身对象中有值为undefiend的属性obj.a, 去获取该值得到的结果跟去获取一个不存在的属性结果是一样的,比如obj.b, 那么如果判断一个属性在对象中是否存在呢
1 2 3 4 5 6 7 8 9 10 11 12
| var myObject = { a: 2 }
(a in myObject); // true (b in myObject); // false
myObject.hasOwnProperty("a"); // true myObject.hasOwnProperty("b"); // false
|
in操作符会检查属性是否存在对象及其[[prototype]]原型中,相比之下hasOwnProperty只会检查属性是否存在myObject对象中。
要注意的是in操作符检查的是容器内是否有某个值,实际上检查的是某个属性名是否存在。
1 2 3
| 4 in [2, 4, 6]; // false 这个数组包含的属性名其实是0 ,1,2
|
是否可通过for…in操作符来枚举可以通过定义属性是设定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| var myObject = {};
Object.defineProperty(myObject, "a", { enumerable: true, value: 2 });
Object.defineProperty(myObject, "b", { enumerable: false, value: 3 });
myObject.b; //3 {"b" in myObject}; //true myObject.hasOwnProperty("b"); //true
for(var k in myObject) { console.log(k, myObject[k]); }
// "a" 2
还可以通过另一种方式来区分属性是否可枚举
propertyIsEnumerable 会检查属性是否存在于对象中(而不是在原型链中)并且满足enumerable: true Object.keys会返回一个数组,包含所有可枚举属性 Object.getOwnPropertyNames 返回一个数组包含所有属性无论是否可枚举
myObject.propertyIsEnumerable("a"); // true
myObject.propertyIsEnumerable("b"); // false
Object.keys(myObject); // ["a"]
Object.getOwnPropertyNames(myObject); // ["a", "b"]
|
Object.keys 和Object.getOwnPropertyNames 都不回查找原型链,只会查找对象直接包含的属性
遍历
我们通常会通过for...in来遍历一个对象,但是这样便利对象属性时的顺序是不确定的,在不同的JavaScript引擎中可能不一样,因此在不同的环境中需要保持一致性时,一定不要相信任何观察到顺序,它们是不可靠的
那么如何直接遍历值而不是数组下标呢
1 2 3 4 5 6 7
| var myArray = [1, 3, 5];
for(var v of myArray) { console.log(v); }
|
因为数组有内置的@iterator, 因此for…of可以直接应用到数组上。
我们可以使用内置的@@iterator来手动遍历数组, ES6中我们使用Symbol.iterator来获取对象的@@iterator内部属性
1 2 3 4 5 6 7 8 9 10 11 12
| var myArray = [1, 3, 5];
var it = myArray[Symbol.iterator]();
it.next(); // {value: 1, done: false}
it.next(); // {value: 3, done: false}
it.next(); // {value: 5, done: false}
it.next(); // {value: undefined, done: undefined}
|
和数组不同普通的对象没有内置的@@iterator,所以无法自动完成for…of遍历,之所以这样做有许多非常复杂的原因, 简单来说,这样做是为了避免影响未来的对象类型.
当然我们可以给任何想遍历的对象定义@@iterator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| var myObject = { a: 2, b: 3 };
Object.defineProperty(myObject, Symbol.iterator, { enumerable: false, writable: false, configurable: true, value: function() { var o = this; var idx = 0; var ks = Object.keys(o);
return { next: function() { return { value: o[ks[idx++]]; done: (idx > ks.length) } } } } });
这样手动遍历
var it = myObject[Symbol.iterator](); it.next(); it.next(); it.next();
for(var v of myObject) { console.log(v); }
|
原型风格继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| function Foo(name) { this.name = name; }
Foo.prototype.myName = function() { return this.name; }
function Bar(name, label) { Foo.call(this, name);
this.label = label; }
Bar.prototype = Object.create(Foo.prototype);
Object.setPrototypeOf(Bar.prototype, Foo.prototype);
Bar.prototype.myLabel = function() { return this.label; }
var a = new Bar("a", "obj a");
a.myName();
a.myLabel();
该方法比较有用,Foo.prototype是否出现在a的[[prototype]]链中 Foo.prototype.isPrototypeOf(a);
|
委托
基于委托实现与上面相同功能的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| Foo = { init: function(who) { this.me = who; }, identify: function() { return "I am " + this.name; } }
Bar = Object.create(Foo);
Bar.speak = function() { alert("Hello, " + this.identify() + " . "); }
var b1 = Object.create(Bar); b1.init("b1");
var b2 = Object.create(Bar); b2.init("b2");
b1.speak(); b2.speak();
|
通过比较发现, 对象关联风格的代码更加简洁,因为这种风格的代码只关注一件事, 对象之间的关联关系。
更简洁的设计
一个关于登陆验证器的设计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| function Controller() { this.errors= []; }
Controller.prototype.showDialog = function(title, msg) { }
Controller.prototype.success = function(msg) { this.showDialog("success", msg); }
Controller.prototype.failure = function(err) { this.errors.push(err);
this.showDialog("Error", err); }
function LoginController() { Controller.call(this); }
LoginController.prototype = Object.create(Controller.prototype);
LoginController.prototype.getUser = function() { return document.getElementById("login_username").value; }
LoginController.prototype.getPassword = function() { return document.getElementById("login_password").value; }
LoginController.prototype.validateEntry = function(user, pw) { user = user || this.getUser(); pw = pw || this.getPassword();
if(!(user && pw)) { return this.failure("Please enter a username & password"); } else if(pw.length < 5) { return this.failure("Password must be 5+ character!"); }
return true; }
LonginController.prototype.failure = function() { Controller.prototype.failure.call(this, "Login invalid: " + err); }
function AuthController(login) { Controller.call(this);
this.login = login; }
AuthController.prototype = Object.create(Controller.prototype);
AuthController.prototype.server = function(url, data) { return $.ajax({ url: url, data: data }); }
AuthController.prototype.checkAuth = function() { var user = this.login.getUser(); var pw = this.login.getPassword();
if(this.login.validateEntry(user, pw)) { this.server("/check-auth", { user: user, pw: pw }) .then(this.success.bind(this)); .fail(this.failure.bind(this)); } }
AuthController.prototype.success = function() { Controller.prototype.success.call(this, "Authenticated;"); }
AuthController.prototype.failure = function(err) { Controller.prototype.failure.call(this, "Auth failed: " + err); }
var auth = new AuthController(new LoginController());
auth.checkAuth();
|
反类,我们可以使用对象关联风格的行为委托来实现更简单的设计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| var LoginController = { errors: [], getUser: function() { return document.getElementById("login_username").value; },
getPassword: function() { return document.getElementById("login_password").value; }, validateEntry: function(user, pw) { user = user || this.getUser(); pw = pw || this.getPassword();
if(!(user && pw)) { return this.failure("Please enter a username & password"); } else if(pw.length < 5) { return this.failure("Password must be 5+ character!"); }
return true; }, showDialog: function() { }, failure: function() { this.errors.push(err); this.showDialog("Error", "Login invalid " + err); }, success: function(msg) { this.showDialog("success", msg); }
}
var AuthController = Object.create(LoginController);
AuthController.errors = [];
AuthController.checkAuth = function() { var user = this.getUser(); var pw = this.getPassword();
if(this.validateEntry(user, pw)) { this.server("/check-auth", { user: user, pw: pw }) .then(this.accepted.bind(this)) .fail(this.rejected.bind(this)); } }
AuthController.server = function(url, data) { return $.ajax({ url: url, data: data }) }
AuthController.accepted = function() { this.showDialog("Success", "Authenticated!"); }
AuthController.rejected = function(err) { this.failure("Auth Failed: " + err); }
AuthController.checkAuth();
|
这种模式我们只需要两个实体LoginController 和 AuthController
总结: 我们用一种极其简单的设计实现了同样的功能, 这既是对象关联风格代码和行为委托设计模式的力量
反词法
ES6简洁与法有一个非常小但重要的缺点
1 2 3 4 5 6 7 8 9 10 11 12 13
| var Foo = { bar() {}, baz: function baz() {} }
去掉语法糖
var Foo = { bar: function() {} baz: function baz() {} }
|
由于函数对象本身没有标识符, 所以bar()的缩写形式实际上会变成一个匿名函数表达式并赋值给bar属性。相比之下 具名函数表达式会额外的给.baz属性附加一个词法名称标识符baz。
匿名函数没有name标识符会导致
- 调用栈更难追踪
- 自我引用(递归, 事件绑定和解除绑定)更难
- 代码稍微难理解
这里只有第二个缺点无法避免,因此使用简洁语法一定要小心这一点。 如果你需要自我引用的话,那最好使用传统的具名函数表达式来定义对应的函数,不要使用简洁语法。
再说一次,我们认为JavaScript种的对象关联比类风格代码更为简洁(而且功能相同)