来自《你不知道的JavaScript(上卷)》第二部分
1.关于this
举例1:
foo被调用了几次?
function foo(num) {
console.log( "foo: " + num );
// 记录 foo 被调用的次数
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( foo.count ); // 0
举例2:
匿名函数无法指向自身
function foo() {
foo.count = 4; // foo 指向它自身
}
setTimeout( function(){
// 匿名(没有名字的)函数无法指向自身
}, 10 );
解决办法:
(1)治标不治本:创建另一个带有count属性的对象
function foo(num) {
console.log( "foo: " + num );
// 记录 foo 被调用的次数
data.count++;
}
var data = {
count: 0
};
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( data.count ); // 4
(2)治标不治本:用foo标识符替代this来引用函数对象。
同样回避了this的问题,并且完全依赖于变量foo的词法作用域。
function foo(num) {
console.log( "foo: " + num );
// 记录 foo 被调用的次数
foo.count++;
}
foo.count=0
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( foo.count ); // 4
(3)强制this指向foo函数对象
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 );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( foo.count ); // 4
1.2关于this的误解
(1)误解this是指向自身的
(2)误解this一直都指向函数的作用域
this在任何情况下都不指向函数的词法作用域
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 的调用位置
查看调用栈的方法:
就本例来说,你可以在浏览器开发者工具中给 foo() 函数的第一行代码设置一个断点,或者直接在第一行代码之前插入一条 debugger; 语句。运行代码时,调试器会在那个位置暂停,同时会展示当前位置的函数调用列表,这就是你的调用栈。
2.2绑定规则
2.2.1默认绑定
独立函数调用
无法应用其他规则时的默认规则
function foo(){
console.log(this.a);
}
var a=2;
foo();//2
分析:
- 在全局作用域中声明变量var a=2;
- 调用foo()时,this指向全局对象,因为在本例中,函数调用时应用了this的默认绑定。
- foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。
- 如果使用严格模式,那么全局对象将无法使用默认绑定,因此this会绑定到undefined
这里有一个微妙但是非常重要的细节,虽然 this 的绑定规则完全取决于调用位置,但是只有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象;严格模式下与 foo()的调用位置无关:
function foo(){
console.log(this.a);
}
var a=2;
(function(){
"use strict";
foo();//2
})();
2.2.2 隐式绑定
隐式绑定的一般适用情况:
调用位置有上下文对象,或被某个对象拥有或包含
function foo(){
console.log(this.a);
}
var obij={
a=2,
foo:foo
};
obj.foo();//2
分析:
- 无论是直接在obj中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj对象。
-但调用位置会适用obj上下文来引用函数,因此可以说函数被调用时obj对象“拥有”或“包含”它。 - 当foo()被调用时,它的落脚点指向obj对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时,this被绑定到obj,因此this.a和obj.a是一样的。
注意: - 对象属性引用链中只有最定测或者最后一层会影响调用位置。
function foo(){
console.log(this.a);
}
var obj2={
a:42,
foo:foo
};
var obj1={
a:2,
obj2:obj2
};
obj1.obj2.foo();//42
隐式丢失
- 被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否严格模式。
例子1:
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实际上引用的是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是全局对象的属性
foFoo(obj.foo);//"oops,global"
例子3:
把函数传入语言内置的函数而不是传入你自己生命的函数,也会发生隐式丢失,从而应用默认绑定。
function foo() {
console.log( this.a );
}
var obj={
a:2,
foo:foo
};
var a="oops, global"; // a 是全局对象的属性
setTimeout(obj.foo,100 ); // "oops, global"
JavaScript 环境中内置的 setTimeout() 函数实现和下面的伪代码类似:
function setTimeout(fn,delay) {
// 等待 delay 毫秒
fn(); // <-- 调用位置!
}
2.2.3显示绑定
如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?
call()或apply
举例:
通过foo.call(),可以在调用foo时强制把它的this绑定到obj上。
function foo(){
console.log(this.a);
}
var obj={
a:2
};
foo.call(obj);//2
绑定丢失的解决办法:
(1)硬绑定
例子1:
我们创建了函数 bar(),并在它的内部手动调用
了 foo.call(obj),因此强制把 foo 的 this 绑定到了 obj。无论之后如何调用函数 bar,它总会手动在 obj 上调用 foo。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。
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
例子1:
创建一个包裹函数,传入所有的参数并返回接受到的所有值:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a:2
};
var bar = function() {
return foo.apply( obj, arguments );
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5
例子2:
创建一个可以重复使用的辅助函数
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn, obj) {
return function() {
return fn.apply( obj, arguments );
};
}
var obj = {
a:2
};
var bar = bind( foo, obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
由于硬绑定是一种非常常用的模式,所以在 ES5 中提供了内置的方法 Function.prototype.
bind,它的用法如下:
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
console.log( b ); // 5
(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
第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和 bind(…) 一样,确保你的回调函数使用指定的 this。
这些函数实际上就是通过 call(…) 或者 apply(…) 实现了显式绑定,这样你可以少些一些
代码。
2.2.4new绑定
在 JavaScript 中,构造函数只是一些使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行 [[ 原型 ]] 连接。
- 这个新对象会绑定到函数调用的 this。
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
2.3优先级
我们需要找到函数的调用位置并判断应当应用哪条规则。但是,如果某个调用位置可以应用多条规则该怎么办?此时需要给这些规则设定优先级。
** new绑定>显示绑定>隐式绑定>全局对象绑定**
- 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。
var bar = new foo() - 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是指定的对象。
var bar = foo.call(obj2) - 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。
var bar = obj1.foo() - 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象。
var bar = foo()
注意:
在new中使用硬绑定函数的情况
作用:
预先设置函数的一些参数,这样在使用new 进行初始化时就可以只传入其余的参数。bind(…) 的功能之一就是可以把除了第一个参数(第一个参数用于绑定 this)之外的其他参数都传给下层的函数(这种技术称为“部分应用”,是“柯里化”的一种)
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
2.4绑定例外
2.4.1被忽略的this
如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
那么什么情况下你会传入 null 呢?
一种非常常见的做法是使用 apply(…) 来“展开”一个数组,并当作参数传入一个函数。类似地,bind(…) 可以对参数进行柯里化(预先设置一些参数。
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 把数组“展开”成参数
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3
这两种方法都需要传入一个参数当作 this 的绑定对象。如果函数并不关心 this 的话,你仍然需要传入一个占位值,这时 null 可能是一个不错的选择。
使用 null 来忽略 this 绑定的缺点
如果某个函数确实使用了this(比如第三方库中的一个函数),那默认绑定规则会把 this 绑定到全局对象(在浏览器中这个对象是 window),这将导致不可预计的后果(比如修改全局对象)
更安全的this
一种“更安全”的做法是传入一个特殊的对象,把 this 绑定到这个对象不会对你的程序产生任何副作用。就像网络(以及军队)一样,我们可以创建一个“DMZ”(demilitarizedzone,非军事区)对象——它就是一个空的非委托的对象。(第五章、第六章)
后续补充
2.4.2间接引用
有可能(有意或者无意地)创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。间接引用最容易在赋值时发生。
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() 而不是p.foo() 或者 o.foo()。根据我们之前说过的,这里会应用默认绑定。
- 注意:对于默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格式,this 会被绑定到 undefined,否则this 会被绑定到全局对象。
2.4.3软绑定
硬绑定的优点:
可以把 this 强制绑定到指定的对象(除了使用 new时),防止函数调用应用默认绑定规则。
硬绑定的缺点:
硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改 this。
如果可以给默认绑定指定一个全局对象和 undefined 以外的值,那就可以实现和硬绑定相
同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。
function foo() {
console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call( obj3 ); // name: obj3 <---- 看!
setTimeout( obj2.foo, 10 );
// name: obj <---- 应用了软绑
软绑定版本的 foo() 可以手动将 this 绑定到 obj2 或者 obj3 上,但如果应用默认绑定,则会将 this 绑定到 obj。
2.5this词法
箭头函数不使用 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, 不是 3 !
分析:
foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1,bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不行)
3对象
3.1语法
构造形式和文字形式生成的对象是一样的。唯一的区别是,在文字声明中你可以添加多个键 / 值对,但是在构造形式中你必须逐个添加属性。
3.2类型
对象是JavaScript的基础
简单基本类型(String、boolean、number、null和undefined)本身并不是对象。
JavaScript 中有许多特殊的对象子类型,我们可以称之为复杂基本类型。函数就是对象的一个子类型(从技术角度来说就是“可调用的对象”)。可以像操作其他对象一样操作函数。
数组也是对象的一种类型。
内置对象
• String
• Number
• Boolean
• Object
• Function
• Array
• Date
• RegExp
• Error
这些内置对象从表现形式来说很像其他语言中的类型(type)或者类(class),比如 Java
中的 String 类。
但是在 JavaScript 中,它们实际上只是一些内置函数。这些内置函数可以当作构造函数(由 new 产生的函数调用)来使用,从而可以构造一个对应子类型的新对象。
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]
3.3内容
对象的内容是由一些存储在特定命名位置的(任意类型的)值组成的,我们称之为属性。
3.3.1 可计算属性名
ES6 增加了可计算属性名,可以在文字形式中使用 [] 包裹一个表达式来当作属性名
var prefix = "foo";
var myObject = {
[prefix + "bar"]:"hello",
[prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
3.3.2属性与方法
属性访问
3.3.3数组
数组支持[ ]访问形式,同时也可以通过索引访问。
注意:如果你试图向数组添加一个属性,但是属性名“看起来”像一个数字,那它会变成一个数值下标(因此会修改数组的内容而不是添加一个属性):
var myArray = [ "foo", 42, "bar" ];
myArray["3"] = "baz";
myArray.length; // 4
myArray[3]; // "baz"
3.3.4复制对象
ES6定义了Object.assign()方法来实现浅复制。
Object.assign(…) 方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。
它会遍历一个或多个源对象的所有可枚举(enumerable,参见下面的代码)的自有键(owned key)并把它们复制(使用 = 操作符赋值)到目标对象,最
后返回目标对象。
3.3.5属性描述符
从 ES5 开始,所有的属性都具备了属性描述符。
value(值)、writable(可写)、
enumerable(可枚举)和 configurable(可配置)
var myObject = {
a:2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
writable
决定是否可以修改属性的值
当writable为false时
在非严格模式下,修改某对象的某属性的值,不会报错,但会修改失败,还是原来的值。
在严格模式下,修改某对象的某属性的值,会报错,TypeError,表示无法修改一个不可写的属性。
configurable
只要属性是可配置的,就可以使用 defineProperty(…) 方法来修改属性描述符
var myObject = {
a:2
};
myObject.a = 3;
myObject.a; // 3
Object.defineProperty( myObject, "a", {
value: 4,
writable: true,
configurable: false, // 不可配置
enumerable: true
} );
myObject.a; // 4
myObject.a = 5;
myObject.a; // 5
Object.defineProperty( myObject, "a", {
value: 6,
writable: true,
configurable: true,
enumerable: true
} ); // TypeError
不论是否处于严格模式,尝试修改一个不可配置的属性描述符都会出错。
把 configurable 修改成false 是单向操作,无法撤销。
除了无法修改,configurable:false 还会禁止删除这个属性。
var myObject = {
a:2
};
myObject.a; // 2
delete myObject.a;
myObject.a; // undefined
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: false,
enumerable: true
} );
myObject.a; // 2
delete myObject.a;
myObject.a; // 2
最后一个 delete 语句(静默)失败了,因为属性是不可配置的。
在本例中,delete 只用来直接删除对象的(可删除)属性。如果对象的某个属性是某个对象 / 函数的最后一个引用者,对这个属性执行 delete 操作之后,这个未引用的对象 / 函数就可以被垃圾回收。但是,不要把 delete 看作一个释放内存的工具(就像 C/C++ 中那样),它就是一个删除对象属性的操作,仅此而已。
enumerable
这个描述符控制的是属性是否会出现在对象的属性枚举中,比如说for…in 循环。如果enumerable 设置成 false,这个属性就不会出现在枚举中,虽然仍然可以正常访问它。
3.3.6不变性
所有的方法创建的都是浅不变形,也就是说,它们只会影响目标对象和它的直接属性。如果目标对象引用了其他对象(数组、对象、函数,等),其他对象的内容不受影响,仍然是可变的。
myImmutableObject.foo; // [1,2,3]
myImmutableObject.foo.push( 4 );
myImmutableObject.foo; // [1,2,3,4]
假设代码中的 myImmutableObject 已经被创建而且是不可变的,但是为了保护它的内容
myImmutableObject.foo,你还需要使用下面的方法让 foo 也不可变。
(1)对象常量
结合 writable:false 和 configurable:false 就可以创建一个真正的常量属性(不可修改、重定义或者删除):
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
} );
(2)禁止扩展
如果你想禁止一个对象添加新属性并且保留已有 属性,可以使用Object.preventExtensions(…):
在非严格模式下,创建属性 b 会静默失败。在严格模式下,将会抛出 TypeError 错误。
var myObject = {
a:2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined
(3)密封
Object.seal(…) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(…) 并把所有现有属性标记为 configurable:false。
所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)
(4)冻结
Object.freeze(…) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(…) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值。
这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改(不过就像我们之前说过的,这个对象引用的其他对象是不受影响的)。
“深度冻结”对象的方法:
首先在这个对象上调用 Object.freeze(…),然后遍历它引用的所有对象并在这些对象上调用 Object.freeze(…)。但有可能会冻结其他(共享)对象。
3.3.7[[Get]]
在语言规范中,myObject.a 在 myObject 上实际上是实现了 [[Get]] 操作,有点像函数调用:[[Get]] ()
var myObject = {
a: 2
};
myObject.a; // 2
myObject.b; // undefined
(1)如果对象里有名称相同的属性
对象默认的内置 [[Get]] 操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值。
(2)如果对象里没有名称相同的属性
[[Get]] 会遍历可能存在的 [[Prototype]] 链,也就是原型链。
注意:
用 [[Get]] 访问属性和访问变量是不一样的,如果你引用了一个当前词法作用域中不存在的变量,并不会像对象属性一样返回 undefined,而是会抛出一个 ReferenceError 异常。
3.3.8[[Put]]
[[Put]] 被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性(这是最重要的因素)。
如果已经存在这个属性,[[Put]] 算法大致会检查下面这些内容。
- 属性是否是访问描述符(参见 3.3.9 节)?如果是并且存在 setter 就调用 setter。
- 属性的数据描述符中 writable 是否是 false ?如果是,在非严格模式下静默失败,在严格模式下抛出 TypeError 异常。
- 如果都不是,将该值设置为属性的值。
3.3.9Getter和Setter
在 ES5 中可以使用 getter 和 setter 部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。getter 是一个隐藏函数,会在获取属性值时调用。setter 也是一个隐藏函数,会在设置属性值时调用。
当你给一个属性定义 getter、setter 或者两者都有时,这个属性会被定义为“访问描述符”(和“数据描述符”相对)。对于访问描述符说,JavaScript 会忽略它们的 value 和writable 特性,取而代之的是关心 set 和 get(还有configurable 和 enumerable)特性。
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; // 2
myObject.b; // 4
myObject.a = 3;
myObject.a; // 2
由于我们只定义了 a 的 getter,所以对 a 的值进行设置时 set 操作会忽略赋值操作,不会抛出错误。
而且即便有合法的 setter,由于我们自定义的 getter 只会返回 2,所以 set 操作是没有意义的。
为了让属性更合理,还应当定义 setter,和你期望的一样,setter 会覆盖单个属性默认的[[Put]](也被称为赋值)操作。通常来说 getter和setter 是成对出现的(只定义一个的话通常会产生意料之外的行为)
var myObject = {
// 给 a 定义一个 getter
get a() {
return this._a_;
},
// 给 a 定义一个 setter
set a(val) {
this._a_ = val * 2;
}
};
myObject.a = 2;
myObject.a; // 4
3.3.10存在性
当myObject.a 的属性访问返回值是undefined时,这个值有可能是属性中存储的 undefined,也可能是因为属性不存在所以返回 undefined。如何区分这两种情况呢?
var myObject = {
a:2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false
- in操作符会检查属性是否在对象及其[[Protocoltype]]原型链中。
- hasOwnProperty(…)只会检查属性是否在myObject对象中,不会检查[[Prototype]]链。
所有的普通对象都可以通过对于Object.prototype 的委托来访问hasOwnProperty(…),但是有的对象可能没有连接到 Object.prototype(通过 Object.create(null) 来创建)。在这种情况下,形如 myObejct.hasOwnProperty(…)就会失败。
这时可以使用一种更加强硬的方法来进行判 断:Object.prototype.hasOwnProperty.call(myObject,“a”),它借用基础的 hasOwnProperty(…) 方法并把它显式绑定到 myObject 上。
(1)枚举
enumerable
例子1:
“可枚举”就相当于“可以出现在对象属性的遍历中。
var myObject = { };
Object.defineProperty(
myObject,
"a",
// 让 a 像普通属性一样可以枚举
{ enumerable: true, value: 2 }
);
Object.defineProperty(
myObject,
"b",
// 让 b 不可枚举
{ enumerable: false, value: 3 }
);
myObject.b; // 3
("b" in myObject); // true
myObject.hasOwnProperty( "b" ); // true
// .......
for (var k in myObject) {
console.log( k, myObject[k] );
}
// "a" 2
例子2:
另一种区分属性是否可枚举的方式
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(…) 会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(…)
会返回一个数组,包含所有属性,无论它们是否可枚举。 - 区别:
- in 和 hasOwnProperty(…) 的区别在于是否查找 [[Prototype]] 链(in会查找原型链)
- Object.keys(…)和Object.getOwnPropertyNames(…) 都只会查找对象直接包含的属性。
3.4遍历
遍历属性的值的方法
对于数组来说
-
如果是数值索引的数组,可以使用for in循环
-
ES5 中增加了一些数组的辅助迭代器,包括forEach(…)、every(…) 和 some(…),唯一的区别就是它们对于回调函数返回值的处理方式不同。
- forEach(…) 会遍历数组中的所有值并忽略回调函数的返回值;
- every(…) 会一直运行直到回调函数返回false(或者“假”值);
- some(…) 会一直运行直到回调函数返回true(或者“真”值);
- every(…) 和 some(…) 中特殊的返回值和普通 for 循环中的 break 语句类似,它们会提前终止遍历;
-
使用 for…in 遍历对象是无法直接获取属性值的,因为它实际上遍历的是对象中的所有可
枚举属性,你需要手动获取属性值。- 遍历数组下标时采用的是数字顺序(for 循环或者其他迭代器),但是遍历对
象属性时的顺序是不确定的,在不同的JavaScript 引擎中可能不一样。因此,在不同的环境中需要保证一致性时,一定不要相信任何观察到的顺序,它们是不可靠的。
- 遍历数组下标时采用的是数字顺序(for 循环或者其他迭代器),但是遍历对
如何直接遍历值而不是数组下标(或者对象属性)?
ES6增加了for…of循环语法
var myArray = [ 1, 2, 3 ];
for (var v of myArray) {
console.log( v );
}
// 1
// 2
// 3
for…of 循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next() 方法来遍历所有返回值。
4混合对象“类”
4.1类理论
4.1.1“类”设计模式
4.1.2JavaScript中的“类”
4.2类的机制
4.2.1建造
一个类就是一张蓝图。为了获得真正可以交互的对象,我们必须按照类来建造(也可以说实例化)一个东西,这个东西通常被称为实例,有需要的话,我们可以直接在实例上调用方法并访问其所有公有数据属性。
这个对象就是类中描述的所有特性的一份副本。
4.2.2构造函数
构造函数的方法名通常和类名同名,大多需要用new来调用,它的任务就是初始化实例需要的所有信息。
4.3类的继承
4.3.1多态
参考链接:链接1:link链接2:link
多态的概念:
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
- 面向对象编程中的多态主要是通过抽象类和抽象函数实现的,js中也可以从这两个方面实现多态。
- 传统意义上的多态,是通过派生类继承并实现基类中的抽象(虚)函数来实现的,含有抽象函数的类是抽象类,抽象类是不能够实例化的,同时,抽象函数没有函数体,也不能够直接调用,只能有派生类继承并实现。
- 在高级程序语言中,上述这些检测均在程序编译时进行,不符合要求的程序编译将不通过,但是在js中,有了些许变化:
- js是解释性语言,不需要进行预编译,所以js中抽象类和抽象函数的使用并没有那么严格的要求。
- js中可以对未定义的方法进行调用,当然这一过程会报错,而检测时在执行调用时进行的。
所以,js中的抽象类可以定义实例,但就其意义而言,我们可以定义一个空的没有成员的类来代替,同样,js中的抽象函数,我们可以不必在基类中声明,直接进行调用,在派生类中实现即可,当然,也可以通过在基类中定义一个空的抽象方法实现。
class Vehicle {
engines = 1
ignition() {
output( "Turning on my engine." );
}
drive() {
ignition();
output( "Steering and moving forward!" )
}
}
class Car inherits Vehicle {
wheels = 4
drive() {
inherited:drive()
output( "Rolling on all ", wheels, " wheels!" )
}
}
class SpeedBoat inherits Vehicle {
engines = 2
ignition() {
output( "Turning on my ", engines, " engines." )
}
pilot() {
inherited:drive()
output( "Speeding through the water with ease!" )
}
}
4.3.2多重继承
有些面向类的语言允许你继承多个“父类”。多重继承意味着所有父类的定义都会被复制到子类中。
多重继承的缺点:
如果两个父类中都定义了 drive() 方法的话,
子类引用的是哪个呢?
JavaScript本身并不提供“多重继承”功能。
4.4混入
在继承或者实例化时,JavaScript 的对象机制并不会自动执行复制行为。简单来说,JavaScript 中只有对象,并不存在可以被实例化的“类”。一个对象并不会被复制到其他对象,它们会被关联起来。
由于在其他语言中类表现出来的都是复制行为,因此 JavaScript 开发者也想出了一个方法来模拟类的复制行为,这个方法就是混入。接下来我们会看到两种类型的混入:显式和隐式。
4.4.1显示混入
首先我们来回顾一下之前提到的 Vehicle 和 Car。由于 JavaScript 不会自动实现 Vehicle
到 Car 的复制行为,所以我们需要手动实现复制功能。这个功能在许多库和框架中被称为extend(…),但是为了方便理解我们称之为 mixin(…)。
// 非常简单的 mixin(..) 例子 :
function mixin( sourceObj, 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 forward!" );
}
};
var Car = mixin( Vehicle, {
wheels: 4,
drive: function() {
Vehicle.drive.call( this );
console.log(
"Rolling on all " + this.wheels + " wheels!"
);
}
} );
现在 Car 中就有了一份 Vehicle 属性和函数的副本了。从技术角度来说,函数实际上没有被复制,复制的是函数引用。所以,Car 中的属性 ignition 只是从 Vehicle 中复制过来的对于ignition() 函数的引用。相反,属性 engines 就是直接从 Vehicle 中复制了值 1。
Car 已经有了 drive 属性(函数),所以这个属性引用并没有被 mixin 重写,从而保留了Car 中定义的同名属性,实现了“子类”对“父类”属性的重写(参见 mixin(…) 例子中的 if 语句)。
(1)再说多态
(2)混合复制…
// 非常简单的 mixin(..) 例子 :
function mixin( sourceObj, targetObj ) {
for (var key in sourceObj) {
// 只会在不存在的情况下复制
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
(3)寄生多态
显式混入模式的一种变体被称为“寄生继承”,它既是显式的又是隐式的,主要推广者是Douglas Crockford。
以下是它的工作原理:
//“传统的 JavaScript 类”Vehicle
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();
4.4.2隐式混入
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 不是共享状态)
通过在构造函数调用或者方法调用中使用Something.cool.call( this ),我们实际上“借用”了函数 Something.cool() 并在 Another 的上下文中调用了它(通过 this 绑定;参加第 2 章)。最终的结果是 Something.cool() 中的赋值操作都会应用在 Another 对象上而不是Something 对象上。
因此,我们把 Something 的行为“混入”到了Another 中。
虽然这类技术利用了 this 的重新绑定功能,但是 Something.cool.call( this ) 仍然无法变成相对(而且更灵活的)引用,所以使用时千万要小心。通常来说,尽量避免使用这样的结构,以保证代码的整洁和可维护性。
5原型
5.1[[Prototype]]
var myObject = {
a:2
};
myObject.a; // 2
当你试图引用对象的属性时会触发[[Get]] 操作,比如 myObject.a。对于默认的 [[Get]] 操作来说,第一步是检查对象本身是否有这个属性,如果有的话就使用它。
但是如果 a 不在 myObject 中,就需要使用对象的 [[Prototype]] 链了。
对于默认的 [[Get]] 操作来说,如果无法在对象本身找到需要的属性,就会继续访问对象的 [[Prototype]] 链:
var anotherObject = {
a:2
};
// 创建一个关联到 anotherObject 的对象
var myObject = Object.create( anotherObject );
myObject.a; //
现在 myObject 对象的 [[Prototype]] 关联到了anotherObject。显然 myObject.a 并不存在,但是尽管如此,属性访问仍然成功地(在anotherObject 中)找到了值 2。
但是,如果 anotherObject 中也找不到 a 并且 [[Prototype]] 链不为空的话,就会继续查找下去。
这个过程会持续到找到匹配的属性名或者查找完整条 [[Prototype]] 链。如果是后者的话,[[Get]] 操作的返回值是 undefined。
5.1.1Object.prototype
所有普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype。由于所有的“普通”(内置,不是特定主机的扩展)对象都“源于”(或者说把 [[Prototype]] 链的顶端设置为)这个 Object.prototype 对象,所以它包含 JavaScript 中许多通用的功能。
5.1.2属性设置和屏蔽
myObject.foo = "bar";
- 如果 myObject 对象中包含名为 foo 的普通数据访问属性,这条赋值语句只会修改已有的属
性值。 - 如果 foo 不是直接存在于 myObject 中,[[Prototype]] 链就会被遍历,类似 [[Get]] 操作。
如果原型链上找不到 foo,foo 就会被直接添加到 myObject 上。 - 如果属性名 foo 既出现在 myObject 中也出现在 myObject 的 [[Prototype]] 链上层,那么就会发生屏蔽。myObject 中包含的 foo 属性会屏蔽原型链上层的所有 foo 属性,因为myObject.foo 总是会选择原型链中最底层的 foo 属性。
- 如果 foo 不直接存在于 myObject 中而是存在于原型链上层时 myObject.foo = “bar” 会出现的三种情况:
- 如果在 [[Prototype]] 链上层存在名为 foo 的普通数据访问属性并且【没有】被标记为只读,那就会直接在 myObject 中添加一个名为 foo 的新属性,它是屏蔽属性。
- 如果在 [[Prototype]] 链上层存在 foo,但是它被标记为只读(writable:false),那么无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
- 如果在 [[Prototype]] 链上层存在 foo 并且它是一个 setter(参见第 3 章),那就一定会调用这个 setter。foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义 foo 这个setter。
- 如果你希望在第二种和第三种情况下也屏蔽 foo,那就不能使用 = 操作符来赋值,而是使用 Object.defineProperty(…)来向 myObject 添加 foo。