读《你不知道的js——上卷(2)》

第二部分 this 和对象原型

第 1 章 关于 this

thisJavascript 关键字,被自动定义在所有函数的作用域中。

this: 是在==运行时绑定==的,并不是在编写时绑定,只取决于函数调用方式。

this: 既不指向自身,也不指向函数的词法作用域。

this 在任何情况下==都不指向==函数的词法作用域

1.2 对 this 的误解

1.2.1 指向自身(误解)

我们想要记录函数 foo 被调用的次数:

function foo(num) {
    console.log( "foo: " + num );
    // 记录 foo 被调用的次数
    this.count++; 
    data.count++;
}

foo.count = 0;

var data = {
    count: 0
}

var i;

for (i=0; i<10; i++) {
    if (i > 5) {
        foo( i ); 
        // foo.call( foo, i ) 可以确保 this 指向函数对象 foo 本身
    }
}
     // foo: 6
     // foo: 7
     // foo: 8
     // foo: 9
// foo 被调用了多少次?
console.log( foo.count ); // 0
// 这里利用的词法作用域
console.log( data.count ); // 4

执行 foo.count = 0 时,的确向函数对象 foo 添加了一个属性 count。但是函数内部代码 this.count 中的 this 并不是指向那个函数对象,所以虽然属性名相同,根对象却并不相同。

具名函数可以使用函数名指向自身,匿名函数使用 arguments. callee 指向自身(这个已被弃用?应避免使用匿名函数)

1.2.2 它的作用域(误解)

第二种常见的误解是,this 指向函数的作用域。在某种情况下它是正确的,但是在其他情况下它却是错误的。

JavaScript 内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript 代码访问,它存在于 JavaScript 引擎内部。

第 2 章 this 全面解析

2.1 调用位置

调用位置:函数在代码中被调用的 位置(而不是声明的位置)。

调用栈:为了到达当前执行位置所调用的所有函数(可以把调用栈想象成一个函数调用链)。

看如下代码:

function baz() {
    // 当前调用栈是:baz
    // 因此,当前调用位置是全局作用域
    console.log( "baz" );
    bar(); // <-- bar 的调用位置 
}
function bar() {
    // 当前调用栈是 baz -> bar
    // 因此,当前调用位置在 baz 中
    console.log( "bar" );
    foo(); // <-- foo 的调用位置
}
function foo() {
    // 当前调用栈是 baz -> bar -> foo 
    // 因此,当前调用位置在 bar 中
    console.log( "foo" );
}
baz(); // <-- baz 的调用位置

2.2 绑定规则

执行过程中调用位置如何决定 this 的绑定对象?首先要找到「调用位置」,然后判断用了下列那一条规则。

2.2.1 默认绑定

最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。

function foo () {
    console.log(this.a)
}
var a = 1;
foo() // 2

因为 foo 是使用不带任何修饰的函数引用进行调用的,只能应用「默认绑定」,所以 this 指向全局对象。

严格模式下,全局对象将无法使用默认绑定,因此 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 a = 2;
(function(){
    "use strict";
    foo(); // 2
})();

foo 运行在非严格模式下,this 才会默认绑定到全局对象。
foo 在非严格模式下调用,不会影响 this 的绑定。

2.2.2 隐式绑定

看代码:

function foo() { 
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo 
};
obj.foo(); // 2

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象foo 的调用位置会使用 obj 上下文来引用函数,所以 this 被绑定到了 obj 上。

隐式丢失

「隐式绑定」 的函数会丢失绑定对象,会应用默认绑定,从而吧 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"

barobj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

代码2:

参数传递是一种隐式赋值

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"

2.2.3 显式绑定

使用函数的 callbindapply 方法;

function foo() { 
    console.log( this.a );
}
var obj = { 
    a:2
};
foo.call( obj ); // 2
   // 在调用 foo 时,将 foo 的 this 绑定到 obj 上  
   
function foo() { 
    console.log( this );
}
foo.call( 1 );//  Number {1}

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

显式绑定仍然存在「丢失绑定」问题,但有其他解决方案。

1. 硬绑定

在 bar 内部将 foo 的 this强制绑定到 obj 上,无论后面如何调用 bar,都不会改变 this 的绑定。

function foo() { 
    console.log( this.a );
}
var obj = { 
    a:2
};
var bar = function() { 
    foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬绑定的 bar 不可能再修改它的 this
bar.call( window ); // 2

bind(..) 会返回一个新函数,需要我们手动调用。

var a ={
    name : "Cherry",
    fn : function (a,b) {
        console.log( a + b)
    }
}

var b = a.fn;
b.bind(a,1,2)()  // 3

2. API 调用的「上下文」

第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一 个可选的参数,通常被称为「上下文」(context),其作用和 bind(..) 一样,确保你的回调 函数使用指定的 this

代码:

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

2.2.4 new 绑定

使用构造调用的时候,this会自动绑定在new期间创建的对象上

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[原型]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

2.3 优先级

new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定

2.4 例外绑定

如果你把 null 或者 undefined 作为 this 的绑定对象传入 callapply 或者 bind,这些值
在调用时会被忽略,实际应用的是「默认绑定」规则:

通常 apply 通过传入 null 来展开数组

function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 把数组“展开”成参数
foo.apply( null, [2, 3] ); // a:2, b:3

2.5 this 词法

箭头函数中 this 由外层(函数或全局)作用域来决定.

箭头函数的绑定无法被修改。(new 也不行!).

如下代码:foo() 内部创建的箭头函数会捕获调用时 foo()this。由于 foo()this 绑定到 obj1bar (引用箭头函数)的 this 也会绑定到 obj1.

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, 不是 3 !

第 3 章 对象

3.2 类型

stringnumbernullbooleanundefinedobject

typeof null 时会返回字符串 object。实际上,null 本身是基本类型。

3.3 内容

.a 语法通 常被称为“属性访问”

["a"] 语法通 常被称为“键访问”

对象中,属性名永远都是字符串,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串 [object Object]

3.3.4 复制

// 深复制
var newObj = JSON.parse( JSON.stringify( someObj ) );

ES6 定义了 Object.assign(targetObject, sourceObject) 方法来实现浅复制.

3.3.6 不变性

希望属性或对象是不可变的。「所有的方法创建的都是浅不变性,只会影响目标对象和 它的直接属性」,如果目标对象引用了其他对象(数组,对象,函数等),==其他对象的内容不受影响==。

可通过下列方法实现深不可变性(即 如果目标对象引用了其他对象(数组,对象,函数等),其他对象的内容==会受影响==)

1、对象常量

结合 writable:false 和 configurable:false 实现。

2、禁止扩展

禁止一个对象添加新属性并且保留已有属性,可以使用 Object.prevent Extensions(..)

var myObject = { 
    a:2
 };
 Object.preventExtensions( myObject );
 myObject.b = 3;
 myObject.b; // undefined
 
 //在非严格模式下,创建属性 b 会静默失败。
 // 在严格模式下,将会抛出 TypeError 错误。

3、密封

Object.seal(..) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false

密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以 修改属性的值)

4、冻结

Object.freeze(..) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal(..) 并把所有“数据访问”属性标记为 writable:false

3.3.9 Getter和Setter

对象默认的 [[Put]][[Get]] 操作分别可以控制属性值的设置和获取。在 ES5 中可以使用 gettersetter 部分改写默认操作,但是只能应用在==单个属性==上,无法 应用在整个对象上

3.3.10 存在性

in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中.

hasOwnProperty(..) 只会检查属性是否在对象自身中,不会检查 [[Prototype]] 链.

第 4 章 混合对象「类」

面向类的设计模式:实例化( instantiation )、继承( inheritance )、多态( polymorphism )。

4.1 类理论

类意味着复制。

4.1.1 “类”设计模式

你可能从来没把类作为设计模式来看待,因为讨论得最多的是面向对象设计模式

类不是必须的编程基础,而是一种可选的代码抽象。

4.1.2 JavaScript中的“类”

JavaScript 中实际上没有「类」,只有一些近似类的语法元素(比如 newinstanceof 以及 ES6 中的 class 关键字。)

4.2 类的机制

类仅仅是一个抽象的表示,需要先实例化才能对其进行操作。

4.3 类的继承

多态并不表示子类和父类有关联,子类得到的只是父类的一份副本。类的继承其实就是==复制==。

4.4 混入

在继承或者实例化时,JavaScript 的对象机制并不会自动执行复制行为。简单来说, JavaScript 中只有对象,并不存在可以被实例化的“类”。在其他语言中类表现出来的都是复制行为,因此 JavaScript 开发者也想出了一个方法来 模拟类的复制行为,这个方法就是==混入==

4.5 小结

传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类 中。

多态(在继承链的不同层次名称相同但是功能不同的函数)看起来似乎是从子类引用父 类,但是本质上引用的其实是复制的结果。

JavaScript 并不会(像类那样)自动创建对象的副本。

混入模式(无论显式还是隐式)可以用来模拟类的复制行为,但是通常会产生丑陋并且脆 弱的语法,比如显式伪多态(OtherObj.methodName.call(this, ...)),这会让代码更加难 懂并且难以维护。

显式混入实际上无法完全模拟类的复制行为,因为对象只能复制引用.

第 5 章 原型

5.1 [[Prototype]]

JavaScript 内置属性,是对其他对象的引用,对象在创建时 [[Prototype]] 属性都会被赋予一个非空的值.

当你通过各种语法进行属性查找时都会查找 [[Prototype]] 链,直到找到属性或者
查找完整条原型链。

5.1.1 Object.prototype

[[Prototype]] 链最终都会指向内置的 Object.prototype

5.1.2 属性设置和屏蔽

newObject.foo = 'demo'

上面的代码,如果 newObject 中存在 foo 属性,则上述语句只会修改已有属性。

如果 foo 不存在 newObject 上,则开始遍历 [[Prototype]],如果在原型链上找不到 foo,则将 foo 直接添加在 newObject 上。

如果 foo 即存在于 newObject 上,又存在于 [[Prototype]] 上,[[Prototype]] 上的 foo 会被屏蔽。

如果 foo 不存在于 newObject 上,存在于 [[Prototype]] 上,则有三种情况:

  1. 如果 [[Prototype]] 上存在 foo且 ==没有被标记为只读==,那就直接在 newObject 上新添加一个 foo 属性;
  2. 如果 [[Prototype]] 上存在 foo 且 ==被标记为只读==,严格模式下会报错,非严格模式下会忽略。
  3. 如果 [[Prototype]] 上存在 foo 同时它是一个 setter,那么 foo 不会被添加到 newObject 上,也不会重新定义 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

尽管 myObject.a++ 看起来应该(通过委托)查找并增加 anotherObject.a 属性,但是别忘 了 ++ 操作相当于 myObject.a = myObject.a + 1。因此 ++ 操作首先会通过 [[Prototype]] 查找属性 a 并从 anotherObject.a 获取当前属性值 2,然后给这个值加 1,接着用 [[Put]] 将值 3 赋给 myObject 中新建的屏蔽属性 a.

5.4 对象关联

5.4.1 创建关联

Object.create()

var foo = {
something: function() {
             console.log( "Tell me something good..." );
         }
};
var bar = Object.create( foo ); 
bar.something(); // Tell me something good...

Object.create(..) 会创建一个新对象( bar )并把它关联到我们指定的对象( foo )

new 的构造函数调用会生成 .prototype.constructor 引用

部分实现 Object. create(..) 的功能:

if (!Object.create) { 
    Object.create = function(o) {
        function F(){}
        F.prototype = o; 
        return new F();
}; }

5.4 小结

关联两个对象最常用的方法是使用 new 关键词进行函数调用,会把新对象的 .prototype 属性关联到“其他对象”。

[[Prototype]] 机制就是指对象中的一个内部链接引用另一个对象,这个机制的本质就是对象之间的关联关系。

第 6 章 行为委托

6.6 小结

行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。

JavaScript 的 [[Prototype]] 机制本质上就是行为委托机制

文章归类于: 开卷有益

文章标签: #Javascript

版权声明: 自由转载-署名-非商用