你根本不懂Javascript(4): this关键字和对象原型

本文最初发布于http://szhshp.org
转载请注明

This关键字

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我们可以传入一个上下文到调用的函数中,例如这样:

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是指向函数自身

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:

function foo(num) {
    console.log( "foo: " + num );

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

foo.count = 0;

或者操作一个全局的count.

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

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

这太愚蠢了...

function foo() {
    var a = 2;
    this.bar(); //还不如不加this关键字直接调用_(:з」∠)_
}

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

foo(); //undefined

方法调用及调用栈

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

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. 默认绑定

    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
  2. 隐式绑定

    function foo() {
        console.log( this.a ); //`this.a` is synonymous with `obj.a`.
    }
    
    var obj = {
        a: 2,
        foo: foo
    };
    
    obj.foo(); // 2
    

    注意这里的调用处仅仅会剥离一层,因此最后一个调用者将会是this所代表的内容

    function foo() {
        console.log( this.a );
    }
    
    var obj2 = {
        a: 42,
        foo: foo
    };
    
    var obj1 = {
        a: 2,
        obj2: obj2
    };
    
    obj1.obj2.foo(); // 42
    
  3. 隐式丢失

    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函数

    function setTimeout(fn,delay) {
    // wait (somehow) for `delay` milliseconds
    fn(); // <-- call-site!
    }
    
  4. 显式绑定

    当调用call()或者applt()的时候我们可以强行传一个obj作为this

    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)

  5. 强绑定

    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. 除非函数返回了一些莫名其妙的东西,否则自动返回新建立的元素
function foo(a) {
    this.a = a+1;
}

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

绑定顺序

  1. new绑定的条件下,那么这是一个全新的Obj

    var bar = new foo()
    
  2. 通过call或者apply进行显式绑定,或者使用了bind进行强绑定,那么这就是个显式绑定的Object

        var bar = foo.call( obj2 )
    
  3. 通过上下文进行隐式调用,或者是某个对象的Attr,那么this就是当前上下文

        var bar = obj1.foo()
    
  4. 否则就是默认绑定了。记住如果是严格模式this=undefined, 否则this=global object

        var bar = foo()
    

例外情况

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

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

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

var a = 2;

foo.call( null ); // 2
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

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

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"

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关键字

一个例子:

function foo() {
    setTimeout(() => {
        // `this` here is lexically adopted from `foo()`
        console.log( this.a );
    },100);
}

var obj = {
    a: 2
};

foo.call( obj ); // 2

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

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

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

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

"use strict"; //注意必须要在严格模式下才会报错

var myObject = {};

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

myObject.a = 3; // TypeError

Configurable

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

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

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删除对应的属性

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

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

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

Enumerable

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

Immutability

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

对象常量 Object Constant

很简单:

writable:false and configurable:false

var myObject = {};

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

关闭扩充性 Prevent Extensions

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

var myObject = {
    a: 2
};

Object.preventExtensions( myObject );

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

Seal

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

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

var obj = {name: 'John'}

// 密封
Object.seal(obj)

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

Freeze

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

var obj = {name: 'John'}

// 密封
Object.freeze(obj)

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

Class

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

基本和多数OO语言一样

// 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;
  }
};

构造函数和属性方法

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);

静态方法

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


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));

继承


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();

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

function Animal (name) {
  this.name = name;  
}

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

//和上面一样继承

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

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关键字

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

多继承

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

//将一个类传入,并且返回一个扩展之后的类
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

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的关系

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也会指向那个新方法

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的属性

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"

正确的继承方法

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"

错误的继承方法

// 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的扩展

// 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的方法:

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

更详细的一种方法:

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

更简单的一种方法:

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

简单粗暴的ES5的方法:

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

总结

上方继承代码集合:

function Foo() { /* .. */ }
Foo.prototype...

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

var b1 = new Bar( "b1" );

类反射判断:


// 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)模式来实现上方的代码:

var Foo = { /* .. */ };

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

var b1 = Object.create( Bar );

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

// 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

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容