本文最初發(fā)布于http://szhshp.org
轉(zhuǎn)載請(qǐng)注明
This關(guān)鍵字
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
同時(shí)如果不使用this
我們可以傳入一個(gè)上下文到調(diào)用的函數(shù)中,例如這樣:
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
幾個(gè)對(duì)this
關(guān)鍵字的誤解
認(rèn)為this
是指向函數(shù)自身
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?
但是實(shí)際上我們操作的不是這個(gè)foo
里面的count
而是一個(gè)全局變量count
解決方案
當(dāng)然解決這個(gè)問(wèn)題很簡(jiǎn)單,不要在函數(shù)中操作this
就是一個(gè)Solution:
function foo(num) {
console.log( "foo: " + num );
// keep track of how many times `foo` is called
foo.count++;
}
foo.count = 0;
或者操作一個(gè)全局的count.
或者用另一種辦法強(qiáng)行使用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 );
}
}
認(rèn)為this
指向函數(shù)的scope
這太愚蠢了...
function foo() {
var a = 2;
this.bar(); //還不如不加this關(guān)鍵字直接調(diào)用_(:з」∠)_
}
function bar() {
console.log( this.a );
}
foo(); //undefined
方法調(diào)用及調(diào)用棧
想要理解this
首先就要了解一個(gè)方法在哪里調(diào)用的
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`
多數(shù)瀏覽器的Debugger
工具可以方便地看到調(diào)用棧
調(diào)用規(guī)則
-
默認(rèn)綁定
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
不生效并且會(huì)報(bào)Undefined
- 直接定義的變量都屬于
-
隱式綁定
function foo() { console.log( this.a ); //`this.a` is synonymous with `obj.a`. } var obj = { a: 2, foo: foo }; obj.foo(); // 2
注意這里的調(diào)用處僅僅會(huì)剝離一層,因此最后一個(gè)調(diào)用者將會(huì)是
this
所代表的內(nèi)容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"
特別對(duì)于上面
setTimeout
函數(shù)function setTimeout(fn,delay) { // wait (somehow) for `delay` milliseconds fn(); // <-- call-site! }
-
顯式綁定
當(dāng)調(diào)用
call()
或者applt()
的時(shí)候我們可以強(qiáng)行傳一個(gè)obj
作為this
function foo() { console.log( this.a ); } var obj = { a: 2 }; foo.call( obj ); // 2
同時(shí)注意如果給
this
傳進(jìn)原始類型的數(shù)據(jù)時(shí),對(duì)應(yīng)數(shù)據(jù)會(huì)進(jìn)行裝包(boxing),即轉(zhuǎn)換成對(duì)應(yīng)Obj (new String(..), new Boolean(..), or new Number(..), respectively) -
強(qiáng)綁定
function foo() { console.log( this.a ); } var obj = { a: 2 }; var bar = function() { foo.call( obj ); // 強(qiáng)行將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()
方法可以強(qiáng)行設(shè)定this
的值為某個(gè)其他變量。
使用new
關(guān)鍵字時(shí)發(fā)生了什么
- 新建立一個(gè)Obj
- 將這個(gè)Obj與原型相連接(見(jiàn)后文詳解)
- 新建立的Obj設(shè)置為對(duì)應(yīng)函數(shù)的
this
- 除非函數(shù)返回了一些莫名其妙的東西,否則自動(dòng)返回新建立的元素
function foo(a) {
this.a = a+1;
}
var bar = new foo( 2 );
console.log( bar.a ); // 3
綁定順序
-
new
綁定的條件下,那么這是一個(gè)全新的Objvar bar = new foo()
-
通過(guò)
call
或者apply
進(jìn)行顯式綁定,或者使用了bind
進(jìn)行強(qiáng)綁定,那么這就是個(gè)顯式綁定的Objectvar bar = foo.call( obj2 )
-
通過(guò)上下文進(jìn)行隱式調(diào)用,或者是某個(gè)對(duì)象的Attr,那么
this
就是當(dāng)前上下文var bar = obj1.foo()
-
否則就是默認(rèn)綁定了。記住如果是嚴(yán)格模式
this=undefined
, 否則this=global object
var bar = foo()
例外情況
當(dāng)模塊不需要用到this
的時(shí)候,但是卻需要使用bind
等函數(shù),可以將null
傳到this
。
同時(shí)這種情況下就會(huì)默認(rèn)使用默認(rèn)綁定
的規(guī)則
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
話說(shuō)這個(gè)到底怎么翻譯啊。。重定向嗎?
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
還是很好理解的,上面的賦值語(yǔ)句執(zhí)行后返回了一個(gè)單純的foo
變量,因此導(dǎo)致了Indirection
,并且使用了默認(rèn)綁定
注意默認(rèn)綁定的規(guī)則:
-
non-strict mode
模式下:引用global object
-
strict mode
模式下:對(duì)應(yīng)引用變成Undefined
語(yǔ)義綁定/Lexical this/ES6
ES6多了個(gè)新玩意:箭頭符號(hào)
相關(guān)的綁定稱作"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 ); // 返回值是一個(gè)函數(shù),并且函數(shù)里面的this被綁定到obj1
bar.call( obj2 );
// 輸出2, not 3!
如果是普通函數(shù)輸出應(yīng)該是3因?yàn)?code>this綁定到了obj2
而語(yǔ)義綁定無(wú)法被重載,即使用了new
關(guān)鍵字
一個(gè)例子:
function foo() {
setTimeout(() => {
// `this` here is lexically adopted from `foo()`
console.log( this.a );
},100);
}
var obj = {
a: 2
};
foo.call( obj ); // 2
另一種針對(duì)箭頭符號(hào)的解決方案,通過(guò)外部重新賦值來(lái)實(shí)現(xiàn)可讀性,這樣就知道這兒的this
是指向函數(shù)的了
function foo() {
var self = this; // lexical capture of `this`
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
不過(guò)上述兩段代碼都是某種意義上的偷懶
,如果真的想要掌握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.
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 );
上面這一段玩意,如果使用
- Shadow Copy:那么a會(huì)直接復(fù)制,bcd會(huì)保留對(duì)函數(shù)的引用
- Deep Copy:完全復(fù)制abcd,這樣會(huì)造成環(huán)形引用導(dǎo)致錯(cuò)誤
屬性標(biāo)識(shí)符 Property Descriptors
沒(méi)什么好說(shuō)的,就幾個(gè)特殊的屬性:
Writable
注意必須要在嚴(yán)格模式下才會(huì)報(bào)錯(cuò)
"use strict"; //注意必須要在嚴(yán)格模式下才會(huì)報(bào)錯(cuò)
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // not writable!
configurable: true,
enumerable: true
} );
myObject.a = 3; // TypeError
Configurable
表示是否允許下一次使用defineProperty
進(jìn)行配置
非嚴(yán)格模式下也會(huì)報(bào)錯(cuò), 這是一種無(wú)法返回的操作
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
并且設(shè)置為false之后也無(wú)法使用delete
刪除對(duì)應(yīng)的屬性
myObject.a; // 2
delete myObject.a;
myObject.a; // 2, 上一句上刪除失敗了
delete
用于刪除一個(gè)是對(duì)象的屬性
, 如果這個(gè)屬性是某變量的最后一個(gè)屬性, 那么delete
之后就會(huì)變成空引用并且對(duì)應(yīng)資源會(huì)被回收但是這玩意不能用于內(nèi)存回收, 他只是刪除了一個(gè)屬性而已
Enumerable
很多奇怪的函數(shù)里面會(huì)進(jìn)行判斷這個(gè)屬性
Immutability
這不是一個(gè)實(shí)際的屬性, 不過(guò)我們有時(shí)候需要將一個(gè)變量變得永恒不變
, 通過(guò)下面這些辦法:
對(duì)象常量 Object Constant
很簡(jiǎn)單:
writable:false
andconfigurable:false
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
} );
關(guān)閉擴(kuò)充性 Prevent Extensions
Object.preventExtensions(..)
將令變量無(wú)法添加新屬性
var myObject = {
a: 2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined
- 嚴(yán)格模式下: 報(bào)錯(cuò)
- 非嚴(yán)格模式: 不報(bào)錯(cuò), 但是修改無(wú)效, 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)
// 無(wú)法修改已有屬性的值
obj.name = 'Backus'
console.log(obj.name) // 'John', 修改失敗
Class
這里只強(qiáng)調(diào)ES6的class
的使用方法
基本和多數(shù)OO語(yǔ)言一樣
// 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;
}
};
構(gòu)造函數(shù)和屬性方法
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);
靜態(tài)方法
不通過(guò)初始化實(shí)例就能調(diào)用的方法
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();
注意即使是以前使用原型創(chuàng)造的父類也可以進(jìn)行繼承
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關(guān)鍵字
class Lion extends Cat {
speak() {
super.speak(); // 直接用super關(guān)鍵字
console.log(this.name + ' roars.');
}
}
多繼承
ES不支持多繼承,但是可以用mixin
的方法偽裝一個(gè):
//將一個(gè)類傳入,并且返回一個(gè)擴(kuò)展之后的類
var calculatorMixin = Base => class extends Base {
calc() { }
};
//同樣將一個(gè)類傳入,并且返回一個(gè)擴(kuò)展之后的類
var randomizerMixin = Base => class extends Base {
randomize() { }
};
class Foo { } //初始化一個(gè)類
//將類傳入,進(jìn)行兩次擴(kuò)展,然后擴(kuò)展到子類Bar中,如此就進(jìn)行了多次擴(kuò)張類似于多繼承
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是繼承過(guò)來(lái)的自然返回false
myObject.a++; // oops, implicit shadowing!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true
注意上面如果不給子類自增而直接給父類執(zhí)行自增,那么子類因?yàn)槭钦{(diào)用繼承的屬性因此也會(huì)返回3
- 當(dāng)一個(gè)屬性在繼承鏈的高層被發(fā)現(xiàn)并且可寫(xiě)的話, 那么就會(huì)發(fā)生Property Shadowing
- 當(dāng)然如果在高層發(fā)現(xiàn)并且不可寫(xiě), 那么就會(huì)設(shè)置失敗, 并且嚴(yán)格模式下會(huì)直接報(bào)錯(cuò)
- 單原型鏈上存在一個(gè)與這個(gè)屬性相關(guān)的
Setter
并且一定會(huì)調(diào)用到這個(gè)Setter
, 那么這個(gè)屬性的再次賦值必然會(huì)失敗
constructor
constructor 沒(méi)啥特別的, 一個(gè)類對(duì)應(yīng)的函數(shù)就是一個(gè)constructor
但是使用new
關(guān)鍵字的時(shí)候會(huì)調(diào)用這個(gè)constructor, 這是唯一一個(gè)constructor和函數(shù)的區(qū)別
constructor和prototype的關(guān)系
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
的時(shí)候執(zhí)行了對(duì)應(yīng)的constructor, 輸出 -
t
是沒(méi)有prototype
這個(gè)屬性的, 因?yàn)樗皇莄lass而是obj -
test.prototype.constructor
是test()
定義的時(shí)候創(chuàng)建的 -
t.constructor
也指向同一個(gè)test()
另外, 如果將test
的prototype
改為另一個(gè)方法, 那么t.constructor
也會(huì)指向那個(gè)新方法
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
因?yàn)槲覀儗?code>test.prototype轉(zhuǎn)到了一個(gè)新的Obj上面, 并且修改之后test.prototype.constructor
不存在了 ,因此接下來(lái)初始化的Obj會(huì)繼承最高層的Object.prototype.constructor
解決這個(gè)問(wèn)題的方法很簡(jiǎn)單, 在切換這個(gè)test.prototype
的同時(shí)也將constructor也賦值過(guò)去, 或者直接在新的prototype里面放一個(gè)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"
錯(cuò)誤的繼承方法
// 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進(jìn)行擴(kuò)展(比如添加新方法)的時(shí)候?qū)嶋H擴(kuò)展了Foo
第二行同樣使用Foo的constructor來(lái)創(chuàng)建新實(shí)例, 但是要注意進(jìn)行擴(kuò)展(比如擴(kuò)展this)的時(shí)候同樣會(huì)擴(kuò)展到Foo
ES6的擴(kuò)展
// 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
前三種方法中: 父類必然是子類實(shí)例對(duì)應(yīng)的class
就是OOP里面根據(jù)instance獲取對(duì)應(yīng)class的方法:
a instanceof Bar; // true
a instanceof Foo; // true, Bar is inherited from Foo
更詳細(xì)的一種方法:
function isRelatedTo(o1, o2) {
function F(){}
F.prototype = o2;
return o1 instanceof F; //重點(diǎn)還是和F的prototype進(jìn)行匹配, 即使F是個(gè)空函數(shù)
}
更簡(jiǎn)單的一種方法:
Foo.prototype.isPrototypeOf( a ); // true
簡(jiǎn)單粗暴的ES5的方法:
Object.getPrototypeOf( a ) === Foo.prototype; // false, 如果Bar繼承于Foo, 此處依然檢測(cè)不出來(lái)
Object.getPrototypeOf( a ) === Bar.prototype; // true
總結(jié)
上方繼承代碼集合:
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
使用原始的對(duì)象連接OLOO (objects-linked-to-other-objects)模式來(lái)實(shí)現(xiàn)上方的代碼:
var Foo = { /* .. */ };
var Bar = Object.create( Foo );
Bar...
var b1 = Object.create( Bar );
對(duì)應(yīng)的類反射就有些不同:
// 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