L3F.WIN

Github及Hexo的使用

0%

《你不知道的JavaScript》上卷读书笔记第二部分

第1章 关于this

this 关键字是Javascript中最复杂的机制之一。到现在我也是对this一知半解,完全没有弄清楚。

为什么要用this

this 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function identify(){
return this.name.toUpperCase();
}

function speak(){
var greeting = "Hello, I'm " + identify.call( this );
console.log( greeting );
}

var me = {
name: "Lisa"
}

var you = {
name: "John"
}

identify.call( me ); //LISA
identify.call( you ); //JOHN
speak.call( me ); //Hello, I'm LISA
speak.call( you ); //Hello, I'm JOHN

简单说明
每个函数都包含两个非继承而来的方法:call()方法和apply()方法。
call()的第一个参数是作为函数上下文的对象,之后的参数可以是任意形式。

this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简洁并且易于复用。

误解

太拘泥于“ this ”的字面意思就会产生一些误解。有两种常见的对于this的解释,但是他们都是错误的。

  1. 指向自身(错误理解
    如下代码和我们想象的不一样了
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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 ); //0
    //6 7 8 9
    难道foo.count不应该是4么?
    如何修改才能叫this指向foo.count呢
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function foo( num ){
    console.log( "foo: " + num );
    //记录foo被调用的次数
    //注意,在当前的调用方式下,this确实指向foo
    this.count++;
    }

    foo.count = 0;

    var i;
    for( i=0; i<10; i++ ){
    if( i > 5 ){
    //使用call(),可以确保this指向对象foo本身
    foo.call(foo, i);
    }
    }
    console.log( foo.count ); //4
    //6 7 8 9
  2. 它的作用域(错误理解
    第二种常见的错误理解,this指向函数的作用域。它不完全正确
    这里需要明确一个概念是,this在任何情况下都不指向函数的词法作用域,在Javascript内部,作用域确实和对象类似,可见的标识符都是他的属性,但是作用域“对象”无法通过Javascript代码访问,它存在于Javascript引擎内部。
    错误例
    1
    2
    3
    4
    5
    6
    7
    8
    function foo(){
    var a = 2;
    this.bar(); //首先这里就是错误的,调用bar不需要this
    }
    function bar(){
    console.log( this.a ); //链接foo想要不执行就读取foo中的a是不可能实现的。
    }
    bar();
  3. this 到底是什么
    this是在运行时进行绑定的,并不是在编写时绑定。它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
    当一个函数被调用时,会创建一个活动记录。这个记录会包含函数在那里被调用,函数的调用方法,传入的参数等信息。this就是记录的其中一个属性,会在函数执行的过程中用到。

第2章 this全面解析

我们了解到每个函数的this是在调用时被绑定的,完全取决于函数的调用方法。

调用位置

调用位置,说白了就是函数在代码中被调用的位置(不是声明位置),只有了解了这个概念,才可以知道this到底引用的是什么。

绑定规则

默认绑定

默认绑定一般出现在独立函数调用,如下例

1
2
3
4
5
function foo(){
console.log( this.a ); //this默认绑定到全局对象,如果在严格模式下 this将绑定到undefined
}
var a = 2;
foo(); //2

通常情况下不要在代码中混合使用strict mode 和 non-strict mode

隐式绑定

另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。

1
2
3
4
5
6
7
8
9
10
function foo(){
console.log( this.a );
}
var a = 22;
var obj = {
a: 2,
foo: foo
}
foo(); //22
obj.foo(); //2

通过这段代码我们基本上就可以了解到了。根据函数调用的位置不同,this绑定也是不同的。
还有,对像属性引用链中只有最顶层或最后一层会影响调用位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo(){
console.log( this.a );
}

var obj2 = {
a: 42,
foo: foo //this,只绑定最后一层
}

var obj1 = {
a: 2,
obj2: obj2
}
obj1.obj2.foo(); //42

隐式丢失
一个最常见的this绑定问题就是被绑定的函数会丢失绑定的对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上。

1
2
3
4
5
6
7
8
9
10
11
12
function foo(){
console.log( this.a );
}

var obj = {
a: 2,
foo: foo
}

var bar = obj.foo //函数别名
var a = "oops, global"; //a是全局对象的属性
bar(); //oops, global

显式绑定

通过Javascript提供的特殊函数call()及apply()方法,他们的第一个参数是一个对象,他们会把这个对象绑定到this,接着在调用函数时指定这个this,因为你可以直接指定this的绑定对象,因此我们称之为显示绑定
关于丢失绑定的解决办法

  1. 硬绑定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function foo(){
    console.log( this.a );
    }

    var obj = {
    a: 2,
    foo: foo
    }

    var bar = function(){
    foo.call( obj ); //在这里已经硬绑定了this,再怎么也无法修改了
    } //函数别名
    var a = "oops, global"; //a是全局对象的属性
    bar(); //2
    setTimeout(bar(), 100); //2
    bar.call(window) //2

    这种情况,无论以后怎么调用函数bar,它总会手动在obj上调用foo,这种绑定是一种显式的强制绑定被称之为硬绑定

    示例1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function foo(something){
    console.log( this.a, something );
    return this.a + something;
    }

    var obj = {
    a: 2
    }

    var bar = function(){
    return foo.apply(obj, arguments); //arguments对象是所有(非箭头)函数中都可用的局部变量。
    };

    var b = bar( 3 ); //2 3
    console.log( b ); //5

    硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值
    另一种使用方法是创建一个i可以重复使用的辅助函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function foo(something){
    console.log( this.a, something );
    return this.a + something;
    }

    var obj = {
    a: 2
    }

    var bar = foo.bind( obj );

    var b = bar( 3 ); //2 3
  2. API调用的“上下文”
    第三方库及Javascript语言许多的新的内置函数都提供了可选的参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function foo(el){
    console.log(el, this.id);
    }

    var obj = {
    id: "awesome"
    };

    //调用foo()时,把this绑定到obj
    [1,2,3].forEach(foo, obj); //"awesome""awesome""awesome"

    new绑定

    Javascript的new和其他语言的new有所区别,new在Javascript中只是一个普通的函数,用于创建新的对象,并将这个新对象绑定到函数调用的this上。

优先级

了解了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 obj1 = {
a: 2,
foo: foo
}

var obj2 = {
a: 3,
foo: foo
}

obj1.foo(); //2
obj2.foo(); //3
obj1.foo.call( obj2 ); //3
obj2.foo.call( obj1 ); //2

可以看出显式绑定优先级更高
接下来我们比较一下new绑定和隐式绑定的优先级别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function foo(something){
this.a = something;
}

var obj1 = {
foo: foo
};

var obj2 ={};

obj1.foo( 2 );
console.log( obj1.a ); //2

obj1.foo.call( obj2, 3 );
console.log( obj2.a ); //3

var bar = new obj1.foo( 4 );
console.log( obj1.a ); //2
console.log( bar.a ); //4

明显new绑定级别要高于隐式绑定
(实际上new和call,apply无法共同使用,所以无法直接判断优先级)
不过通过.bind我们可以判断new绑定和显式绑定的优先级

1
2
3
4
5
6
7
8
9
10
11
12
function foo(something){
this.a = something;
}

var obj1 = {};

var bar = foo.bind( obj1 ); //通过bind将bar硬绑定到obj1
bar( 2 );
console.log( obj1.a ); //2

var baz = new bar( 3 );
console.log( baz.a ); //3

判断this
this绑定可以按照以下的顺序进行判断
① 函数是否存在new调用?如果是的话,this绑定的是新创建的对象
② 函数是否通过call | apply或是硬绑定调用?如果是的话,this绑定的是指定对象
③ 函数是否在某个上下文中调用?如果是的话,this绑定的是那个上下文对象
④ 如果都不是,则绑定全局对象,严格模式下就绑定到undefined

绑定例外

在特殊情况下this的绑定也有例外

被忽略的this

如果你把null或是undefined 作为绑定对象传入到 call, apply或者bind,这些值会在调用时被忽略。

1
2
3
4
5
6
7
function foo(){
console.log( this.a );
}

var a = 2;

foo.call( null ); //2 ,null被忽略了

但是这种直接使用null,会给程序带来许多意想不到的bug,所以可以使用一下的安全this

1
2
3
4
5
6
7
8
9
10
11
function foo( a, b ){
console.log( "a: " + a + " b: " + b );
}

//把DMZ做成一个空对象
var Φ = Object.create( null );

//把数组展开成参数
foo.apply( Φ, [2,3] ); //a: 2 b: 3
var bar = foo.bind(Φ, 2);
bar( 3 ); // a: 2 b: 3

间接引用

创建一个函数的间接引用时,调用这个函数会应用默认的绑定规则

1
2
3
4
5
6
7
8
9
10
function foo(){
console.log( this.a );
}

var a = 2;
var o = {a: 3, foo: foo};
var p = {a: 4};

o.foo(); //3
(p.foo = o.foo)(); //2

p.foo = o.foo的返回值是对目标函数的引用,因此调用位置为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
25
26
27
28
29
30
31
32
33
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;
};
}

function foo(){
console.log( "name: " + this.name );
}

var obj = {name: "obj1"},
obj2 = {name: "obj2"},
obj3 = {name: "obj3"};
var fooOBJ = foo.softBind( obj );
fooOBJ();

obj2.foo = foo.softBind( obj );
obj2.foo();

fooOBJ.call( obj3 );

setTimeout( obj2.foo, 10 );

(太深奥,以至于此处完全不懂,之后返工)

第3章 对象

对象是什么?

语法

  1. 声明形式(文字形式)
    1
    var myObj = {key: value, ....};
  2. 构造形式
    1
    2
    var myObj = new Object();
    myObj.key = value;
    区别,声明形式,可以一次添加多个键值对,构造形式,需要一个一个添加。
    一般都会使用声明形式来创建对象

类型

Javascript包含6种类型
string, number, boolean, null, undefined, object
函数,数组都属于对象

内置对象
String, Number, Boolean, Object, Function, Array, Date, RegExp, Error

内容

对象的内容是由一些存储在特定命名位置的值组成的,我们称之为属性。
访问对象里的值需要使用 .[]操作符

1
2
3
4
5
6
var myObject = {
a: 2
}

myObject.a; //2
myObject["a"]; //2

在对象中,属性名永远是字符串,即使是数字也会转换成字符串

可计算属性名

可计算属性名最常用的场景在ES6中,可以在文字形式中使用[]包裹一个表达式作为属性名

1
2
3
4
5
6
7
8
var prefix = "foo";

var myObject = {
[prefix + "bar"]: "hello",
[prefix + "baz"]: "world"
};

myObject.prefixbar //hello

属性与方法

在Javascript中从技术角度来说,函数永远不“属于”一个对象,无论返回的值是什么类型,每次访问对象的属性就是属性访问。
如下

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(){
console.log( "foo" );
}

var someFoo = foo; //对foo的变量引用

var myObject = {
someFoo: foo
}

foo; //function foo(){..}
someFoo; //function foo(){..}
myObject.someFoo; //function foo(){..}

someFoo和myObject.someFoo是对同一个函数的不同引用。并不能说函数属于哪一个对象。

数组

数组最好使用[]来访问,最好只用对象来存储键/值对,数组来存储数值下标/值对。

复制对象

  1. 浅复制 ES6提供了一个方法 Object.assign({目标对象}, myObject);
  2. 深复制 ??

属性描述符

ES5之后,提供了一种新的方法Object.getOwnPropertyDescriptor()

1
2
3
4
5
var myObject = {
a: 2
}

console.log(Object.getOwnPropertyDescriptor( myObject, "a" )); //{value: 2, writable: true, enumerable: true, configurable: true}

除了得到属性值外,还获得了对象的3个特性

  1. writable 是否可以修改
  2. configurable 是否可以配置,如果设置成false,除了不能修改其属性外,配置也不能更改。(Object.defineProperty({},”a”,{})
  3. enumerable 是否可以枚举

不变性

如果你希望属性或是对象是不可变的,按照以下提供的方法可以做到浅不变形

  1. 对象常量,结合writable: false和 configurable: false。
    1
    2
    3
    4
    5
    6
    var myObject = {};
    Object.defineProperty( myObject, "FAVORITE_NUMBER", {
    value: 42,
    writable: false,
    configurable: false
    } );
  2. 禁止扩展
    1
    2
    3
    4
    5
    6
    7
    var myObject = {
    a: 2
    };

    Object.preventExtensions( myObject );
    myObject.b = 3;
    myObject.b; //undefined
  3. 密封 Object.seal()会创建一个“密封”的对象,其实就是Object.preventExtenssions() 和 configurable: false的结合
  4. 冻结 Object.freeze() 会创建一个冻结对象,其实就是Object.seal() + writable: false

[[Get]]

[[Get]]操作首先在对象中查找是否有名称相同的属性,如果找到就返回这个属性的值。没有则返回undefined (抽象理解即可,一般用不上)

[[Put]]

同[[Get]]一样大概了解就行,作用于给对象属性赋值的时候发生此操作。

Getter和Setter

对象默认的[[Put]]和[[Get]]操作分别可以控制属性值的设置和获取。
在ES5中可以使用getter和setter部分改写默认操作,只能应用在单个属性上,无法应用在整个对象上。
当你给一个属性定义getter,setter或者两者都有时,这个属性会被定义为“访问描述符”。对于访问描述符来说,Javascript会忽略它们的value和writable特性,取而代之的是set和get(configurable和enumerable)特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var myObject = {
//给a定义一个getter
get a(){
return 2;
}
};

Object.defineProperty( myObject, //目标对象
"b", //属性名
{ //描述符
//给b设置一个getter
get: function(){
return this.a * 2
},

//确保b会出现在对象的属性列表中
enumerable: true
} );
myObject.a = 3;
console.log(myObject.a); //2

由于我们只定义了a的getter,所以对a的值进行设置时,set操作会忽略赋值操作,不会抛出错误,即使有合法的setter,由于我们自定义的getter只会返回2,所以set操作是没有任何意义的。
另外一个例子
为了让属性更合理,我们设置一个setter,setter会覆盖单个属性默认的[[Put]]操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
var myObject = {
//给a定义一个getter
get a(){
return this._a_;
},
//给a设置一个setter
set a(val){
this._a_ = val * 2;
}
};

myObject.a = 2;
console.log(myObject.a); //4

本例中,实际上我们把赋值[[Put]]操作中的值2存储到了另一个变量a中。名称a值是一种惯例,没有任何特殊的行为。

存在性

属性值存储的值是undefined,同属性不存在而返回的undefined,如果区分这两种情况?

1
2
3
4
5
6
7
var myObject = {
a: 2
};
("a" in myObject); //true
("b" in myObject); //false
myObject.hasOwnProperty("a"); //true
myObject.hasOwnProperty("b"); //false

遍历

数组的遍历我们可以使用for..in
对于数值索引的数组来说,可以使用标准的for循环遍历值

1
2
3
4
5
var myArray = [1,3,4];

for(var i = 0; i < myArray.length; i++){
console.log(myArray[i]);
} // 1,3 ,4

ES5中也提供了很多的方法 forEach(), every(), some(),但是他们的共同特点就是遍历数组下标。
ES6提供了一个新的额遍历数组方法 for..of

1
2
3
4
5
var myArray = [1,3,4];

for( var v of myArray){
console.log( v );
} //1,3,4

第4章 类

———————————————-以下内容为坑—————————————————
原因ES6为止Javascript还未正式启动对类的正式支持,非要强求只会带来更多的隐患。

类理论

类/继承描述了一种代码的组织结构形式——一种在软件中对真实世界中问题领域的建模方法

“类”设计模式

类其实就是一种设计模式,如同设计图,模子工厂。

JavaScript中的“类”

其实Javascript并没有提供类的功能,我们只能够使用一些近似类的方法比如 newinstanceofclass

类的机制

建造

实例 的概念来源于房屋建造, 类好比是蓝图,而实例则是根据蓝图造好的房子
"类和实例图"

构造函数

类实例是由一个特殊的类方法构造的,这个方法名通常和类名相同,被称为构造函数
———————————————-以上内容为坑—————————————————

第5章 原型

[[Prototype]]

Javascript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空的值。

1
2
3
4
5
var anotherObj = {
a: 2
}
var myObj = Object.create( anotherObj );
console.log( myObj.a ); //2

虽然myObj里面没有a属性,但是通过[[Prototype]]链找到了anotherObj,并[[Get]]到了属性a
即使你是用for..in遍历对象时原理和查找[[Prototype]]链类似。

1
2
3
4
5
6
7
8
var anotherObj = {
a: 2
}
var myObj = Object.create( anotherObj );

for(var k in myObj){
console.log("found: " + k); //found a
}

Object.prototype

[[Prototype]]的尽头,最终都会指向内置的Object.prototype

属性设置和屏蔽

1
myObject.foo = "bar";

的详细过程

  1. 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性并且没有被标记为只读(writable: false),那就会在myObject中添加一个名为foo的新属性,它是屏蔽属性。
  2. 如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable: false),那么无法修改已有属性或者在myObject上创建屏蔽属性。
  3. 如果在[[Prototype]]链上层存在foo并且它是一个setter,那就一定会调用这个setter。foo不会被添加到myObject, setter相关

“类”

Javascript 是特殊的面向对象的语言,它可以直接创建对象,并直接定义对象自己的行为。

“类”函数

在Javascript中没有类可以被复制,也不能创建实例,但是以new创建的新对象并不会和原型对象失去联系,他们是互联的。

“构造函数”

在Javascript中并不存在构造函数,你在普通的函数调用前面加上 new 关键字之后,new会劫持所有普通函数并用构造对象的形式来调用它。说白了Javascript没有构造函数,只有调用。

技术

参考以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
function Foo( name ){
this.name = name;
}

Foo.prototype.myName = function(){
return this.name;
};

var a = new Foo( "a" );
var b = new Foo( "b" );

console.log(a.myName()); //a
console.log(b.myName()); //b

这段代码展示了另外两种“面向类”的技巧:

  1. this.name = name 给每个对象(a, b)都添加了一个.name属性。
  2. Foo.prototype.myName 它给Foo对象添加一个属性(函数)。现在a,b都可以使用此函数。
    其实a,b并没有复制对象 Foo.prototype,只是关联到了 Foo.prototype上

(原型)继承

参考以下代码

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( name ){
this.name = name;
}

Foo.prototype.myName = function(){
return this.name;
};

function Bar(name, lable){
Foo.call(this, name);
this.lable = lable;
}
//我们创建了一个新的Bar.prototype对象并关联到Foo.prototype对象
Bar.prototype = Object.create( Foo.prototype );

//注意!现在没有Bar.prototype.constructor了
//如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype.myLable = function(){
return this.lable;
};

var a = new Bar( "a", "Object a" );

console.log(a.myName()); //a
console.log(a.myLable()); // Object a

这段代码的核心部分就是语句 Bar.prototype = Object.create( Foo.prototype ); 调用Object.create()会凭空创建一个新对象并把新对象内部的[[Prototype]]关联到拟制定的对象上
以下两种方法是错误的

1
2
3
4
5
//和你想要的机制不一样! 这里属于直接引用
Bar.prototype = Foo.prototype

//基本上满足你的需求,但是可能会产生一些副作用
Bar.prototype = new Foo();

所以如果想创建一个合适的关联对象,我们必须使用Object.create()

检查“类”关系
假设我们要寻找一个对象的委托的对象
检查一个实例的继承祖先(Javascript中的委托关联)通常被称为内省(或者反射)
判断对象和函数之间的关系可以直接使用 instanceof
如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Foo(){
console.log("hahaha");
}
Foo.prototype.blash = function(){

};

var a = new Foo();

if( a instanceof Foo){
console.log("True"); //true
}else{
console.log("False");
}

那如何判断对象与对象的关系呢?
使用 Foo.prototype.isPrototypeOf( a ); //true
我们也可以直接获取一个对象的[[Prototype]]链,如下
Object.getPrototypeOf( a ) === Foo.prototype; //true

对象关联

现在我们知道了,[[Prototype]]机制就是存在于对象中的一个内部链接,它会引用其他对象。
通常来说这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就会继续在[[Prototype]]关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的[[Prototype]],以此类推。这一系列对象的链接被称为“原型链”。

  1. 创建关联
    Object.create()会创建一个新对象并把它关联到我们指定的对象
    1
    2
    3
    4
    5
    6
    7
    var foo = {
    something: function(){
    console.log("Tell me something good..");
    }
    };
    var bar = Object.create( foo );
    bar.something(); //Tell me something good..
  2. 关联关系是备用
    如果我们把对象之间的关联当成是处理“缺失”属性或方法时的一种备用,我们就需要考虑一下是否需要重新设计你的代码。
    为了更清晰的设计你的软件我们可以使用内部委托来替换直接委托,可以使你的代码更清晰。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var anotherObject = {
    cool: function(){
    console.log( "cool!" );
    }
    },
    var myObject = Object.create( anotherObject );
    myObject.doCool = function(){
    this.cool(); //内部委托
    };

    myObject.doCool(); //cool!;