《你不知道的js(上卷)》笔记2(this和对象原型)
学了多种语言,发现
javascript
的this
是最难以捉摸的。this
不就是指向当前对象的指针吗?可是结合上下文来看,却又往往不知道this
到底指的是谁了,所以Javascript
最主要的两个知识点,除了闭包,就是this
了。
1. 关于this
this
关键字是javascript
中最复杂的机制之一。它是一个很特别的关键字,被自动定义在 所有函数的作用域中。
this
提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计
得更加简洁并且易于复用。
function identify() {
return this.name.toUpperCase();
}
var me = {
name: "Kyle"
};
identify.call( me ); // KYLE
this
并不像我们所想的那样指向函数本身。
function 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
函数内部代码this.count
中的this
并不是指向那个函数对象,所以虽然属性名相同,根对象却并不相同,困惑随之产生。
函数内部代码this.count
最终值为NaN
,同时也是全局变量。
可以使用函数名称标识符来代替this
来引用函数对象。这样,更像是静态变量。
function foo(num) {
foo.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
console.log( foo.count ); // 4
另外一种方式是强制this
指向foo
函数对象。
function foo(num) {
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo.call(foo, i );
}
}
console.log( foo.count ); // 4
this
到底是什么
this
是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件。this
的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
调用位置
函数被调用的位置。每个函数的 this 是在调用 时被绑定的,完全取决于函数的调用位置,因为它决定了this
的绑定。
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 的调用位置
1.1 绑定规则
默认绑定
声明在全局作用域中的变量就是全局对象的一个同名属性。
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
在本 例中,函数调用时应用了this
的默认绑定,因此this
指向全局对象。
foo()
是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。
如果使用严格模式,那么全局对象将无法使用默认绑定,因此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 obj = {
a: 2,
foo: foo
};
var obj1 = {
a: 42,
obj: obj
};
obj.foo(); // 2
obj1.obj.foo(); // 2
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this
绑定到这个上下文对象。因为调 用foo()
时this
被绑定到obj
,因此this.a
和obj.a
是一样的。
对象属性引用链中只有最顶层或者说最后一层会影响调用位置。
隐式绑定的函数可能会丢失绑定对象,而应用默认绑定,把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 doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置!
}
doFoo( obj.foo ); // "oops, global"
bar
是obj.foo
的一个引用,bar()
其实是一个不带任何修饰的函数调用。
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果一样。
显式绑定
可以使用函数的call(..)
和apply(..)
方法实现显式绑定。
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
如下例子,无论bar
绑定到哪个对象上,foo
始终绑定在obj
上,称之为硬绑定。
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
var bar = function() {
foo.call( obj );
};
bar.call( window ); // 2
在 ES5 中提供了内置的方法Function.prototype.bind
就是硬绑定。
如果你把null
或者undefined
作为this
的绑定对象传入call
、apply
或者 bind
,这些值在调用时会被忽略,实际应用的是默认绑定规则。
new
绑定
JavaScript
中new
的机制实 际上和面向类的语言完全不同。
在JavaScript
中,构造函数只是一些 使用new
操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。
使用new
来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
-
创建(或者说构造)一个全新的对象。
-
这个新对象会被执行[[原型]]连接。
-
这个新对象会绑定到函数调用的this。
-
如果函数没有返回其他对象,那么
new
表达式中的函数调用会自动返回这个新对象。
function foo(p1,p2) {
this.val = p1 + p2;
}
// 之所以使用 null 是因为在本例中我们并不关心硬绑定的 this 是什么
// 反正使用 new 时 this 会被修改
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" );
baz.val; // p1p2
绑定规则优先级:new
绑定 > 显式绑定 > 隐式绑定 > 默认绑定
箭头函数无法使用以上四种绑定规则。
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
2. 对象
对象的两种形式定义:声明(文字)形式和构造形式。
var myObj = {
key: value
// ...
};
var myObj = new Object();
myObj.key = value;
六种主要类型: string
,number
,boolean
,null
,undefined
,object
除object
外的5种类型为简单基本类型,本身并不是对象,但是typeof null
会返回字符串 "object"。
内置对象:String
,Number
,Boolean
,Object
,Function
,Array
,Date
,RegExp
,Error
var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
var strObject = new String( "I am a string" );
typeof strObject; // "object"
strObject instanceof String; // true
// 检查 sub-type 对象
Object.prototype.toString.call( strObject ); // [object String]
在必要时语言会自动把字符串字面量转换成一个String
对象,可以访问属性和方法。
对于Object
、Array
、Function
和RegExp
来说,无论使用文字形式还是构 造形式,它们都是对象,不是字面量。
属性
属性名永远是字符串,虽然在数组下标中使用的的确是数字,但是在对象属性名中数字会被转换成字符串。
ES6 增加了可计算属性名,最常用的场景可能是 ES6 的符号(Symbol)。
var prefix = "foo";
var myObject = {
[prefix + "bar"]:"hello",
[prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
如果你试图向数组添加一个属性,但是属性名“看起来”像一个数字,那它会变成 一个数值下标
var myArray = [ "foo", 42, "bar" ];
myArray["3"] = "baz";
myArray.length; // 4
myArray[3]; // "baz"
复制对象
对于JSON
安全的对象来说,有一种巧妙的复制方法:
var newObj = JSON.parse( JSON.stringify( someObj ) );
ES6 定义了Object.assign(..)
方法来实现浅复制。
属性描述符
三个特性:writable(可写)、 enumerable(可枚举)和 configurable(可配置)。
var myObject = {
a:2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
在创建普通属性时属性描述符会使用默认值,我们也可以使用 Object.defineProperty(..)
来添加一个新属性或者修改一个已有属性(如果它是configurable
)并对特性进行设置。
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
} );
myObject.a; // 2
writable
决定是否可以修改属性的值,如果在严格模式下,这 种方法会出错(TypeError)。
把configurable
修改成 false 是单向操作,无法撤销!不管是不是处于严格模式,尝 试修改一个不可配置的属性描述符都会出错(TypeError)。
属性是不可配置时使用 delete
也会失败。
如果把enumerable
设置成false
,这个属性就不会出现在枚举中(比如for..in循环
),虽然仍 然可以正常访问它。
不变性
常量: 结合writable:false
和configurable:false
就可以创建一个真正的常量属性(不可修改、 重定义或者删除)
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
});
禁止扩展: 如果你想禁止一个对象添加新属性并且保留已有属性,可以使用Object.preventExtensions(..)
密封: Object.seal(..)
会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(..)
并把所有现有属性标记为configurable:false
。
冻结: Object.freeze(..)
会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(..)
并把所有“数据访问”属性标记为writable:false
,这样就无法修改它们 的值。
get和set
var myObject = {
// 给 a 定义一个 getter
_a:2,
get a() {
return this.a;
},
// 给 a 定义一个 setter
set a(_a){
this._a = _a;
}
};
Object.defineProperty(
myObject, // 目标对象
"b", // 属性名
{
// 描述符
// 给 b 设置一个 getter
get: function(){
return this.a * 2
},
// 确保 b 会出现在对象的属性列表中
enumerable: true
}
);
myObject.a; // 2
myObject.b; // 4
在不访问属性值的情况下判断对象中是否存在这个属性:
var myObject = {
a:2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false
in
操作符会检查属性是否在对象及其 [[Prototype]] 原型链中,相比之下,hasOwnProperty(..)
只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 链。
有的对象可能没有连接到Object.prototype
,可以使用Object.prototype.hasOwnProperty. call(myObject,"a")
进行判断。
propertyIsEnumerable(..)
会检查给定的属性名是否直接存在于对象中(而不是在原型链 上)并且满足enumerable:true
。
Object.keys(..)
会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(..)
会返回一个数组,包含所有属性,无论它们是否可枚举。
数组有内置的@@iterator
,因此for..of
可以直接应用在数组上。
var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { done:true }
手动定义@@iterator
:
var myObject = { 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)
};
} };
} } );
3. 原型
JavaScript
中的对象有一个特殊的 [[Prototype]] 内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时 [[Prototype]] 属性都会被赋予一个非空的值。
对于默认的 [[Get]] 操作来说,第一步是检查对象本身是 否有这个属性,如果有的话就使用它。但是如果不存在与对象本身,就需要会继续访问对象的 [[Prototype]] 链。
var anotherObject = {
a:2
};
// 创建一个关联到 anotherObject 的对象
var myObject = Object.create( anotherObject );
myObject.a; // 2
任何可以通过原型链访问到并且是enumerable
的属性都会被枚举。
使用in
操作符来检查属性在对象中是否存在时,同样会查找对象的整条原型链(无论属性是否可枚举)。
所有普通的 [[Prototype]] 链最终都会指向内置的Object.prototype
,它包含 JavaScript
中许多通用的功能,比如.toString()
。
原型链上层时myObject.foo = "bar"
会出现的三种情况:
-
如果[[Prototype]]链上层存在名为foo的普通数据访问属性并且不是只读,就会直接在 myObject 中添加一个名为 foo 的新 属性,它是屏蔽属性。
-
如果[[Prototype]]链上层存在名为foo的普通数据访问属性并且只读,则无法修改已有属性或者在 myObject 上创建屏蔽属性。
-
如果在[[Prototype]]链上层存在
foo
并且它是一个setter
,那就一定会 调用这个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
++
操作首先会通过 [[Prototype]] 查找属性a
并从anotherObject.a
获取当前属性值2
,然后给这个值加1
,接着用 [[Put]] 将值3
赋给myObject
中新建的屏蔽属性a
。
类
所有的函数默认都会拥有一个 名为prototype
的公有并且不可枚举的属性,它会指向另一个对象,这个对象通常被称为该对象的原型。
function Foo() {
// ...
}
Foo.prototype; // { }
在方法射调用new
时创建对象时,该对象最后会被关联到这个方法的prototype
对象上。
function Foo() {
// ...
}
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true
new Foo()
会生成一个新对象,这个新对象的内部链接[[Prototype]]关联的是 Foo.prototype
对象。最后我们得到了两个对象,它们之间互相关联。
在JavaScript
中,我们并不会将一个对象(“类”)复制到另一个对象(“实例”),只是将它们关联起来。这个机制通常被称为原型继承。
构造函数
使用new
创建的对象会调用类的构造函数。
function Foo() {
// ...
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true
Foo.prototype
默认有一个公有并且不可枚举的属性.constructor
,这个属性引用的是对象关联的函数。
可以看到通过“构造函数”调用new Foo()
创建的对象也有一个.constructor
属性,指向 “创建这个对象的函数”。
函数本身并不是构造函数,然而,当你在普通的函数调用前面加上new
关键字之后,就会把这个函数调用变成一个“构造函数 调用”。实际上,new
会劫持所有普通函数并用构造对象的形式来调用它。
在JavaScript
中对于“构造函数”最准确的解释是,所有带new
的函数调用。
如果 你创建了一个新对象并替换了函数默认的.prototype
对象引用,那么新对象并不会自动获 得.constructor
属性。
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
可以给 Foo.prototype 添加一个 .constructor 属性,不过这需要手动添加一个符
合正常行为的不可枚举属性。
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
Object.defineProperty( Foo.prototype, "constructor" , {
enumerable: false,
writable: true,
configurable: true,
value: Foo // 让 .constructor 指向 Foo
});
继承
典型的“原型风格”:
function Foo(name) {
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.constructor 了
// 如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.myLabel = function() {
return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"
ES6 开始可以直接修改现有的Bar.prototype
Object.setPrototypeOf( Bar.prototype, Foo.prototype );
检查一个实例的继承关系
// 非常简单:b 是否出现在 c 的 [[Prototype]] 链中
b.isPrototypeOf( c );
Object.getPrototypeOf( a ) === Foo.prototype; // true
// 非标准的方法访问内部 [[Prototype]] 属性
a.__proto__ === Foo.prototype; // true
写了这么多,实在写不下去了。《你不知道的js》都是满满的干货,笔记记到这里发现好多知识都非常有用,没办法省略。几下这些笔记,也是为了复习一下,以免忘得太快了,所以受益的终究还是自己呀。