this和对象——源自《你所不知道的JavaScript》

this

关于this的错误观点:
1、this指向函数自身。

但其实,this并不指向函数自身。有一种传统的但是现在已经被弃用和批判的用法,是使用 arguments. callee 来引用当前正在运行的函数对象。这是唯一一种可以从匿名函数对象内部引用自身的方法。然而,更好的方式是避免使用匿名函数,至少在需要 自引用时使用具名函数(表达式)。arguments.callee 已经被弃用,不应该再使用它。

我们如果希望函数中的this指向自身,应该使用方法 foo.call( foo, i );来强制this指向函数。

2、this指向函数的作用域。

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的调用位置

this的绑定规则(重点*)

1、默认绑定
2、隐式绑定
3、显示绑定
4new绑定

1、默认绑定
可以把这条规则看作是无法应用其他规则时的默认规则。
this 为默认绑定时, this 指向全局对象。(非严格模式下,在严格模式下指向undefined)

2、隐式绑定

function foo() { 
    console.log( this.a );
}
var obj = { 
    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 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 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() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

特别的:参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,因此下面这个例子的结果和上面的代码一样。

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"

3、显式绑定

具体来说就是使用call(..) 和 apply(..) 方法。它们的第一个参数是一个对象,它们会把这个对象绑定到 this,接着在调用函数时指定这个 this。因为你可以直接指定 this 的绑定对象,因此我们称之为显式绑定。

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

可惜,显式绑定仍然无法解决我们之前提出的丢失绑定问题。

  1. 硬绑定:显式绑定的一个变种可以解决绑定丢失的问题。
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

我们来看看这个变种到底是怎样工作的。
我们创建了函数 bar(),并在它的内部手动调用了 foo.call(obj),因此强制把 foo 的 this 绑定到了 obj。无论之后如何调用函数 bar,它总会手动在 obj 上调用 foo。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。

由于硬绑定是一种非常常用的模式,所以在 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

bind(..) 会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。

4、new绑定

首先要明确的是:JavaScript 中 new 的机制实际上和面向类的语言完全不同!

首先我们重新定义一下 JavaScript 中的“构造函数”。在 JavaScript 中,构造函数只是一些使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上, 它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。

简单的说就是,在JavaScript中实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

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

this词法

ES6中引入了箭头函数,它会根据外层作用域来决定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 也不行!)

对象

类型

对象是 JavaScript 的基础。在 JavaScript 中一共有六种主要类型(术语是“语言类型”):

string • number • boolean • null • undefined • object

注意,简单基本类型(string、boolean、number、null 和 undefined)本身并不是对象。 null 有时会被当作一种对象类型,但是这其实只是语言本身的一个 bug,即对 null 执行 typeof null时会返回字符串”object”。实际上,null本身是基本类型。

有一种常见的错误说法是“JavaScript 中万物皆是对象”,这显然是错误的。

内置对象

StringNumberBooleanObjectFunctionArrayDateRegExpError

这些内置对象从表现形式来说很像其他语言中的类型(type)或者类(class),比如 Java 中的 String 类。
但是在 JavaScript 中,它们实际上只是一些内置函数。这些内置函数可以当作构造函数来使用,从而可以构造一个对应子类型的新对象。

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

特别的,JS引擎会自动把 String/Number 字面量转换成 String /Number 对象,所以以字面量定义的 String/Number 也可以访问 String/Number 对象的属性和方法。
例如:


var strPrimitive = "I am a string"; 
console.log( strPrimitive.length ); // 13 
console.log( strPrimitive.charAt( 3 ) ); // "m"

另外,null 和 undefined 没有对应的构造形式,它们只有文字形式。相反,Date 只有构造,没有文字形式。
对于 Object、Array、Function 和 RegExp(正则表达式)来说,无论使用文字形式还是构造形式,它们都是对象,不是字面量。

内容

如果要访问对象中某属性位置上的值,我们需要使用. 操作符或者 [] 操作符。
.语法通常被称为“属性访问”,[..] 语法通常被称为“键访问”。

这两种语法的主要区别在于 . 操作符要求属性名满足标识符的命名规范,而 [“..”] 语法可以接受任意 UTF-8/Unicode 字符串作为属性名。
举例来说,如果要引用名称为 “Super- Fun!” 的属性,那就必须使用 [“Super-Fun!”] 语法访问,因为 Super-Fun! 并不是一个有效的标识符属性名。

在对象中,属性名永远都是字符串。如果你使用 string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串。即使是数字也不例外,虽然在数组下标中使用的的确是数字,但是在对象属性名中数字会被转换成字符串,所以当心不要搞混对象和数组中数字的用法。

属性的方法

即使你在对象的文字形式中声明一个函数表达式,这个函数也不会“属于”这个对象——它们只是对于相同函数对象的多个引用。

关于数组

数组也是对象,所以虽然每个下标都是整数,你仍然可以给数组添加属性。

var myArray = [ "foo", 42, "bar" ]; 
myArray.baz = "baz"; 
myArray.length; // 3
myArray.baz; // "baz"

复制对象

JavaScript 初学者最常见的问题之一就是如何复制一个对象。看起来应该有一个内置的 copy()方法,但是实际上事情比你想象的更复杂,因为我们无法选择一个默认的复制算法。

首先,我们应该判断它是浅复制还是深复制。
同时,由于对象的属性可能是引用,如果这个对象的某个属性的引用指向了自身,那么就会导致死循环。我们是应该检测循环引用并终止循环(不复制深层元素)?还是应当直接报错或者是选择其他方法?

对于 JSON 安全(也就是说可以被序列化为一个 JSON 字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象来说,有一种巧妙的复制方法:

var newObj = JSON.parse( JSON.stringify( someObj ) );

相比深复制,浅复制非常易懂并且问题要少得多,所以 ES6 定义了 Object.assign(..) 方法来实现浅复制。Object.assign(..) 方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。它会遍历一个或多个源对象的所有可枚举(enumerable) 的自有键并把它们复制(使用 = 操作符赋值)到目标对象,最 后返回目标对象。

属性描述符

一个普通的对象属性对应的属性描述符(也被称为“数据描述符”,因为它只保存一个数据值)不仅仅只是它自身的值(value)。它还包含另外三个特性:writable(可写)、 enumerable(可枚举)和 configurable(可配置)。

var myObject = {};
Object.defineProperty( myObject, "a", {
    value: 2,
    writable: true, 
    configurable: true,  
    enumerable: true
} );
myObject.a; // 2

我们使用 defineProperty(..) 给 myObject 添加了一个普通的属性并显式指定了一些特性。

1. Writable
writable 决定是否可以修改属性的值。

2. Configurable
只要属性是可配置的,就可以使用 defineProperty(..) 方法来修改属性描述符。

要注意有一个小小的例外:即便属性是 configurable:false,我们还是可以 把 writable 的状态由 true 改为 false,但是无法由 false 改为 true。

除了无法修改,configurable:false 还会禁止删除这个属性。

3. Enumerable
从名字就可以看出,这个描述符控制的是属性是否会出现在对象的属性枚举中,比如说 for..in 循环。如果把 enumerable 设置成 false,这个属性就不会出现在枚举中,虽然仍然可以正常访问它。相对地,设置成 true 就会让它出现在枚举中。

不变性

ES5中很多方式可以帮助你创建一个不可更改的对象,但是很重要的一点是,所有的方法创建的都是浅不变性。

也就是说,它们只会影响目标对象和它的直接属性。如果目标对象引用了其他对象(例如数组、对象、函数),其他对象的内容不受影响,仍然是可变的。

方法1. 对象常量
结合 writable:false 和 configurable:false 就可以创建一个真正的常量属性(不可修改、 重定义或者删除)。

方法2. 禁止扩展
如果你想禁止一个对象添加新属性并且保留已有属性,可以使用 Object.prevent Extensions(..)。

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

方法4. 冻结
Object.freeze(..) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal(..) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值。
这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改(不过就像我们之前说过的,这个对象引用的其他对象是不受影响的)。

[[Get]]

object.a 是一次属性访问,但是这条语句并不仅仅是在 myObjet 中查找名字为 a 的属性,myObject.a 在 myObject 上实际上是实现了 [[Get]] 操作。
对象默认的内置 [[Get]] 操作首先在对象中查找是否有名称相同的属性, 如果找到就会返回这个属性的值。
如果没有找到名称相同的属性,按照 [[Get]] 算法的定义会执行另外一种非常重要的行为:遍历可能存在的 [[Prototype]] 链, 也就是原型链。
如果无论如何都没有找到名称相同的属性,那 [[Get]] 操作会返回值 undefined。

注意,这种方法和访问变量时是不一样的。如果你引用了一个当前词法作用域中不存在的变量,并不会像对象属性一样返回 undefined,而是会抛出一个 ReferenceError 异常。

[[Put]]

既然有可以获取属性值的 [[Get]] 操作,就一定有对应的 [[Put]] 操作。

[[Put]] 被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性。
如果已经存在这个属性,[[Put]] 算法大致会检查下面这些内容。

1. 属性是否是访问描述符?如果是并且存在setter就调用setter。
2. 属性的数据描述符中writable是否是false?如果是,在非严格模式下静默失败,在严格模式下抛出 TypeError 异常。
3. 如果都不是,将该值设置为属性的值。

如果对象中不存在这个属性,[[Put]] 操作会更加复杂。涉及到Prototype(原型链)以及属性屏蔽问题。

myObject.foo = 1;

1. 如果在[[Prototype]]链上层已经存在该普通数据访问属性并且没有被标记为只读(writable:false),那就会直接在 myObject 中添加一个名为 foo 的新属性,它是屏蔽属性。
2. 如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable:false),那么 无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
3. 如果在[[Prototype]]链上层存在foo并且它是一个setter,那就一定会调用这个 setter。foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义 foo 这个 setter。

对象默认的 [[Put]] 和 [[Get]] 操作分别可以控制属性值的设置和获取。这两个“函数”是属于对象的,要注意它们和下面我们要讲的Getter和Setter的区别。

Getter和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
    }
);

不管是对象文字语法中的get a() { .. },还是defineProperty(..)中的显式定义,二者都会在对象中创建一个不包含值的属性,对于这个属性的访问就是运行定义时赋给它们的函数, 函数的返回值会被当作属性访问的返回值。

如果只定义了 a 的 getter,所以对 a 的值进行设置时 set 操作会忽略赋值操作,不会抛出错误。

为了让属性更完整合理,还应当定义 setter,setter 会覆盖单个属性默认的 [[Put]] 操作。通常来说 getter 和 setter 应该是成对出现的。

存在性

判断对象中是否存在这个属性的方法。

var myObject = { a:2 };
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false
//或者下面是更为稳妥地函数调用方法
Object.prototype.hasOwnProperty.call(myObject,"a")

in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中(全搜索)。
相比之下, hasOwnProperty(..) 只会检查属性是否在 myObject 对象中(因为是OwnProperty),不会检查 [[Prototype]] 链。

属性的可枚举性检查

myObject.propertyIsEnumerable( "a" ); // true
myObject.propertyIsEnumerable( "b" ); // false

另外还有:
Object.keys(..) 会返回一个数组,包含所有 [[可枚举]] 属性;
Object.getOwnPropertyNames(..) 会返回一个数组,包含所有属性,无论它们是否可枚举;

in 和 hasOwnProperty(..) 的区别在于是否查找 [[Prototype]] 链,然而,Object.keys(..) 和 Object.getOwnPropertyNames(..) 都只会查找对象直接包含的属性。

混合对象“类”

混入

在继承或者实例化时,JavaScript 的对象机制并不会执行复制行为(子类是父类的副本)。
JavaScript 中只有对象,并不存在可以被实例化的“类”。一个对象并不会被复制到其他对象,它们会被通过prototype链关联起来。

由于在其他语言中类表现出来的都是复制行为,因此 JavaScript 开发者也想出了一个方法来模拟类的复制行为,这个方法就是混入。接下来我们会看到两种类型的混入:显式和隐式。

显式混入:

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

但是,其实这样的复制是存在问题的,因为JavaScript 中的函数无法(用标准、可靠的方法)真正地复制,所以你只能复制对共享函数对象的引用(函数就是对象)。如果你修改了共享的函数对象,那 sourceObj 和 targetObj 两个对象都会受到影响。

寄生继承

显式混入模式的一种变体。

//“寄生类”Car 继承自Vehicle
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 Something = {  
    cool: function() {
        this.greeting = "Hello World";
        this.count = this.count ? this.count + 1 : 1; 
    }
};
var Another = {
    cool: function() {
        // 隐式把 Something 混入 Another
        Something.cool.call( this );
    }
};

通过在方法调用中使用Something.cool.call( this ),实际上是“借用”了函数 Something.cool() 并在 Another 的上下文中调用了它。最终的结果是 Something.cool() 中的赋值操作都会应用在 Another 对象上而不是 Something 对象上。
因此,我们把 Something 的行为“混入”到了 Another 中。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值