函数表达式和函数声明
区分函数声明和表达式最简单的方法是看function 关键字出现在声明中的位 置(不仅仅是一行代码,而是整个声明中的位置)
如果foo被绑定在所在的作用域中(也就是说可以通过foo()去调用)那么他就是一个函数声明,如果把他绑定在函数表达式自身的函数中(function(){})()或者var demo=function()意味着他是一个函数表达式,只能在自身所代表的作用域内访问,不会污染全局作用域
匿名和具名函数
setTimeout( function() { console.log("I waited 1 second!"); }, 1000 );
匿名函数不利于调试,可读性差,考虑到可读性推荐使用行内函数表达式(外部依然不可调用)
setTimeout( function timeoutHandler() { console.log( "I waited 1 second!" ); }, 1000 );
垃圾收集
click 函数可能会形成一个覆盖整个作用域的闭包,JavaScript 引擎极有可能依然保存需要被释放的垃圾
function process(data) {
// 在这里做点有趣的事情
}
var someReallyBigData = { .. };
process( someReallyBigData );
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt) {
console.log("button clicked");
}, /*capturingPhase=*/false );
在process调用后,函数some..已经没有存在的必要,所以最好用一个块作用域去包裹需要被销毁的代码,让引擎知道没必要继续保存
经典for循环问题
for (let i=0; i<10; i++) {
console.log( i );
}
for 循环头部的let 不仅将i 绑定到了for 循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个i 的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个timer函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i,所有函数共享一个i的引用
变量提升
函数和var声明的变量都会提升到作用域上方,但是函数表达式是不会提升的
foo(); // 不是ReferenceError, 而是TypeError!
var foo = function bar() {
// ...
};
这段代码中调用之前并没有做赋值操作(应该是当成了a=1 var a)来处理,所以foo()是对undefined进行调用。此外即使是具名函数表达式,名称标识符在赋值之前也是无法在作用域中使用的
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};
等同于
var foo
foo()
bar()
foo=function(){
var bar=...self
}
ReferenceError (引用错误) 调用的变量或者方法未被定义便会报此错误
TypeError (类型错误) 数据类型错误,最常见的是Vue中父传子时props接收参数时容易报该错误,子元素接收的是Number类型,父元素传的是字符串便会报错
词法作用域
function foo() {
console.log( a ); // 3(不是2 !)
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
js是没有动态作用域的,找不到a时他会顺着栈在调用foo的地方查找a,而不是在嵌套的作用域链中向上查
this指向
var obj = {
id: "awesome",
cool: function coolFn() {
console.log( this.id );
}
};
var id = "not awesome"
obj.cool(); // 酷
setTimeout( obj.cool, 100 );//undefined
问题就在于cool函数丢失了this的绑定,回调的时候就找不到指向的对象了
解决方法:
var self = this;
//先提前存储下this
var obj = {
count: 0,
cool: function coolFn() {
var self = this;
if (self.count < 1) {
setTimeout(()=> {
self.count++;
console.log("awesome?");
}, 100);
}
}
};
obj.cool();
但是上面这段代码不用提前存储也不影响,因为箭头函数会向上查找this
这个代码片段中的箭头函数并非是以某种不可预测的方式同所属的this 进行了解绑定,而只是“继承”了cool() 函数的this 绑定
更靠得住/合适的书写方式
var obj = {
count: 0,
cool: function coolFn() {
if (this.count < 1) {
setTimeout(function timer() {
this.count++; // this 是安全的
// 因为bind(..)
console.log("more awesome");
}.bind(this), 100); // look, bind()!
}
}
};
obj.cool(); // 更酷了。
通过bind回调可以保证this的指向不变
为什么要使用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, 我是KYLE
speak.call( you ); // Hello, 我是 READER
在上面的代码中this指向当前参数(这段代码可以在不同的上下文对象(me 和you)中重复使用函数identify() 和speak(),不用针对每个对象编写不同版本的函数。),如果不用this的话也可采用参数传递上下文对象的方式
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify(context);
console.log(greeting);
}
var me = {
name: "Kyle"
};
var you = {
name: "Reader"
};
identify(you)
speak(me)
解决丢失绑定的问题
硬绑定:
强制把foo 的this 绑定到了obj。无论之后如何调用函数bar,它总会手动在obj 上调用foo。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。
function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
var bar = function() {
foo.call(obj);
};
bar(); // 2
// 硬绑定的bar 不可能再修改它的this
bar.call( window ); // 2
API
第三方库的许多函数,以及JavaScript 语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind(..) 一样,确保你的回调函数使用指定的this。
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 调用foo(..) 时把this 绑定到obj
[1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome
new
使用new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行[[ 原型]] 连接。
- 这个新对象会绑定到函数调用的this。📍
- 如果函数没有返回其他对象,那么new 表达式中的函数调用会自动返回这个新对象。
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
this五种绑定
this默认绑定
this默认绑定我们可以理解为函数调用时无任何调用前缀的情景,它无法应对我们后面要介绍的另外四种情况,所以称之为默认绑定,默认绑定时this指向全局对象(非严格模式):在严格模式环境中,默认绑定的this指向undefined
隐式绑定
什么是隐式绑定呢,如果函数调用时,前面存在调用它的对象,那么this就会隐式绑定到这个对象上:
function fn() {
console.log(this.name);
};
let obj = {
name: '听风是风',
func: fn
};
obj.func() //听风是风
如果函数调用前存在多个对象,this指向距离调用自己最近的对象, 即使那个对象没有那个属性
function fn() {
console.log(this.name);
};
let obj = {
func: fn,
};
let obj1 = {
name: '听风是风',
o: obj
};
obj1.o.func() //undefined,这里调用的obj对象
🎈显式绑定相比隐式绑定(bind/apply/call)优先级更高
function foo() {
console.log(this.a);
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo()//2
obj2.foo(); // 3
obj1.foo.call(obj2); // 3
obj2.foo.call(obj1); // 2
类的继承
显式混入
通过一个minin函数进行复制
function mixin(sourceObj, targetObj) {
for (var key in sourceObj) {
// 只会在不存在的情况下复制
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
var Vehicle = {
engines: 1,
ignition: function() {
console.log("Turning on my engine.");
},
drive: function() {
this.ignition();
console.log("Steering and moving forward!");
}
};
var Car = mixin(Vehicle, {
wheels: 4,
drive: function() {
Vehicle.drive.call(this);
console.log(
"Rolling on all " + this.wheels + " wheels!"
);
}
});
console.log(Car)
寄生继承
既是显示又是隐式的,在父类的原型链上定义方法,然后子类去实例化他的父类,用一个变量保存父类原来的方法,改变方法的指向为当前上下文,重写实例上的方法
// “传统的JavaScript 类”Vehicle
function Vehicle() {
this.engines = 1;
}
Vehicle.prototype.ignition = function() {
console.log("Turning on my engine.");
};
Vehicle.prototype.drive = function() {
this.ignition();
console.log("Steering and moving forward!");
};
// “寄生类” Car
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 myCar = new Car();
myCar.drive();
首先我们复制一份Vehicle 父类(对象)的定义,然后混入子类(对象)的定义(如果需要的话保留到父类的特殊引用),然后用这个复合对象构建实例。
原型链
屏蔽
var anotherObject = {
a: 2
};
var myObject = Object.create(anotherObject);
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("a"); // false
myObject.a++; // 隐式屏蔽!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty("a"); // true
尽管myObject.a++ 看起来应该(通过委托)查找并增加anotherObject.a 属性,但是别忘了++ 操作相当于myObject.a = myObject.a + 1。因此++ 操作首先会通过[[Prototype]]查找属性a 并从anotherObject.a 获取当前属性值2,然后给这个值加1,接着用[[Put]] 将值3 赋给myObject 中新建的屏蔽属性a,天呐!修改委托属性时一定要小心。如果想让anotherObject.a 的值增加, 唯一的办法是 anotherObject.a++
实际上进行+运算的时候找到了最初的原型对象,但是只是取他的值+1然后复制给另一个对象中新建的属性
构造函数
function Foo() {
console.log('foo')
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
console.log(a.constructor === Foo); // true
总结:实际上实例对象没有一个指向函数的.constructor ,只是因为在函数声明的时候Foo.prototype.constructor是Foo函数在声明时的默认对象,a是被委托指向Foo
看起来a.constructor === Foo 为真意味着a 确实有一个指向Foo 的.constructor 属性,但是事实不是这样。
这是一个很不幸的误解。实际上,.constructor 引用同样被委托给了Foo.prototype,而Foo.prototype.constructor 默认指向Foo
把.constructor 属性指向Foo 看作是a 对象由Foo“构造”非常容易理解,但这只不过是一种虚假的安全感。a.constructor 只是通过默认的[[Prototype]] 委托指向Foo,这和“构造”毫无关
Foo.prototype 的.constructor 属性只是Foo 函数在声明时的默认属性。如果 你创建了一个新对象并替换了函数默认的.prototype 对象引用,那么新对象并不会自动获得.constructor 属性。
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!