你根本不懂Javascript(EP4 this关键字和对象原型)
本文于本博客首发,转载请注明 翻译/总结自 You-Dont-Know-JS
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
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 ); } }
这太愚蠢了……
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
工具可以方便地看到调用栈
-
默认绑定
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
- 直接定义的变量都属于
-
隐式绑定
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
-
隐式丢失
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! }
-
显式绑定
当调用
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) -
强绑定
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
的值为某个其他变量。
- 新建立一个 Obj
- 将这个 Obj 与原型相连接(见后文详解)
- 新建立的 Obj 设置为对应函数的
this
- 除非函数返回了一些莫名其妙的东西,否则自动返回新建立的元素
function foo(a) { this.a = a+1; } var bar = new foo( 2 ); console.log( bar.a ); // 3
-
new
绑定的条件下,那么这是一个全新的 Objvar bar = new foo()
-
通过
call
或者apply
进行显式绑定,或者使用了bind
进行强绑定,那么这就是个显式绑定的 Objectvar bar = foo.call( obj2 )
-
通过上下文进行隐式调用,或者是某个对象的 Attr,那么
this
就是当前上下文var bar = obj1.foo()
-
否则就是默认绑定了。记住如果是严格模式
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
话说这个到底怎么翻译啊……重定向吗?
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
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
还是需要:
Use only lexical scope and forget the false pretense of
this
-style code.Embrace
this
-style mechanisms completely, including usingbind(……)
where necessary, and try to avoidself = this
and arrow-function "lexical this" tricks.
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 );
上面这一段玩意,如果使用
- Shadow Copy:那么 a 会直接复制,bcd 会保留对函数的引用
- Deep Copy:完全复制 abcd,这样会造成环形引用导致错误
没什么好说的,就几个特殊的属性:
注意必须要在严格模式下才会报错
"use strict"; //注意必须要在严格模式下才会报错 var myObject = {}; Object.defineProperty( myObject, "a", { value: 2, writable: false, // not writable! configurable: true, enumerable: true } ); myObject.a = 3; // TypeError
表示是否允许下一次使用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
之后就会变成空引用并且对应资源会被回收但是这玩意不能用于内存回收, 他只是删除了一个属性而已
很多奇怪的函数里面会进行判断这个属性
这不是一个实际的属性, 不过我们有时候需要将一个变量变得永恒不变
, 通过下面这些办法:
很简单:
writable:false
andconfigurable:false
var myObject = {}; Object.defineProperty( myObject, "FAVORITE_NUMBER", { value: 42, writable: false, configurable: false } );
Object.preventExtensions(……)
将令变量无法添加新属性
var myObject = { a: 2 }; Object.preventExtensions( myObject ); myObject.b = 3; myObject.b; // undefined
- 严格模式下: 报错
- 非严格模式: 不报错, 但是修改无效, b 依然等于 2
Object.seal(……)
= Object.preventExtensions(……)
+ configurable:false
但是依然可以修改属性的值
var obj = {name: 'John'} // 密封 Object.seal(obj) // 可以修改已有属性的值 obj.name = 'Backus' console.log(obj.name) // 'Backus'
Object.freeze(……)
= Object.seal(……)
+ writable:false
var obj = {name: 'John'} // 密封 Object.freeze(obj) // 无法修改已有属性的值 obj.name = 'Backus' console.log(obj.name) // 'John', 修改失败
这里只强调 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)) { }
所有的Object
都的最顶层都是Object.prototype
.
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
- 当一个属性在继承链的高层被发现并且可写的话, 那么就会发生 Property Shadowing
- 当然如果在高层发现并且不可写, 那么就会设置失败, 并且严格模式下会直接报错
- 单原型链上存在一个与这个属性相关的
Setter
并且一定会调用到这个Setter
, 那么这个属性的再次赋值必然会失败
constructor 没啥特别的, 一个类对应的函数就是一个 constructor
但是使用new
关键字的时候会调用这个 constructor, 这是唯一一个 constructor 和函数的区别
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 而是 objtest.prototype.constructor
是test()
定义的时候创建的t.constructor
也指向同一个test()
另外, 如果将test
的prototype
改为另一个方法, 那么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.
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
// 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 );
前三种方法中: 父类必然是子类实例对应的 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
关于本文
文章标题 | 你根本不懂Javascript(EP4 this关键字和对象原型) |
发布日期 | 2017-04-16 |
文章分类 | Tech |
相关标签 | #JS |
留言板
PLACE_HOLDER
PLACE_HOLDER
PLACE_HOLDER
PLACE_HOLDER