第一部分 作用域和闭包
第1章 作用域是什么
LHS:变量出现在左侧,目的是对变量进行赋值
RHS:变量出现在右侧,目的获取变量的值
ReferenceError 同作用域判别失败相关,而 TypeError 则代表作用域判别成功了,但是对结果的操作是非法或不合理的。
第2章 词法作用域
词法作用域就是定义在词法阶段的作用域。词法作用域是由在写代码时,将变量和块作用域写在哪里决定的,因此当词法分析器处理代码时会保持作用域不变。
eval和with会在运行时修改或创建新的作用域。如果代码中大量使用 eval(..) 或 with,那么运行起来一定会变得非常慢。
这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认 为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。
第3章 函数作用域和块作用域
函数作用域
// 第一段代码
var a = 2;
function foo() {
var a = 3; console.log( a );
}
foo();
console.log( a );
// 第二段代码
var a = 2;
(function foo() {
var a = 3; console.log( a );
})();
console.log( a ); // 2
区分函数声明和表达式最简单的方法是看 function 关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果 function 是声明中 的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
比较一下前面两个代码片段。第一个片段中 foo 被绑定在所在作用域中,可以直接通过foo() 来调用它。第二个片段中 foo 被绑定在函数表达式自身的函数中而不是所在作用域中。
换句话说,(function foo(){ .. })作为函数表达式意味着foo只能在..所代表的位置中 被访问,外部作用域则不行。foo 变量名被隐藏在自身中意味着不会非必要地污染外部作用域。
try/catch
try/catch 的 catch 分句会创建一个块作用域,其中声明的变量仅在 catch 内部有效。
try {
undefined(); // 执行一个非法操作来强制制造一个异常
}
catch (err) {
console.log( err ); // 能够正常执行!
}
console.log( err ); // ReferenceError: err not found
let
let 关键字可以将变量绑定到所在的任意作用域中(通常是{...}内部)。换句话说,let为其声明的变量隐式地了所在的块作用域。
var foo = true;
if (foo) {
let bar = foo * 2;
bar = something( bar ); console.log( bar );
}
console.log( bar ); // ReferenceError
const
同样可以用来创建块作用域变量,但其值是固定的 (常量)
第4章 提升
回忆一下,引擎会在解释JavaScript 代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的 声明,并用合适的作用域将它们关联起来。
因此,变量和函数在内的所有声明都会在任何代码被执行前首先被处理
a = 2;
var a;
console.log( a );
处理顺序
var a;
a = 2;
console.log( a );
只有声明会被提前,而赋值和其他运行逻辑会留在原地
foo(); // 不是 ReferenceError,而是 TypeError!
var foo = function bar() { // ...
};
第5章 闭包
function foo() {
var a = 2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn(); // 妈妈快看呀,这就是闭包!
}
在执行函数fn时访问到了foo的内部变量a,形成闭包
把内部函数 baz 传递给 bar,当调用这个内部函数时(现在叫作 fn),它涵盖的 foo() 内部
作用域的闭包就可以观察到了,因为它能够访问 a。
闭包的另一种形式:赋值给全局变量
var fn;
function foo() {
var a = 2;
function baz() { console.log( a );
}
fn = baz; // 将 baz 分配给全局变量 }
function bar() {
fn(); // 妈妈快看呀,这就是闭包!
}
foo();
bar(); // 2
另一种形式的闭包
function wait(message) {
setTimeout( function timer() {
console.log( message ); // message依然可以用
},1000 ); }
wait( "Hello,closure!" );
wait(..) 执行 1000 毫秒后,它的内部作用域并不会消失,timer 函数依然保有 wait(..)作用域的闭包。
在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包!
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j ); // 闭包
},j*1000 );
})( i );
}
块级作用域
for (var i=1; i<=5; i++) {
let j = i; // 是的,闭包的块作用域!
setTimeout( function timer() {
console.log( j );
},j*1000 );
}
闭包的使用案例--模块
var foo = (function CoolModule() {
var something = "cool";
var another = [1,2,3];
function doSomething() { console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,doAnother: doAnother
}; })();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
模块有两个主要特征:(1)为创建内部作用域而调用了一个包装函数;(2)包装函数的返回 值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。
this词法
var obj = {
id: "awesome",cool: function coolFn() {
console.log( this.id );
} };
var id = "not awesome"
obj.cool(); // 酷
setTimeout( obj.cool,100 ); // 不酷
cool函数丢失了和this之间的绑定。取一个self=this。或者可以直接用ES6的箭头函数
var obj = {
count: 0,cool: function coolFn() {
// var self = this;
if (this.count < 1) {
setTimeout( timer => {
this.count++;
console.log( "awesome?" );
},100 );
} }
};
obj.cool(); // 酷吧?
或者使用bind(this)
var obj = {
count: 0,cool: function coolFn() { if (this.count < 1) {
setTimeout( function timer(){
this.count++; // this 是安全的 // 因为 bind(..)
console.log( "more awesome" ); }.bind( this ),100 ); // look,bind()!
} }
};
obj.cool(); // 更酷了。
第二部分 this和对象原型
第1章 关于this
误解
1.指向自身
this并不是指向函数自身。而是指向函数执行时所在的那个词法作用域。
要想使得this指向函数自身,可以使用foo.call(foo,参数)
的形式
2.它的作用域
第二种常见的误解是,this指向函数的作用域
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); // ReferenceError: a is not defined
那么this到底是什么?
this是在运行时进行绑定的,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录。这个记录会包含函数在哪里被调用、函数的调用方法、传参等信息。this就是记录的起重工一个属性。
第2章 this全面解析
绑定规则
默认绑定
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用 默认绑定
值得注意的一点是,如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定 到 undefined:
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined
隐式绑定
function foo() {
console.log( this.a );
}
var obj = {
a: 2,foo: foo
};
obj.foo(); // 2
几种隐式丢失现象
function foo() {
console.log( this.a );
}
var obj = {
a: 2,foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops,global"; // a 是全局对象的属性
bar(); // "oops,global"
虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,foo: foo
};
var a = "oops,global"; // a 是全局对象的属性 doFoo( obj.foo ); // "oops,global"
显示绑定
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
通过 foo.call(..),我们可以在调用 foo 时强制把它的 this 绑定到 obj 上。
new绑定
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
判断this
- 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
var bar = new foo()
- 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是 指定的对象。
var bar = foo.call(obj2)
- 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。
var bar = obj1.foo()
- 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到 全局对象。
var bar = foo()
第3章 对象
基本类型
- string
- number
- boolean
- null
- undefined
- symbol
- object
前五种是简单基本类型,本身并不是对象
内置对象
- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
console.log( strPrimitive.length ); // 13
console.log( strPrimitive.charAt( 3 ) ); // "m"
使用以上两种方法,我们都可以直接在字符串字面量上访问属性或者方法,之所以可以这 样做,是因为引擎自动把字面量转换成 String 对象,所以可以访问属性和方法。
var myObject = { a: 2
};
myObject.a; // 2
myObject["a"]; // 2
.a 语法通常被称为“属性访问”,["a"] 语法通常被称为“键访问”。
这两种语法的主要区别在于 . 操作符要求属性名满足标识符的命名规范,而 [".."] 语法 可以接受任意 UTF-8/Unicode 字符串作为属性名
属性描述符
- writable
决定是否可以修改属性的值。如果为false,非严格模式下的修改将失败
- configurable
只要元素时可配置的,就可以使用defineProperty()来修改属性描述符。configurable为false还会禁止删除这个属性
- enumerable
控制的是属性是否会出现在对象的属性枚举中,比如for...in循环
不变性
1.对象常量
结合writable:false
和configurable:false
就可以创建一个真正的常量属性
2.禁止扩展
Object.preventExtensions()
禁止一个对象添加新属性并八六已有属性
3.密封
Object.seal()
这个方法实际上会在一个现有对象上调用 Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false
4.冻结Object.freeze(..)
会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal(..) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值。
第4章 混合对象“类”
多态并不表示子类和父类有关联,子类得到的只是父类的一份副本。类的继承其实就是复制。
多重集成
A-->B
A-->C
B-->D
C-->D
如果A有drive方法并且B和C都重写了这个方法,那么D引用时选择哪个版本呢?
Javascript并不提供“多重集成”功能。
混入
显示混入
由于 JavaScript 不会自动实现 Vehicle 到 Car 的复制行为,所以我们需要手动实现复制功能。
// 非常简单的 mixin(..) 例子 :
function mixin( sourceObj,targetObj ) {
for (var key in sourceObj) {
// 只会在不存在的情况下复制
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
var Vehicle = {
engines: 1,ignition: function() {
console.log( "Turning on my engine." );
},drive: function() {
this.ignition();
console.log( "Steering and moving forward!" );
}
};
var Car = mixin( Vehicle,{
wheels: 4,drive: function() {
Vehicle.drive.call( this );
console.log(
"Rolling on all " + this.wheels + " wheels!");
}
} );
隐式混入
var Something = {
cool: function() {
this.greeting = "Hello World";
this.count = this.count ? this.count + 1 : 1; }
};
Something.cool();
Something.greeting; // "Hello World"
Something.count; // 1
var Another = {
cool: function() {
// 隐式把 Something 混入 Another
Something.cool.call( this ); }
};
Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1(count 不是共享状态)
小结
类是一种设计模式。许多语言提供了对于面向类软件设计的原生语法。JavaScript 也有类似的语法,但是和其他语言中的类完全不同。
类意味着复制。
混入模式(无论显式还是隐式)可以用来模拟类的复制行为,但是通常会产生丑陋并且脆 弱的语法,比如显式伪多态(OtherObj.methodName.call(this,...))
,这会让代码更加难 懂并且难以维护。
第5章 原型
[[prototype]]
相当于隐式原型__proto__
属性设置和屏蔽
myObject.foo = "bar";
- 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性(参见第3章)并且没 有被标记为只读(writable:false),那就会直接在 myObject 中添加一个名为 foo 的新 属性,它是屏蔽属性。
- 如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable:false),那么 无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会 抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
- 如果在[[Prototype]]链上层存在foo并且它是一个setter(参见第3章),那就一定会 调用这个 setter。foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义 foo 这 个 setter。
var anotherObject = { a:2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false
myObject.a++; // 隐式屏蔽!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true
Object.create()的polyfill代码
if (!Object.create) {
Object.create = function(o) {
function F(){}
F.prototype = o;
return new F();
};
}
第6章 行为委托
类理论
假设我们需要在软件中建模一些类似的任务(“XYZ”、“ABC”等)。
如果使用类,那设计方法可能是这样的:定义一个通用父(基)类,可以将其命名为 Task,在 Task 类中定义所有任务都有的行为。接着定义子类 XYZ 和 ABC,它们都继承自 Task 并且会添加一些特殊的行为来处理对应的任务。
非常重要的是,类设计模式鼓励你在继承时使用方法重写(和多态),比如说在 XYZ 任务 中重写 Task 中定义的一些通用方法,甚至在添加新行为时通过 super 调用这个方法的原始 版本。你会发现许多行为可以先“抽象”到父类然后再用子类进行特殊化(重写)。
伪代码
class Task {
id;
// 构造函数 Task()
Task(ID) { id = ID; }
outputTask() { output( id ); }
}
class XYZ inherits Task {
label;
// 构造函数 XYZ()
XYZ(ID,Label) { super( ID ); label = Label; } outputTask() { super(); output( label ); }
}
class ABC inherits Task {
// ...
}
委托理论
首先你会定义一个名为 Task 的对象(和许多 JavaScript 开发者告诉你的不同,它既不是类 也不是函数),它会包含所有任务都可以使用(写作使用,读作委托)的具体行为。接着, 对于每个任务(“XYZ”、“ABC”)你都会定义一个对象来存储对应的数据和行为。你会把 特定的任务对象都关联到 Task 功能对象上,让它们在需要的时候可以进行委托。
Task = {
setID: function(ID) { this.id = ID; },outputID: function() { console.log( this.id ); }
};
// 让XYZ委托Task
XYZ = Object.create( Task );
XYZ.prepareTask = function(ID,Label) {
this.setID( ID );
this.label = Label;
};
XYZ.outputTaskDetails = function() {
this.outputID();
console.log( this.label );
};
// ABC = Object.create( Task );
// ABC ... = ...
在这段代码中,Task 和 XYZ 并不是类(或者函数),它们是对象。XYZ 通过Object. create(..)
创建,它的 [[Prototype]] 委托了 Task 对象。
委托行为意味着某些对象(XYZ)在找不到属性或者方法引用时会把这个请求委托给另一个对象(Task)。
小结
行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。JavaScript 的 [[Prototype]] 机制本质上就是行为委托机制。
原文链接:https://www.f2er.com/js/415896.html