你不知道的JS学习笔记
第一部分:作用域和闭包
第1章 作用域是什么
1.1 编译原理
尽管通常将JavaScript
归类为“动态”或“解释执行”语言,但事实上它是一门编译语言。与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植。
在传统编译语言的流程中,程序中一段源代码在执行之前会尽力三个步骤,即“编译”:
- 分词/词法分析(Tokenizing/Lexing)
这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块被成为词法单元。
var a = 2; //分解成 var、a、=、2、;。//空格是否会被当作词法单元,取决于空格在这门语言中是否具有意义。复制代码
分词和词法分析主要差异在于词法单元的识别是通过有状态还是无状态的方式进行的。
简单的说,如果词法单元生成器在判断a
是一个独立的词法单元还是其他词法单元的一部分时, 调用的是有状态的解析规则,那么这个过程就被称为词法分析。
- 解析/语法分析(Parsing)
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree, AST)。
- 代码生成
将AST转换为可执行代码的过程被称为代码生成。
抛开具体细节,简单来说就是有某种方法可以将var a = 2;
的AST转化为一组机器指令,用来创建一个叫作a
的变量(包括分配内存等),并将一个值储存a
中。
JS引擎要复杂的多。例如,在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。
1.2 理解作用域
作用域:负责收集并维护由所有的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
-
编译器
- LHS查询(目的是对变量赋值)
- RHS查询(获取变量的值)
当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询。
1.3 作用域嵌套(由内到外查找)
1.4 异常
如果RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferenceError
异常。
相较之下,当引擎执行LHS查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下。(严格模式禁止自动或隐式地创建全局变量)
ReferenceError
同作用域判别失败相关,而TypeError
则代表作用域判别成功了,但是对结果的操作时非法或不合理的。
第2章 词法作用域
2.1 词法阶段
词法作用域就是定义在词法阶段的作用域。
作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符遮蔽了外部的标识符)。
全局变量会自动称为全局对象(比如浏览器中的window
对象)的属性,因此可以不直接通过全局对象的词法名称,而是间接的通过对全局对象属性的引用来对其访问。例如:
window.a复制代码
2.2 欺骗词法
欺骗词法作用域会导致性能下降。(不推荐使用)
- eval- with复制代码
第3章 函数作用域和块作用域
函数作用域的含义是指, 属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。
3.1 隐藏内部实现(阻止对私有变量或私有函数的访问)
隐藏变量和函数是从最小特权原则中引申出来的,也叫最小授权或最小暴露原则。在软件设计中,应该最小限度地暴露必要内容。而将其他内容都“隐藏”起来,比如某个模块或对象的API设计。
-
规避冲突
- 全局命名空间(用对象的属性进行访问)
var MyReallyCoolLibrary = { awesome: "stuff", doSomething: function() { //... } doAnotherThing: function() { //... }};复制代码
- 模块管理
3.2 函数作用域
区分函数声明和表达式最简单的方法是看
function
关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果function
是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
匿名函数的几个缺点:
- 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
- 如果没有函数名,当函数需要引用自身时只能使用已经过期的
arguments.callee
, 比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑自身。 - 匿名函数省略了对于代码可读性 / 可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。
3.3 块作用域
块作用域时一个用来对之前的最小授权原则进行扩展的工具,将代码从函数中隐藏信息扩展为在块中隐藏信息。
3.3.1 with
3.3.2 try/catch
try{ undefined(); //执行一个非法操作来强制制造一个异常}catch( err ){ console.log( err ); // 能够正常执行}console.log( err ); //ReferenceError: err not found复制代码
3.3.3 let
只要声明是有效的,在声明中的任意位置都可以使用{...}
括号来为let
创建一个用于绑定的块。
{ console.log( bar ); //ReferenceError! let bar = 2;}复制代码
-
- 垃圾收集
function process(data) { // 在这里做点有趣的事情}var someReallyBigData = {...};process( someReallyBigData );var btn = document.getElementById( "my_button" );btn.addEventListener("click", function click(evt) { console.log("button clicked");}, /*capturingPhase=*/false);复制代码
-
let
循环
3.3.4 const
第四章 提升
任何声明在某个作用域内的变量,都将附属于这个作用域。
4.1 编译器的正确思考思路
变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
var a = 2;
不是一个声明,在JavaScript
实际上会将其看成两个声明:
var a;
和 a = 2;
。第一个定义声明是在编译阶段进行的。第二个赋值声明会留在原地等待执行阶段。
只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。
var a; //编译阶段a = 2; //执行console.log( a );复制代码
foo(); //TypeError 类型(执行时)bar(); //ReferenceError 引用(未定义)var foo = function bar() { //...}复制代码
提升之后 =>
var foo;foo();bar();foo = function () { var bar = ...self...; //...}复制代码
4.2 函数优先
函数声明和变量声明都会被提升,函数首先被提升,然后才是变量。
foo(); //1 !!!var foo;function foo() { console.log( 1 );}foo = function() { console.log( 2 );}复制代码
一个普通块内部的函数声明通常会被提升到所在作用域的顶部,这个过程不会像下面的代码暗示的那样可以被条件判断所控制。
foo(); //TypeError: foo is not a functionvar a = true;if (a) { function foo() { console.log("a"); }}else{ function foo() { console.log("b"); }}复制代码
应该尽可能避免在块内部声明函数。
第5章 作用域闭包
理解闭包可以看作是某种意义上的重生
5.1 实质
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
function foo() { var a = 2; function bar() { console.log( a ); } return bar}var baz = foo();baz(); //2, 唔,这就是闭包的效果 复制代码
闭包的神奇之处在于可以阻止引擎垃圾回收器对某内部作用域进行回收。
//直接传递函数function foo() { var a = 2; function baz() { console.log( a ); //2 } bar( baz );}function bar(fn) { fn(); //闭包, 外部调用baz,可以访问a}foo();复制代码
//间接传递函数var fn;function foo() { var a = 2; function baz() { console.log( a ); } fn = baz; //将baz分配给全局变量}function bar() { fn(); //闭包, 外部调用baz,可以访问a}foo();bar(); //2复制代码
5.2 闭包使用场景
function wait(message) { setTimeout( function timer() { console.log( message ); }, 1000)}wait( "Hello, closure!" );复制代码
在引擎内部,内置的工具函数setTimeout
持有对一个参数的引用,这个参数也许叫作fn
和func
,或者其他类型的名字。引擎会调用这个函数,在例子中就是内部的Timer
函数,而词法作用域在这个过程中保持完整。
在定时器,事件监听器,Ajax
请求,跨窗口通信,Web Workers
或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包。
闭包就是“一块特定的作用域” --- 个人理解
5.3 循环和闭包
for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 );}复制代码
我们试图在每个迭代时都会给自己“捕获”一个i
的副本。但根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i
。
IIFE
for (var i=1; i<=5; i++) { (function(j) { setTimeout( function timer() { console.log( j ); }, i*1000 ) })(i);}复制代码
let
for (let i=1; i<=5; i++) { setTimeout( function timer() { console.log( j ); }, i*1000 )}复制代码
5.4 模块
5.4.1 模块介绍
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 }}var foo = CoolModule();foo.doSomething(); //coolfoo.doAnother(); //1 ! 2 ! 3复制代码
这个模式在Javascript
中称为模块。
从模块中返回一个实际的对象并不是必须的,也可以直接返回一个内部函数。jQuery
就是一个很好的例子,jQuery
和$
标识符就是jQuery
模块的公用API
,但它们本身都是函数(由于函数也是对象,它们本身也可以拥有属性)。
模块模式需要具备两个条件。
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
单例模式var foo = (function CoolModule(id) { function change() { //修改公共API publicAPI.identify = identify2; } function identify1() { console.log( id ); } function identify2() { console.log( id.toUpperCase() ); } var publicAPI = { change: change, identify: identify1 }; return publicAPI;})(" foo module ");foo.identify(); //foo modulefoo.change(); // 1 ! 2 ! 3foo.identify(); //FOO MODULE复制代码
5.4.2 现代的模块机制
5.4.3 未来的模块机制(ES6)
//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( 'rhino' )); //Let me introduce: rhinofoo.awesome(); //LET ME INTRODUCE: HIPPO复制代码
第二部分:this和对象原型
第1章 关于this
任何足够先进的技术都和魔法无异。--- Arthur C.Clarke
使用this
可以自动引用合适的上下文对象,而不需要显式传递上下文对象,这样可以让代码更简洁。
1.1 关于this的误解:
- 指向自身
function foo() { console.log( "foo: " + num ); //记录count被调用的次数 this.count++; //无意中创建了一个全局变量,它的值为NaN, this(默认)指向全局。}foo.count = 0;var i;for(i=0; i<10; i++) { if(i > 5) { foo( i ); }}// foo: 6// foo: 7// foo: 8// f00: 9console..log(foo.count); // 0 为什么是0?复制代码
改进:
function foo() { console.log( "foo: " + num ); //记录count被调用的次数 //注意,在当前的调用方式下(参见下方代码),this确实指向foo}foo.count = 0;var i;for(i=0; i<10; i++){ if(i > 5) { //使用call(..)可以确保this指向函数对象foo本身 foo.call( foo, i ) }}// foo: 6// foo: 7// foo: 8// f00: 9console..log(foo.count); // 4复制代码
- 它的作用域(this指向函数的作用域)
this在任何情况下都不指向函数的词法作用域。 在
Javascript
内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域"对象"无法通过Javascript
代码访问,它存在于Javascript
引擎内部。
function foo() { var a = 2; this.bar();}function bar() { console.log( this.a );}foo(); //ReferenceError: a is not defined复制代码
每当你想把this
和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的。
1.2 总结
this
实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
第2章 this全面解析
2.1 调用位置---分析调用栈
利用浏览器的调式工具
2.2 绑定规则
- 默认绑定(独立函数调用)
function foo() { console.log( this.a );}var a = 2;// 无任何修饰调用,默认绑定[非严格模式]foo(); //2复制代码
严格模式
function foo() { "use strict"; console.log( this.a );}var a = 2;// 严格模式foo(); // TypeError: this is not defined复制代码
虽然
this
的绑定规则完全取决于调用位置,但是只有foo()
运行在非strict mode
下时,默认绑定才能绑定到全局对象;在严格模式下调用foo()
则不影响默认绑定:
function foo() { console.log( this.a );}var a = 2;(function(){ "use strict"; foo();//2})();复制代码
- 隐式绑定
考虑的规则: 调用的位置是否具有上下文对象。
function foo() { console.log( this.a );}var obj = { a: 2, foo: foo};obj.foo(); //2 函数被调用时obj对象“拥有”或者“包含”函数引用。复制代码
对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。
function foo() { console.log( this.a );}var obj2 = { a: 42, foo: foo};var obj1 = { a: 2, obj2: obj2}obj1.obj2.foo(); //42复制代码
隐式丢失---隐式绑定的函数会丢失绑定对象,会应用默认绑定,从而帮this绑定到全局对象或者undefined上(取决于是否严格模式)
function foo() { console.log( this.a );}var obj = { a: 2, foo: foo}var bar = obj.foo; //函数别名var a = "oops, global"; // a 是全局对象的属性bar(); // "oops, global"复制代码
发生在传入回调函数的情况(更常见,更微妙【更变态】):
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"//把函数传入语言内置的函数而不是传入你自己声明的函数,结果一样。比如传入setTimeout复制代码
- 显示绑定
call
, apply
和bind
function foo() { console.log( this.a );}var obj = { a: 2};foo.call( obj ); // 2复制代码
如果你传入额余个原始值(字符串类型、布尔类型或者数字类型)来当做this的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(..)
、new Boolean(..)
或者new Number(..)
)。这通常被成为装箱。
1. 硬绑定 call, apply和bind2. API调用的上下文```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//实际上就是使用call或者apply实现了现实绑定```复制代码
- new绑定
JavaScript
中的构造函数: 在JavaScript
中,构造函数只是一些使用new
操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new
操作符调用的普通函数而已。
实际上并不存在所谓的“构造函数”,只有对于函数的构造调用。
使用new来调用函数,会自动执行下面的操作:
- 创建(或者构造)一个全新的对象;
- 这个新对象会执行
[[Prototype]]
连接; - 这个新对象会绑定到函数调用的
this
; - 如果函数没有返回其他对象,那么
new
表达式中的函数调用会自动返回这个新对象。
function foo(a) { this.a = a}var bar = new foo(2);console.log( bar.a ); // 2复制代码
2.3 优先级
判断this
- 函数是否在
new
中调用(new
绑定)?如果是,this绑定的是新创建的对象; - 函数是否通过
call
,apply
(显示绑定)或硬绑定调用?如果是,this
绑定的是指定的对象; - 函数是否在某个上下文对象中调用(隐式绑定)?如果是,
this
绑定的是哪个上下文对象; - 如果都不是,使用默认绑定。严格模式,绑定到
undefined
,否则绑定到全局对象。
2.4 绑定例外
- 被忽略的this
如果你把null或者undefined作为this的绑定对象传入call, apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
总是使用
null
来忽略this
绑定可能产生一些副作用。如果这个函数确实使用了this
(比如第三方库中的一个函数),那默认绑定规则会把this
绑定了全局对象(在浏览器中这个对象是window
),这将导致不可预计的后果(比如修改全局对象)。
更安全的this(不对全局对象产生影响)
function foo(a, b){ console.log( "a:" + a + ", b:" + b );}//我们的DMZ空对象var Ø = Object.create( null );//把数组展开成参数foo.apply( Ø, [2, 3] ); // a: 2, b: 3//使用bind()进行柯里化var bar = foo.bind( Ø, 2 );bar(3); // a: 2, b: 3复制代码
- 间接引用
function foo() { console.log( this.a );}var a = 2;var o = { a: 3, foo: foo };var p = { a: 4 };o.foo(); //3 隐式绑定(p.foo = 0.foo)(); //2 默认绑定复制代码
**注意:**对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。
- 软绑定
基于硬绑定的问题:硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显示绑定来修改
this
。
如果可以给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this的能力。--- 软绑定
if ( !Function.prototype.softBind ){ Function.prototype.softBind = function(obj) { var fn = this; // 捕获所有curried参数 var curried = [].slice.call( arguments. 1); var bound = function() { return fn.apply( ( !this||this === (window||global) ) ? obj : this, curried.concat.apply( curried, arguments ); ); }; bound.prototype = Object.create( fn.prototype ); return bound; }}复制代码
2.5 this词法
箭头函数不使用this
的四种标准规则,而是根据外层(函数或者全局)作用域来决定this
。
function foo() { // 返回一个箭头函数 return (a) => { // this 继承自foo() console.log( this.a ); };}var obj1 = { a: 2}var obj2 = { a: 3}var bar = foo.call( obj1);bar.call(obj2); // 2复制代码
箭头函数最常用于回调函数中,例如事件处理器或者定时器。
建议:
- 只使用词法作用域并完全抛弃错误
this
风格的代码; - 完全采用
this
风格,在必要时使用bind(..)
,尽量避免使用self = this
和箭头函数。
第3章 对象
3.1 语法和类型
- 对象可以通过两种形式定义: 声明(文字)形式和构造形式。
声明(文字)形式:
var myObj = { key: value //...}复制代码
构造形式(少用):
var myObj = new Object();myObj.key = value;复制代码
- Javascript中一共有六种主要类型:
- string
- number
- boolean
- null
- undefined
- object
注意: 以上简单类型本身并不是对象。
null
有时会被当作一种对象类型,但是这其实只是语言本身的一个bug
,即对null
执行typeof
null
会返回字符串'object'
。实际上,null
本身事基本类型(解释)。
不同的对象在底层都表示为二进制,在JavaScript
中二进制前三位都为0的话会被判断为object
类型,null
的二进制表示全为0,自然前三位也是0,所以执行typeof
会返回'object'
。
- 内置对象:
- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
在Javascript中,以上内置对象实际上只是一些内置函数,可以当作构造函数来使用。
3.2 对象内容
存储在对象容器内部的事这些属性的名称,它们就像指针(从技术角度来说就是引用)一样(栈),指向这些值真正的存储位置(堆)。
- 属性和方法:
“函数”和“方法”在Javascript中是可以互换的。即使你在对象的文字形式中声明一个函数表达式,这个函数也不会“属于”这个对象 --- 它们只是对于相同函数对象的多个引用。
- 数组:
var myArray = [ "foo", 42, "bar" ];myArray.baz = 'baz';myArray.length; // 3myArray.baz; // 'baz' ---添加了命名属性,但是数组的length并没有发生变化。复制代码
注意:如果你试图向数组添加一个属性,但是属性名“看起来”像一个数字,那它会变成一个数值下标(因此会修改数组的内容而不是添加一个属性)。
var myArray = [ "foo", 42, "bar" ];myArray['3'] = 'baz';myArray.length; // 4myArray[3]; // 'baz';复制代码
- 复制对象
深复制:
对于JSON安全(也就是说可以被序列化为一个JSON字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象来说,可以通过以下方式进行复制:
var newObj = JSON.parse( JSON.stringify( someObj ) )复制代码
浅复制:
使用ES6方法Object.assign()
var newObj = Object.assign( {}, newObject )复制代码
**注意:**由于Object.assign(..)
就是使用=操作符来赋值,所以源对象属性的一些特性(比如writable
)不会被赋值到目标对象。
-
属性描述符
writable, configurable, enumerable
.-
writable
严格模式与非严格模式 -
把
configurable
修改成false
是单向操作,无法撤销。 -
即便属性是
configurable: false
,我们还是可以把writable
的状态由true
改为false
,但是无法由false
改为true
。 -
不要把
delete
看作一个释放内存的工具,它就是一个删除对象的操作而已。
-
-
不变性
- 对象常量
- 禁止扩展
- 密封
- 冻结
-
[[Get]]/[[Put]]
var myObject = { a: 2}myObject.a; //2复制代码
在语言规范中, myObject.a
在myObject
上实际上实现了[[Get]]
操作(有点像函数调用:[[Get]]()
)。对象默认的内置[[Get]]
操作首先在对象中查找是否有名称相同的属性,如果找到就返回这个属性的值。如果没有找到这个属性,按照[[Get]]
算法的定义会执行另外一种非常重要的行为 --- 遍历可能存在的原型链。
- Getter/Setter
var myObject = { // 给a定义一个getter get a() { return 2; };}Object.defineProperty( myObejct, // 目标对象 'b', //属性名 { // 描述符 // 给b设置一个getter get: function() { return this.a * 2 } // 确保b出现在对象的属性列表中 enumerable: true })myObject.a; // 2myObject.b; // 4复制代码
- 存在性
in操作符会检查属性是否在对象及其
[[Prototype]]
原型链中。相比之下,hasOwnProperty(..)
之后检查属性是否在myObject
对象中,不会检查原型链。
对象可能没有连接到
Object.prototype
,直接使用myObject.hasOwnProperty(..)
会失败,可以采用一种更加强硬的方法来进行判断:Object.prototype.hasOwnProperty.call(myObejct, "a")
,它解压基础的hasOwnProperty(..)
方法并把它显示绑定到myObject
上。
4 in [2, 4, 6]; //fasle//[2, 4, 6]这个数组中包含的属性名是0,1,2,没有4复制代码
-数组 <-- for循环-对象 <-- for..in复制代码
var myObject = {};Object.defineProperty( myObject, "a", // 让a像普通属性一样可以枚举 { enumerable: true, value: 2 });Object.defineProperty( myObject, "b", // 让b不可枚举 { enumerable: false, value: 3 });myObject.propertyIsEnumerable( "a" ); //truemyObject.propertyIsEnumerable( "b" ); //falseObject.keys( myObject ); // ["a"]Object.getOwnPropertyNames( myObject ); // ["a", "b"]复制代码
propertyIsEnumerable(..)
会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足enumerable:true
。
Object.keys(..)
会返回一个数组,包含所有可枚举属性,Obejct.getOwnPropertyNames(..)
会返回一个数组,包含所有属性,无论它们是否可枚举。
in
和hhasOwnProperty(..)
的区别在于是否查找[[prototype]]
链,然而,Object.keys(..)
和Object.getOwnPropertyNames(..)
都只会查找对象直接包含的属性。
3.3 遍历
forEach(..)
every(..)
some(..)
for..of循环语法
和数组不同,普通的对象没有内置的@@iterator
,所以无法自动完成for..of
循环。
==> 给任何想遍历的对象定义@@iterator
var object = { 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) }; } }; }});//手动遍历myObjectvar it = myObject[Symbol.iterator]();it.next(); { value:2, done:false }it.next(); { value:3, done:false }it.next(); { value:undefined, done:true }for (var v of myObject){ console.log( V );}// 2// 3复制代码
第4章 混合对象“类”
4.1 类理论
类/继承描述了一种代码的组织结构方式 --- 一种在软件中对真实世界中问题领域的建模方法。
面向对象编程强调的是数据和操作数据的行为本质上是互相关联的,因此好的设计是把数据以及和它相关的行为打包。
4.2 类的机制
- 构造函数
类实例是由一个特殊的类方法构造的,在各个方法通常和类名相同,被成为构造函数。
//类class CoolGuy { specialTrick = nothing CoolGuy( trick ) { specialTrick = trick } //类方法,构造函数 showOff() { output( "Here's my trick: ", specialTrick ) }}//实例化一个对象Joe = new CoolGuy("jumping rope")Joe.showOff() // Here's my trick: jumping rope复制代码
4.3 类的继承
-
多态
相对多态: 之所以说“相对”是因为我们并不会定义想要访问的绝对继承层次(或者说类),而是使用相对引用“查找上一层”。
多态的另一个方面是,在继承链的不同层次中一个方法名可以被多次定义,当强调方法时会自动选择合适的定义。
在传统的面向类的语言中,构造函数是属于类的,而
Javascript
中恰好相反,实际上“类”是属于构造函数的。(类似Foo.prototype...
这样的类型引用)。由于JavaScript
中父类和子类的关系只存在与两者构造函数对应的.prototype
对象中,因此它们的构造函数直接并不存在直接联系,从而无法简单地实现两者的相对引用。多态并不表示子类和父类有关联,子类得到的只是父类的一份副本。类的继承其实就是复制。
-
多重继承(继承多个父类)
注意: 上面说类的继承其实就是复制是针对其他传统语言来说的,而Javascript在继承时一个对象并不会被复制到其它对象,只是关联起来。
4.4 混入
在继承或者实例化时,Javascript的对象机制不会自动执行复制行为。简单来说,JavaScript中只有对象,并不存在可以被实例化的“类”。一个对象并不会被复制到其他对象,它们会被关联起来。
混入的意义在于模拟类的复制行为,分为显式混入和隐式混入。
- 显式混入
function mixin( soureceObj, 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 froward!" ); }};var Car = mixin( Vehicle, { wheels: 4, drive: function() { Vehicle.drive.call( this ); //显式多态 console.log( "Rolling on all " + this.wheels + "wheels!" ); }} );复制代码
寄生继承
function Vehicle() { this.engines = 1;}Vehicle.prototype.ignition = function() { console.log("Turning on my engine.");};Vehicle.prototype.drive = function() { this.ignition(); console.log("Steering and moving forward");};//"寄生类" Carfunction Car() { // 首先,car是一个Vehicle var car = new Vehicle(); //接着我们对car进行定制 car.wheels = 4; //保存到Vehicle::drive()的特殊引用 var vehDrive = car.drive; //重写Vehicle::drive car.drive = function() { vehDrive.call( this ); console.log("rolling on all" + this.wheels + "wheels!"); }; return car;}var myCar = new Car();myCar.drive();复制代码
- 隐式混入
var something = { cool: function() { this.greeting = "Hello world"; this.count = this.count ? this.count + 1 : 1; }};something.cool();something.greeting; //"Hello world"something.count; //1var Another = { cool: function() { //隐式把Something混入Another Something.cool.call( this ); }};Another.cool();Another.greeting; //"Hello world"Another.count; // 1 (count 不是共享状态)复制代码
第5章 原型
5.1 [[Prototype]]
使用for..in
遍历对象时原理和查找[[Prototype]]
类似,任何可以通过原型链访问到的属性都会被枚举。使用in
操作符来检查属性咋对象中是否存在时,同样会查找整条原型链。
- 所有普通
[[prototype]]
链最终都会指向内置的Object.prototype
。 - 属性设置与屏蔽 如果向
[[prototype]]
链上层已经存在的属性([[Put]]
)赋值,不一定会触发屏蔽。需要观察[[prototype]]
链上层的该属性是否标记为只读(writable:false
),或者[[prototype]]
链上层存在该属性,并且它是一个sette
r,那就一定会调用setter
。属性为只读和为setter
的情况下都不能触发屏蔽。(尽量避免使用屏蔽)
var anotherObject = { a: 2}var myObject = Object.create( anotherObject );anotherObject.a; // 2myObject.a; // 2anotherObject.hasOwnProperty( "a" ); //truemyObject.hasOwnProperty( "a" ); //faslemyObject.a++; //隐式屏蔽,其实等价于myObject.a = myObject.a + 1;anotherObject.a; // 2myObject.a; // 3myObject.hasOwnProperty( "a" ); //true复制代码
5.2 “类”
JavaScript 只有对象。
- 类函数
function Foo() { //....}var a = new Foo();Object.getPrototypeOf( a ) === Foo.prototype; //true复制代码
Foo
的原型-Foo.prototype
,通过调用new Foo()
创建的每个对象将最终被[[Prototype]]
链接到这个“Foo.prototype
”对象。
在面向类的语言中,类可以被复制多次,就像用模具制作东西一样。而JavaScript
没有类似的复制机制,不能创建一个类的多个实例,只能创建多个对象,它们[[Prototype]]
关联的是同一个对象。new Foo
只是让两个对象互相关联。
委托可以更加准确的描述JavaScript的对象关联机制。
- "构造函数"
function Foo() { //...}Foo.prototype.constructor === Foo; //truevar a = new Foo();a.constructor === Foo; //true复制代码
实际上,Foo
和你程序中的其它函数没有任何区别。函数本身并不是构造函数,然而,当你在普通函数调用前面加上new
关键字之后,就会把这个函数调用变成一个“构造函数调用”。new
会劫持所有的普通函数并用构造对象的形式调用它。
函数不是构造函数,但是当且仅当使用
new
时,函数调用会变成“构造函数调用”。
Foo.prototype
的.constructor
属性只是Foo
函数在声明时的默认属性。如果你创建一个新对象并替换了函数默认的.prototype
对象引用,那么新对象并不会自动获得.constructor
属性。
function Foo() { /*..*/ }Foo.prototype = { /*..*/ }; //创建一个新原型对象var a1 = new Foo();a1.constructor === Foo; //falsea1.constructor === object; //true复制代码
手动修复.constructor
属性
Object.defineProperty( Foo.prototype, "constructor", { enumerable: false, writable: true, configurable: true, value: Foo //让.constructor指向Foo})复制代码
constructor
并不表示(对象)被(它)构造。
5.3 (原型)继承
function Foo() { this.name = name;}Foo.prototype.myName = function() { return this.name;}function Bar(name, label){ Foo.call( this, name ); this.label = label;}//创建一个新的Bar.prototype对象并关联到Foo.prototypeBar.prototype = Object.create( Foo.prototype );//注意,现在没有Bar.prototype.constructor了//如果需要,可以手动修复Bar.prototype.myLabel = function() { return this.label;};var a = new Bar( "a", "obj a" );a.myName(); //"a"a.myLabel(); //"obj a"复制代码
上述代码的核心部分:调用Object.create(..)会创建一个“新”对象并把新对象内部的[[Prototype]]关联到你指定的对象。(这里是Foo.prototype), "创建一个新的Bar.prototype
对象并吧它关联到Foo.prototype
"。
两种把Bar.prototype
关联到Foo.prototype
的方法
//ES6之前需要抛弃默认的Bar.prototypeBar.prototype = Object.create( Foo.prototype );//ES6开始可以直接修改现有的Bar.prototypeObejct.setPrototypeOf( Bar.prototype, Foo.prototype )复制代码
- 检查“类”关系
在传统的面向类环境中检查一个实例(JavaScript中的对象)的继承祖先(JavaScript中的委托关联)通常被称为内省(或者反射)。
instanceof --- 在a的整条[[Prototype]]链中是否有Foo.prototype指向的对象?
isPrototypeOf --- 在a的整条[[Prototype]]链中是否出现过Foo.prototype?
b.isPrototypeOf(c); //b是否出现在c的[[Prototype]]链中 //这个方法并不需要使用函数“类”,它直接使用b和c之间的对象引用来判断它们的关系。复制代码
//直接获取一个对象的链Object.getPrototypeOf( a );Object.getPrototypeOf( a ) === Foo.prototype; // true//绝大多数浏览器支持的一种访问内部[[Prototype]]属性a._proto_ === Foo.prototype; //true复制代码
._proto_的实现(笨蛋“proto”)
Object.defineProperty( Object.prototype, "_proto_", { get: function() { return Object.getPrototypeOf( this ); }, set: fucntion() { // ES6中的setPrototypeOf(...) Obejct.setPrototypeOf( this, o ); return o; }})复制代码
5.4 对象关联
Object.create(null)
这个对象没有原型链,所以instanceOf
操作符无法进行判断,总返回false
,不受原型链的干扰,因此非常适合用来存储数据。
// Object.create()```的```polyfill```代码if(!Object.create) { Object.create = function(o) { function F(){} F.prototype = o; return new F(); }}复制代码
委托设计模式
“委托”是一个更合适的术语,因为对象直接的关系不是复制而是委托。
第6章 行为委托
JavaScript中这个机制的本质就是对象直接的关联关系。
6.1 面向委托的设计
试着把思路从类和继承装换到委托行为的设计模式。
- 类理论(先抽象到父类然后用子类进行特殊化重写)
- 委托理论
//即不是类也不是对象,包含所有任务都可以使用的具体行为Task = { setId: function(id) { this.id = id }; outputId: fuction() { console.log( this.id ); }};//让XYZ委托TaskXYZ = Object.create( Task );//定义一个对象来存储数据和行为XYZ.prepareTask = function( id, label ){ this,setId( Id ); this.label = label;}XYZ.outputTaskData = function() { this.outputId(); console.log( this.label );}//使用,执行任务XYZ需要两个兄弟对象(Task和XYZ)协作完成// ABC = Object.create( Task );// ABC ... = ...复制代码
对象关联风格代码的不同之处:
- 数据成员直接存储在委托者而不是委托目标;
- 兄弟对象一般不会使用相同的命名,提倡使用更有描述性的方法名。尤其要写清相应对象行为的类型。
- this会绑定到委托者(隐式绑定)
委托行为意味着某些对象在找不到属性或者方法引用时会把这个请求委托给另一个对象。
在API接口设计中,委托最后在内部实现,不要直接暴露出去。
- 互相委托(禁止)- 调试(谷歌浏览器和其他浏览器的异同)复制代码
- 比较思维模型
6.2 类和对象
类和对象在实际中的应用场景:创建UI控件(按钮,下拉列表)。
三种代码风格:
- ES5类
//父类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 = $("
- ES6类
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 = $("
- 委托
var Widget = { init:function(width, height) { this.width = width || 50; this.height = height || 50; this.$elem = null; } insert: function($where){ if(this.$elem){ this.$elem.css({ this.width = width || 50; this.height = height || 50; }).appendTo( $where ); } }}var Button = Object.create( Widget );Button.setup = function(width, height, label) { //委托调用 this.init( width, height ); this.label = label || "Default"; this.$elem = $("
对象关联可以更好的支持关注分离原则。
6.3 更简洁的设计
两个控制器对象 --- 操作网页中的登录表单和与服务器进行验证。
(两个控制器对象是兄弟关系,不是父子关系。)
6.4 更好的语法
函数名的简写,但是需要自我引用时,则使用传统的具名函数。
6.5 内省
内省就是检查实例的类型,类实例的内省主要目的是通过创建方式来判断对象的结构和功能。
function Foo() {/*..*/}Foo.prototype...function Bar() {/*..*/}Bar.prototype = Object.create( Foo.prototype );var b1 = new Bar( "b1" );Bar.prototype instanceof Foo; //trueObject.getPrototypeOf( Bar.prototype ) === Foo.prototype; //trueFoo.prototype.isPrototypeOf( Bar.prototype ); //trueb1 instanceof Bar; //trueb1 instanceof Foo; //trueObject.getPrototypeOf(b1) === Bar.prototype; //trueFoo.prototype.isPrototypeOf( b1 ); //trueBar.prototype.isPrototypeOf( b1 ); //true复制代码
var Foo = { /*..*/ };var Bar = Object.create( Foo );Bar...var b1 = Object.create( Bar );Foo.isPrototypeOf( Bar ); //trueObject.getPrototypeOf( Bar ); //trueFoo.isPrototypeOf(b1); //trueBar.isPrototypeOf(b1); //trueObject.getPrototypeOf( b1 ) === bar; //true复制代码