你不知道的JS(上册)

你不知道的JS学习笔记

第一部分:做用域和闭包

第1章 做用域是什么

1.1 编译原理

尽管一般将JavaScript归类为“动态”或“解释执行”语言,但事实上它是一门编译语言。与传统的编译语言不一样,它不是提早编译的,编译结果也不能在分布式系统中进行移植。css

在传统编译语言的流程中,程序中一段源代码在执行以前会尽力三个步骤,即“编译”:算法

  • 分词/词法分析(Tokenizing/Lexing)

这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块被成为词法单元编程

var a = 2; 
//分解成 var、a、=、二、;。
//空格是否会被看成词法单元,取决于空格在这门语言中是否具备意义。
复制代码

分词和词法分析主要差别在于词法单元的识别是经过有状态仍是无状态的方式进行的。设计模式

简单的说,若是词法单元生成器在判断a是一个独立的词法单元仍是其余词法单元的一部分时, 调用的是有状态的解析规则,那么这个过程就被称为词法分析数组

  • 解析/语法分析(Parsing)

这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的表明了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree, AST)。浏览器

  • 代码生成

将AST转换为可执行代码的过程被称为代码生成。安全

抛开具体细节,简单来讲就是有某种方法能够将var a = 2;的AST转化为一组机器指令,用来建立一个叫做a的变量(包括分配内存等),并将一个值储存a中。bash

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是声明中的第一个词,那么就是一个函数声明,不然就是一个函数表达式。

匿名函数的几个缺点:

  1. 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
  2. 若是没有函数名,当函数须要引用自身时只能使用已通过期的arguments.callee, 好比在递归中。另外一个函数须要引用自身的例子,是在事件触发后事件监听器须要解绑自身。
  3. 匿名函数省略了对于代码可读性 / 可理解性很重要的函数名。一个描述性的名称可让代码不言自明。

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;
}
复制代码
    1. 垃圾收集
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);
复制代码
    1. 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 function
var 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持有对一个参数的引用,这个参数也许叫做fnfunc,或者其余类型的名字。引擎会调用这个函数,在例子中就是内部的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(); //cool
foo.doAnother(); //1 ! 2 ! 3
复制代码

这个模式在Javascript中称为模块。

从模块中返回一个实际的对象并非必须的,也能够直接返回一个内部函数。jQuery就是一个很好的例子,jQuery$标识符就是jQuery模块的公用API,但它们自己都是函数(因为函数也是对象,它们自己也能够拥有属性)。

模块模式须要具有两个条件。

  1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会建立一个新的模块实例)。
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有做用域中造成闭包,而且能够访问或者修改私有的状态。
单例模式
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 module
foo.change(); // 1 ! 2 ! 3
foo.identify(); //FOO MODULE
复制代码
5.4.2 现代的模块机制
5.4.3 将来的模块机制(ES6)
//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( 'rhino' )
); //Let me introduce: rhino

foo.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: 9

console..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: 9

console..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, applybind

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2
};

foo.call( obj ); // 2
复制代码

若是你传入额余个原始值(字符串类型、布尔类型或者数字类型)来当作this的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(..)new Boolean(..)或者new Number(..))。这一般被成为装箱。

1. 硬绑定 call, apply和bind

2. 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来调用函数,会自动执行下面的操做:

  1. 建立(或者构造)一个全新的对象;
  2. 这个新对象会执行[[Prototype]]链接;
  3. 这个新对象会绑定到函数调用的this
  4. 若是函数没有返回其余对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a) {
    this.a = a
}

var bar = new foo(2);
console.log( bar.a ); // 2
复制代码

2.3 优先级

判断this

  1. 函数是否在new中调用(new绑定)?若是是,this绑定的是新建立的对象;
  2. 函数是否经过callapply(显示绑定)或硬绑定调用?若是是,this绑定的是指定的对象;
  3. 函数是否在某个上下文对象中调用(隐式绑定)?若是是,this绑定的是哪一个上下文对象;
  4. 若是都不是,使用默认绑定。严格模式,绑定到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
复制代码

箭头函数最经常使用于回调函数中,例如事件处理器或者定时器。

建议:

  1. 只使用词法做用域并彻底抛弃错误this风格的代码;
  2. 彻底采用this风格,在必要时使用bind(..),尽可能避免使用self = this和箭头函数。

第3章 对象

3.1 语法和类型

  1. 对象能够经过两种形式定义: 声明(文字)形式和构造形式。

声明(文字)形式:

var myObj = {
    key: value
    //...
}
复制代码

构造形式(少用):

var myObj = new Object();
myObj.key = value;
复制代码
  1. Javascript中一共有六种主要类型:
  • string
  • number
  • boolean
  • null
  • undefined
  • object

注意: 以上简单类型自己并非对象。null有时会被看成一种对象类型,可是这其实只是语言自己的一个bug,即对null执行typeof null会返回字符串'object'。实际上,null自己事基本类型(解释)。

不一样的对象在底层都表示为二进制,在JavaScript中二进制前三位都为0的话会被判断为object类型,null的二进制表示全为0,天然前三位也是0,因此执行typeof会返回'object'

  1. 内置对象:
  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

在Javascript中,以上内置对象实际上只是一些内置函数,能够看成构造函数来使用。

3.2 对象内容

存储在对象容器内部的事这些属性的名称,它们就像指针(从技术角度来讲就是引用)同样(),指向这些值真正的存储位置()。

  • 属性和方法:

“函数”和“方法”在Javascript中是能够互换的。即便你在对象的文字形式中声明一个函数表达式,这个函数也不会“属于”这个对象 --- 它们只是对于相同函数对象的多个引用。

  • 数组:
var myArray = [ "foo", 42, "bar" ];
myArray.baz = 'baz';
myArray.length; // 3
myArray.baz; // 'baz' ---添加了命名属性,可是数组的length并无发生变化。
复制代码

注意:若是你试图向数组添加一个属性,可是属性名“看起来”像一个数字,那它会变成一个数值下标(所以会修改数组的内容而不是添加一个属性)。

var myArray = [ "foo", 42, "bar" ];
myArray['3'] = 'baz';
myArray.length; // 4
myArray[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.amyObject上实际上实现了[[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; // 2
myObject.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" ); //true
myObject.propertyIsEnumerable( "b" ); //false

Object.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)
                };
            }
        };
    }
});

//手动遍历myObject
var 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");
};

//"寄生类" Car
function 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; //1

var 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]]链上层存在该属性,而且它是一个setter,那就必定会调用setter。属性为只读和为setter的状况下都不能触发屏蔽。(尽可能避免使用屏蔽)
<!--注意隐式屏蔽-->
var anotherObject = {
    a: 2
}

var myObject = Object.create( anotherObject );

anotherObject.a; // 2
myObject.a; // 2

anotherObject.hasOwnProperty( "a" ); //true
myObject.hasOwnProperty( "a" ); //fasle

myObject.a++; //隐式屏蔽,其实等价于myObject.a = myObject.a + 1;
anotherObject.a; // 2
myObject.a; // 3

myObject.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; //true
var 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; //false
a1.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.prototype
Bar.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.prototype
Bar.prototype = Object.create( Foo.prototype );

//ES6开始能够直接修改现有的Bar.prototype
Obejct.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委托Task
XYZ = 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 ... = ...
复制代码

对象关联风格代码的不一样之处:

  1. 数据成员直接存储在委托者而不是委托目标;
  2. 兄弟对象通常不会使用相同的命名,提倡使用更有描述性的方法名。尤为要写清相应对象行为的类型。
  3. this会绑定到委托者(隐式绑定)

委托行为意味着某些对象在找不到属性或者方法引用时会把这个请求委托给另外一个对象。

在API接口设计中,委托最后在内部实现,不要直接暴露出去。

- 互相委托(禁止)
- 调试(谷歌浏览器和其余浏览器的异同)
复制代码
  • 比较思惟模型

6.2 类和对象

类和对象在实际中的应用场景:建立UI控件(按钮,下拉列表)。

三种代码风格:

  1. 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 = $("<button>").text( this.label );
}

//让Button“继承”Widget
Button.prototype = Object.create( Widget.prototype );

//重写render(..)
Button.prototype.render = function($where) {
    //"super"调用
    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( fucntion()){
    var $body = $(document.body);
    var btn1 = new Button(125, 30, "hello");
    var btn2 = new Button(150, 40, "world");
    
    btn1.render($body);
    btn2.render($body);
}
复制代码
  1. 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 = $("<button>").text( this.label );
    }
    render($where) {
        super.render( $where );
        this.$elem.click( this.onClick.bind(this) )
    }
    onClick(evt) {
        console.log( "Button" + this.label + "clicked" );
    }
}

$(document).ready( fucntion()){
    var $body = $(document.body);
    var btn1 = new Button(125, 30, "hello");
    var btn2 = new Button(150, 40, "world");
    
    btn1.render($body);
    btn2.render($body);
}
复制代码
  1. 委托
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 = $("<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( fucntion(){
    var $body = $(document.body);
    
    var btn1 = Object.create(Button);
    btn1.setup( 125, 30, "hello" );
    
    var btn1 = Object.create(Button);
    btn2.setup( 150, 40, "world" );
    
    btn1.build($body);
    btn2.build($body);
} )
复制代码

对象关联能够更好的支持关注分离原则。

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; //true
Object.getPrototypeOf( Bar.prototype ) === Foo.prototype; //true
Foo.prototype.isPrototypeOf( Bar.prototype ); //true

b1 instanceof Bar; //true
b1 instanceof Foo; //true
Object.getPrototypeOf(b1) === Bar.prototype; //true
Foo.prototype.isPrototypeOf( b1 ); //true
Bar.prototype.isPrototypeOf( b1 ); //true
复制代码
var Foo = { /*..*/ };

var Bar = Object.create( Foo );
Bar...

var b1 = Object.create( Bar );

Foo.isPrototypeOf( Bar ); //true
Object.getPrototypeOf( Bar ); //true

Foo.isPrototypeOf(b1); //true
Bar.isPrototypeOf(b1); //true
Object.getPrototypeOf( b1 ) === bar; //true
复制代码
相关文章
相关标签/搜索