第二部分 this 和对象原型
第 1 章 关于 this
this:Javascript 关键字,被自动定义在所有函数的作用域中。
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"
bar 是 obj.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 显式绑定
使用函数的
call、bind和apply方法;
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期间创建的对象上
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行[[原型]]连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
2.3 优先级
new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定
2.4 例外绑定
如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 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 绑定到 obj1, bar (引用箭头函数)的 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 类型
string、number、null、boolean、undefined、object
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 中可以使用getter和setter部分改写默认操作,但是只能应用在==单个属性==上,无法 应用在整个对象上
3.3.10 存在性
in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中.
hasOwnProperty(..) 只会检查属性是否在对象自身中,不会检查 [[Prototype]] 链.
第 4 章 混合对象「类」
面向类的设计模式:实例化( instantiation )、继承( inheritance )、多态( polymorphism )。
4.1 类理论
类意味着复制。
4.1.1 “类”设计模式
你可能从来没把类作为设计模式来看待,因为讨论得最多的是面向对象设计模式
类不是必须的编程基础,而是一种可选的代码抽象。
4.1.2 JavaScript中的“类”
JavaScript中实际上没有「类」,只有一些近似类的语法元素(比如new、instanceof以及 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]] 上,则有三种情况:
- 如果
[[Prototype]]上存在foo且 ==没有被标记为只读==,那就直接在newObject上新添加一个foo属性; - 如果
[[Prototype]]上存在foo且 ==被标记为只读==,严格模式下会报错,非严格模式下会忽略。 - 如果
[[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]] 机制本质上就是行为委托机制