this关键字和对象原型
文章目录

This关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function identify() {
return this.name.toUpperCase();
}

function speak() {
var greeting = "Hello, I'm " + identify.call(this);
console.log(greeting);
}

var me = {
name: "Kyle"
};

var you = {
name: "Reader"
};

identify.call(me); // KYLE
identify.call(you); // READER

speak.call(me); // Hello, I'm KYLE
speak.call(you); // Hello, I'm READER

同时如果不使用 this 我们可以传入一个上下文到调用的函数中, 例如这样:

1
2
3
4
5
6
7
8
9
10
11
function identify(context) {
return context.name.toUpperCase();
}

function speak(context) {
var greeting = "Hello, I'm " + identify(context);
console.log(greeting);
}

identify(you); // READER
speak(me); // Hello, I'm KYLE

几个对 this 关键字的误解

认为 this 是指向函数自身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function foo(num) {
console.log("foo: " + num);

// keep track of how many times `foo` is called
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

// how many times was `foo` called?
console.log(foo.count); // 0 -- WTF?

但是实际上我们操作的不是这个 foo 里面的 count 而是一个全局变量 count

解决方案

当然解决这个问题很简单, 不要在函数中操作 this 就是一个Solution:

1
2
3
4
5
6
7
8
function foo(num) {
console.log("foo: " + num);

// keep track of how many times `foo` is called
foo.count++;
}

foo.count = 0;

或者操作一个全局的count.

或者用另一种办法强行使用 this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function foo(num) {
console.log("foo: " + num);

// keep track of how many times `foo` is called
// Note: `this` IS actually `foo` now, based on
// how `foo` is called (see below)
this.count++;
}

foo.count = 0;

var i;

for (i = 0; i < 10; i++) {
if (i > 5) {
// using `call(..)` , we ensure the `this`
// points at the function object ( `foo` ) itself
foo.call(foo, i);
}
}

认为 this 指向函数的scope

这太愚蠢了…

1
2
3
4
5
6
7
8
9
10
function foo() {
var a = 2;
this.bar(); //还不如不加this关键字直接调用_(:з」∠)_
}

function bar() {
console.log(this.a);
}

foo(); //undefined

方法调用及调用栈

想要理解 this 首先就要了解一个方法在哪里调用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function baz() {
// call-stack is: `baz`
// so, our call-site is in the global scope

console.log("baz");
bar(); // <-- call-site for `bar`
}

function bar() {
// call-stack is: `baz` -> `bar`
// so, our call-site is in `baz`

console.log("bar");
foo(); // <-- call-site for `foo`
}

function foo() {
// call-stack is: `baz` -> `bar` -> `foo`
// so, our call-site is in `bar`

console.log("foo");
}

baz(); // <-- call-site for `baz`

多数浏览器的 Debugger 工具可以方便地看到调用栈

调用规则

  1. 默认绑定
1
2
3
4
5
6
7
8
9
10
var a = 10;
b = 10;
this.a === a; // true
this.b === b; // true
//--------------------------
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
- 直接定义的变量都属于 `global object` 
- 注意这种绑定在 `strict mode` 不生效并且会报 `Undefined` 
  1. 隐式绑定
1
2
3
4
5
6
7
8
9
10
function foo() {
console.log(this.a); // `this.a` is synonymous with `obj.a` .
}

var obj = {
a: 2,
foo: foo
};

obj.foo(); // 2
注意这里的调用处仅仅会剥离一层, 因此最后一个调用者将会是 `this` 所代表的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
console.log(this.a);
}

var obj2 = {
a: 42,
foo: foo
};

var obj1 = {
a: 2,
obj2: obj2
};

obj1.obj2.foo(); // 42
  1. 隐式丢失
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function foo() {
console.log(this.a);
}

var obj = {
a: 2,
foo: foo
};

var bar = obj.foo; // function reference/alias!

var a = "oops, global"; // `a` also property on global object

bar(); // "oops, global"
//--------------------------

function foo() {
console.log(this.a);
}

var obj = {
a: 2,
foo: foo
};

var bar = obj.foo; // function reference/alias!

var a = "oops, global"; // `a` also property on global object

bar(); // "oops, global"
setTimeout(obj.foo, 100); // "oops, global"
特别对于上面 `setTimeout` 函数
1
2
3
4
function setTimeout(fn, delay) {
// wait (somehow) for `delay` milliseconds
fn(); // <-- call-site!
}
  1. 显式绑定
当调用 `call()` 或者 `applt()` 的时候我们可以强行传一个 `obj` 作为 `this` 
1
2
3
4
5
6
7
8
9
function foo() {
console.log(this.a);
}

var obj = {
a: 2
};

foo.call(obj); // 2
同时注意如果给 `this` 传进原始类型的数据时,对应数据会进行装包(boxing),即转换成对应Obj (new String(..), new Boolean(..), or new Number(..), respectively)
  1. 强绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo() {
console.log( this.a );
}

var obj = {
a: 2
};

var bar = function() {
foo.call( obj ); // 强行将obj传给this
};

bar(); // 2
setTimeout( bar, 100 ); // 2

// `bar` hard binds `foo` 's `this` to `obj`
// so that it cannot be overriden
bar.call( window ); // 2
另外使用 `bind()` 方法可以强行设定 `this` 的值为某个其他变量。

使用 new 关键字时发生了什么

  1. 新建立一个Obj
  2. 将这个Obj与原型相连接(见后文详解)
  3. 新建立的Obj设置为对应函数的 this
  4. 除非函数返回了一些莫名其妙的东西,否则自动返回新建立的元素
1
2
3
4
5
6
function foo(a) {
this.a = a + 1;
}

var bar = new foo(2);
console.log(bar.a); // 3

绑定顺序

  1. new 绑定的条件下, 那么这是一个全新的Obj
1
var bar = new foo()
  1. 通过 call 或者 apply 进行显式绑定, 或者使用了 bind 进行强绑定, 那么这就是个显式绑定的Object
1
var bar = foo.call(obj2)
  1. 通过上下文进行隐式调用, 或者是某个对象的Attr, 那么 this 就是当前上下文
1
var bar = obj1.foo()
  1. 否则就是默认绑定了. 记住如果是严格模式 this=undefined , 否则 this=global object
1
var bar = foo()

例外情况

当模块不需要用到 this 的时候, 但是却需要使用 bind 等函数, 可以将 null 传到 this .

同时这种情况下就会默认使用 默认绑定 的规则

1
2
3
4
5
6
7
function foo() {
console.log(this.a);
}

var a = 2;

foo.call(null); // 2
1
2
3
4
5
6
7
8
9
10
function foo(a, b) {
console.log("a:" + a + ", b:" + b);
}

// spreading out array as parameters
foo.apply(null, [2, 3]); // a:2, b:3

// currying with `bind(..)`
var bar = foo.bind(null, 2);
bar(3); // a:2, b:3

Indirection

话说这个到底怎么翻译啊.. 重定向吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

还是很好理解的, 上面的赋值语句执行后返回了一个单纯的 foo 变量, 因此导致了 Indirection , 并且使用了 默认绑定

注意默认绑定的规则:

  • non-strict mode 模式下: 引用 global object
  • strict mode 模式下: 对应引用变成 Undefined

语义绑定/Lexical this/ES6

ES6多了个新玩意: 箭头符号

相关的绑定称作”Lexical this”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function foo() {
// return an arrow function
return (a) => {
// `this` here is lexically adopted from `foo()`
console.log(this.a);
};
}

var obj1 = {
a: 2
};

var obj2 = {
a: 3
};

var bar = foo.call(obj1); // 返回值是一个函数,并且函数里面的this被绑定到obj1
bar.call(obj2);
// 输出2, not 3!

如果是普通函数输出应该是3因为 this 绑定到了 obj2

而语义绑定无法被重载, 即使用了 new 关键字

一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
setTimeout(() => {
// `this` here is lexically adopted from `foo()`
console.log(this.a);
}, 100);
}

var obj = {
a: 2
};

foo.call(obj); // 2

另一种针对箭头符号的解决方案, 通过外部重新赋值来实现可读性, 这样就知道这儿的 this 是指向函数的了

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
var self = this; // lexical capture of `this`
setTimeout(function() {
console.log(self.a);
}, 100);
}

var obj = {
a: 2
};

foo.call(obj); // 2

不过上述两段代码都是某种意义上的 偷懒 , 如果真的想要掌握 this 还是需要:

  1. Use only lexical scope and forget the false pretense of this -style code.

  2. Embrace this -style mechanisms completely, including using bind(..) where necessary, and try to avoid self = this and arrow-function “lexical this” tricks.

Objects

Shadow Copy & Deep Copy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function anotherFunction() {
/*..*/ }

var anotherObject = {
c: true
};

var anotherArray = [];

var myObject = {
a: 2,
b: anotherObject, // reference, not a copy!
c: anotherArray, // another reference!
d: anotherFunction
};

anotherArray.push(anotherObject, myObject);

上面这一段玩意, 如果使用

  1. Shadow Copy:那么a会直接复制,bcd会保留对函数的引用
  2. Deep Copy:完全复制abcd,这样会造成环形引用导致错误

属性标识符 Property Descriptors

没什么好说的, 就几个特殊的属性:

Writable

注意必须要在严格模式下才会报错

1
2
3
4
5
6
7
8
9
10
11
12
"use strict"; //注意必须要在严格模式下才会报错

var myObject = {};

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

myObject.a = 3; // TypeError

Configurable

表示是否允许下一次使用 defineProperty 进行配置

非严格模式下也会报错, 这是一种无法返回的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var myObject = {
a: 2
};

myObject.a = 3;
myObject.a; // 3

Object.defineProperty(myObject, "a", {
value: 4,
writable: true,
configurable: false, // not configurable!
enumerable: true
});

myObject.a; // 4
myObject.a = 5;
myObject.a; // 5

Object.defineProperty(myObject, "a", {
value: 6,
writable: true,
configurable: true,
enumerable: true
}); // TypeError

并且设置为false之后也无法使用 delete 删除对应的属性

1
2
3
myObject.a; // 2
delete myObject.a;
myObject.a; // 2, 上一句上删除失败了

delete 用于删除一个 是对象的属性 , 如果这个属性是某变量的最后一个属性, 那么 delete 之后就会变成空引用并且对应资源会被回收

但是这玩意不能用于内存回收, 他只是删除了一个属性而已

Enumerable

很多奇怪的函数里面会进行判断这个属性

Immutability

这不是一个实际的属性, 不过我们有时候需要将一个变量变得 永恒不变 , 通过下面这些办法:

对象常量 Object Constant

很简单:

writable:false and configurable:false

1
2
3
4
5
6
7
var myObject = {};

Object.defineProperty(myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
});

关闭扩充性 Prevent Extensions

Object.preventExtensions(..) 将令变量无法添加新属性

1
2
3
4
5
6
7
8
var myObject = {
a: 2
};

Object.preventExtensions(myObject);

myObject.b = 3;
myObject.b; // undefined
  1. 严格模式下: 报错
  2. 非严格模式: 不报错, 但是修改无效, b依然等于2

Seal

Object.seal(..) = Object.preventExtensions(..) + configurable:false

但是依然可以修改属性的值

1
2
3
4
5
6
7
8
9
10
var obj = {
name: 'John'
}

// 密封
Object.seal(obj)

// 可以修改已有属性的值
obj.name = 'Backus'
console.log(obj.name) // 'Backus'

Freeze

Object.freeze(..) = Object.seal(..) + writable:false

1
2
3
4
5
6
7
8
9
10
var obj = {
name: 'John'
}

// 密封
Object.freeze(obj)

// 无法修改已有属性的值
obj.name = 'Backus'
console.log(obj.name) // 'John', 修改失败

Class

这里只强调ES6的 class 的使用方法

基本和多数OO语言一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// unnamed
var Rectangle = class {
constructor(height, width) {
this.height = height;
this.width = width;
}
};

// named
var Rectangle = class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
};

构造函数和属性方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}

get area() {
return this.calcArea();
}

calcArea() {
return this.height * this.width;
}
}

const square = new Rectangle(10, 10);

console.log(square.area);

静态方法

不通过初始化实例就能调用的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}

static distance(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;

return Math.sqrt(dx * dx + dy * dy);
}
}

const p1 = new Point(5, 5);
const p2 = new Point(10, 10);

console.log(Point.distance(p1, p2));

继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Animal {
constructor(name) {
this.name = name;
}

speak() {
console.log(this.name + ' makes a noise.');
}
}

class Dog extends Animal {
speak() {
console.log(this.name + ' barks.');
}
}

var d = new Dog('Mitzie');
d.speak();

注意即使是以前使用原型创造的父类也可以进行继承

1
2
3
4
5
6
7
8
9
function Animal(name) {
this.name = name;
}

Animal.prototype.speak = function() {
console.log(this.name + ' makes a noise.');
}

//和上面一样继承

还有另一种继承方法, 使用 Object.setPrototypeOf(Dog.prototype, Animal);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Animal = {
speak() {
console.log(this.name + ' makes a noise.');
}
};

class Dog {
constructor(name) {
this.name = name;
}
}

Object.setPrototypeOf(Dog.prototype, Animal);// If you do not do this you will get a TypeError when you invoke speak

var d = new Dog('Mitzie');
d.speak(); //Mitzie makes a noise.

超类

直接用super关键字

1
2
3
4
5
6
class Lion extends Cat {
speak() {
super.speak(); // 直接用super关键字
console.log(this.name + ' roars.');
}
}

多继承

ES不支持多继承, 但是可以用 mixin 的方法伪装一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//将一个类传入,并且返回一个扩展之后的类
var calculatorMixin = Base => class extends Base {
calc() {}
};

//同样将一个类传入,并且返回一个扩展之后的类
var randomizerMixin = Base => class extends Base {
randomize() {}
};

class Foo {} //初始化一个类

//将类传入,进行两次扩展,然后扩展到子类Bar中,如此就进行了多次扩张类似于多继承
class Bar extends calculatorMixin(randomizerMixin(Foo)) {}

Prototype

所有的 Object 都的最顶层都是 Object.prototype .

Setting & Shadowing Properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var anotherObject = {
a: 2
};

var myObject = Object.create(anotherObject);

anotherObject.a; // 2
myObject.a; // 2

anotherObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("a"); // false,a是继承过来的自然返回false

myObject.a++; // oops, implicit shadowing!

anotherObject.a; // 2
myObject.a; // 3

myObject.hasOwnProperty("a"); // true

注意上面如果不给子类自增而直接给父类执行自增, 那么子类因为是调用继承的属性因此也会返回3

  1. 当一个属性在继承链的高层被发现并且可写的话, 那么就会发生Property Shadowing
  2. 当然如果在高层发现并且不可写, 那么就会设置失败, 并且严格模式下会直接报错
  3. 单原型链上存在一个与这个属性相关的 Setter 并且一定会调用到这个 Setter , 那么这个属性的再次赋值必然会失败

constructor

constructor 没啥特别的, 一个类对应的函数就是一个constructor

但是使用 new 关键字的时候会调用这个constructor, 这是唯一一个constructor和函数的区别

constructor和prototype的关系

1
2
3
4
5
6
7
8
function test() {
console.log("Don't mind me!");
}

var t = new test(); // output: dont mind me

t.constructor === test; // true
test.prototype.constructor == test; // true
  • 首先 new 的时候执行了对应的constructor, 输出
  • t 是没有 prototype 这个属性的, 因为它不是class而是obj
  • test.prototype.constructortest() 定义的时候创建的
  • t.constructor 也指向同一个 test()

另外, 如果将 testprototype 改为另一个方法, 那么 t.constructor 也会指向那个新方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function test() {
console.log("Don't mind me!");
}

var t1 = new test();
t1.constructor === test; // true

test.prototype = {
test2: function() {
console.log("New");
}
}

var t2 = new test();
t2.constructor === Object; // true
t2.constructor === Object.prototype.constructor; // true

因为我们将 test.prototype 转到了一个新的Obj上面, 并且修改之后 test.prototype.constructor 不存在了 , 因此接下来初始化的Obj会继承最高层的 Object.prototype.constructor

解决这个问题的方法很简单, 在切换这个 test.prototype 的同时也将constructor也赋值过去, 或者直接在新的prototype里面放一个 constructor 的属性

1
2
3
4
5
6
7
Object.defineProperty( test.prototype, "constructor" , {
enumerable: false,
writable: true,
configurable: true,
value: test // point `.constructor` at `test`
} );
t2.constructor === test;// true

Generally, such references should be avoided where possible.

“(Prototypal) Inheritance”

正确的继承方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function Foo(name) {
this.name = name;
}

Foo.prototype.myName = function() {
return this.name;
};

function Bar(name, label) {
Foo.call(this, name);
this.label = label;
}

// here, we make a new `Bar.prototype`
// linked to `Foo.prototype`
Bar.prototype = Object.create(Foo.prototype);

// Beware! Now `Bar.prototype.constructor` is gone,
// and might need to be manually "fixed" if you're
// in the habit of relying on such properties!

Bar.prototype.myLabel = function() {
return this.label;
};

var a = new Bar("a", "obj a");

a.myName(); // "a"
a.myLabel(); // "obj a"

错误的继承方法

1
2
3
4
5
// doesn't work like you want!
Bar.prototype = Foo.prototype;

// works kinda like you want, but with side-effects you probably don't want :(
Bar.prototype = new Foo();

第一行改变了引用, 因此之后如果希望可以Bar进行扩展(比如添加新方法)的时候实际扩展了Foo
第二行同样使用Foo的constructor来创建新实例, 但是要注意进行扩展(比如扩展this)的时候同样会扩展到Foo

ES6的扩展

1
2
3
4
5
6
7
// pre-ES6
// throws away default existing `Bar.prototype`
Bar.prototype = Object.create(Foo.prototype);

// ES6+
// modifies existing `Bar.prototype`
Object.setPrototypeOf(Bar.prototype, Foo.prototype);

类反射 Reflection

前三种方法中: 父类必然是子类实例对应的class

就是OOP里面根据instance获取对应class的方法:

1
2
a instanceof Bar; // true
a instanceof Foo; // true, Bar is inherited from Foo

更详细的一种方法:

1
2
3
4
5
function isRelatedTo(o1, o2) {
function F(){}
F.prototype = o2;
return o1 instanceof F; //重点还是和F的prototype进行匹配, 即使F是个空函数
}

更简单的一种方法:

1
Foo.prototype.isPrototypeOf(a); // true

简单粗暴的ES5的方法:

1
2
Object.getPrototypeOf(a) === Foo.prototype; // false, 如果Bar继承于Foo, 此处依然检测不出来
Object.getPrototypeOf(a) === Bar.prototype; // true

总结

上方继承代码集合:

1
2
3
4
5
6
7
8
9
function Foo() {
/* .. */ }
Foo.prototype...

function Bar() {
/* .. */ }
Bar.prototype = Object.create(Foo.prototype);

var b1 = new Bar("b1");

类反射判断:

1
2
3
4
5
6
7
8
9
10
11
// relating `Foo` and `Bar` to each other
Bar.prototype instanceof Foo; // true
Object.getPrototypeOf(Bar.prototype) === Foo.prototype; // true
Foo.prototype.isPrototypeOf(Bar.prototype); // true

// relating `b1` to both `Foo` and `Bar`
b1 instanceof Foo; // true
b1 instanceof Bar; // true
Object.getPrototypeOf(b1) === Bar.prototype; // true
Foo.prototype.isPrototypeOf(b1); // true
Bar.prototype.isPrototypeOf(b1); // true

使用原始的对象连接OLOO (objects-linked-to-other-objects)模式来实现上方的代码:

1
2
3
4
5
6
7
var Foo = {
/* .. */ };

var Bar = Object.create(Foo);
Bar...

var b1 = Object.create(Bar);

对应的类反射就有些不同:

1
2
3
4
5
6
7
8
// relating `Foo` and `Bar` to each other
Foo.isPrototypeOf(Bar); // true
Object.getPrototypeOf(Bar) === Foo; // true

// relating `b1` to both `Foo` and `Bar`
Foo.isPrototypeOf(b1); // true
Bar.isPrototypeOf(b1); // true
Object.getPrototypeOf(b1) === Bar; // true

参考文献