JavaScript笔记 | this和对象原型 |《你不知道的JavaScript(上卷)》第二部分

本文详细解析了JavaScript中的this关键字和对象原型,包括this的调用位置、绑定规则(默认绑定、隐式绑定、显示绑定、new绑定)、优先级和异常情况,以及对象的内容、属性描述符、继承和原型链。文章结合《你不知道的JavaScript(上卷)》深入探讨了JavaScript中的类和对象设计模式,如显式和隐式混入、原型继承、构造函数和多态等概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

来自《你不知道的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 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

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

2.3优先级

我们需要找到函数的调用位置并判断应当应用哪条规则。但是,如果某个调用位置可以应用多条规则该怎么办?此时需要给这些规则设定优先级。
** new绑定>显示绑定>隐式绑定>全局对象绑定**

  1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。
    var bar = new foo()
  2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是指定的对象。
    var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。
    var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 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语法

《你不知道的JavaScript》

构造形式和文字形式生成的对象是一样的。唯一的区别是,在文字声明中你可以添加多个键 / 值对,但是在构造形式中你必须逐个添加属性。

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]] 算法大致会检查下面这些内容。

  1. 属性是否是访问描述符(参见 3.3.9 节)?如果是并且存在 setter 就调用 setter。
  2. 属性的数据描述符中 writable 是否是 false ?如果是,在非严格模式下静默失败,在严格模式下抛出 TypeError 异常。
  3. 如果都不是,将该值设置为属性的值。
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 引擎中可能不一样。因此,在不同的环境中需要保证一致性时,一定不要相信任何观察到的顺序,它们是不可靠的。

如何直接遍历值而不是数组下标(或者对象属性)?
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中,有了些许变化:
    1. js是解释性语言,不需要进行预编译,所以js中抽象类和抽象函数的使用并没有那么严格的要求。
    2. 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!" )
   }
}

《你不知道的JavaScript(上卷)》
《你不知道的JavaScript(上卷)》
《你不知道的JavaScript(上卷)》

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)再说多态
《你不知道的JavaScript(上卷)》
(2)混合复制…

// 非常简单的 mixin(..) 例子 :
function mixin( sourceObj, targetObj ) {
    for (var key in sourceObj) {
        // 只会在不存在的情况下复制
        if (!(key in targetObj)) {
            targetObj[key] = sourceObj[key];
        }
    }
    return targetObj;
}

《你不知道的JavaScript(上卷)》
《你不知道的JavaScript(上卷)》《你不知道的JavaScript(上卷)》
(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” 会出现的三种情况:
    1. 如果在 [[Prototype]] 链上层存在名为 foo 的普通数据访问属性并且【没有】被标记为只读,那就会直接在 myObject 中添加一个名为 foo 的新属性,它是屏蔽属性。
    2. 如果在 [[Prototype]] 链上层存在 foo,但是它被标记为只读(writable:false),那么无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
    3. 如果在 [[Prototype]] 链上层存在 foo 并且它是一个 setter(参见第 3 章),那就一定会调用这个 setter。foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义 foo 这个setter。
    • 如果你希望在第二种和第三种情况下也屏蔽 foo,那就不能使用 = 操作符来赋值,而是使用 Object.defineProperty(…)来向 myObject 添加 foo。
      《你不知道的JavaScript(上卷)》
      《你不知道的JavaScript(上卷)》

5.2类

5.2.1类函数
5.2.2构造函数
5.2.3技术

5.3(原型)继承

5.4对象关联

5.4.1创建关联
5.4.2关联关系是备用

6行为委托

6.1面向委托的设计

6.1.1类理论
6.1.2委托理论
6.1.3比较思维模型

6.2类与对象

6.2.1控件类
6.2.2委托控件对象

6.3更简洁的设计

6.4更好的语法

6.5内省

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值