5.1 总结
我喜欢开篇,直接就总结完。
闭包产生的2种情况
- 当函数作为另一个函数的参数
- 函数作为返回值返回
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz();//2 朋友,这就是闭包效果
5.2 循环和闭包
要说明闭包,for循环是一个常见的例子
for ( i = 1; i <= 5; i++) {
setTimeout(function timer(){
console.log(i);
}, i\*1000);
}
这段代码再运行时候会每秒一次的频率输出5次6.
延迟函数的回调会在全部循环迭代结束的之后时候进行调用(请查询宏任务、微任务相关知识点),而不是每次迭代时候调用。所以最后调用i,但是i是公共的,并且值为最后一个循环决定的6。所以结果是5次6
那怎么给每个迭代的版本获取一个实时的i,满足哪怕是最后循环迭代完再调用定时函数,但是每个定时函数都是调用自己版本的,而不是调用最后的公用6呢?
那就是每次循环迭代时候,我们给每一个迭代都绑定一个i。如下所示,我们使用let让每一个i都再内部迭代进行绑定。
for(let i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i);
},i\*1000);
}
5.3 模块
在js中的模块也是和闭包息息相关。
模块:
- 必须要有外部的封闭函数,该函数必须要被调用一次
- 封闭的函数至少要返回一个内部函数
- 使用立即执行函数配合有奇效
var foo = (function(){
var something = "cool";
var another = [1,2,3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join("!"));
}
return {
doSomething,
doAnother
}
})();
foo.doSomething();//cool
foo.doAnother();//1!2!3
第二部分 this和对象原型
第1章 关于this
1.1为什么要使用this
首先记住,this提供了一种更优雅的方式隐式的传递一个对象引用,从而使API设计更加的简洁,并且更加易于复用
1.2对this的一些误解
- 误解一、指向自身
按照this这个单词的语意,我们总是会把他认为是指向自身,事实上有些时候确实如此,但是并不是总指向自身。分析下面的模式。
function foo(num) {
//记录count被调用的次数
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
console.log(foo.count);//0
是不是没想到,哈哈哈。我读到这里时候也是没想到,好像就是从来没有仔细想过这件事一样。首先这里解释一下,这里的this.count会在全局创建一个变量,值为NaN。至于为什么后面第2章在解释,这里只是为了说明this真的不是任何时候指向自己。下面是单独打印的结果。
(function foo(num) {
//记录count被调用的次数
this.count++;
console.log(this);//window,也就是全局
console.log(this.count);//NaN
}())
- 误解二、指向他的作用域
首先这里this有时候指向作用域,有时候又不是,但是明确的一点就是任何时候this都是不会指向他的词法作用域。因为词法作用域是属于引擎的,无法通过js代码进行访问。
首先明确一下前面的案例中foo()中的this是指向window,这里我们在前面的基础上改一改
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log(this.a);
}
foo();//undefined
明明this是window,也就是全局,然后bar也在全局,this.bar()没问题,但是因为bar中有this,按照词法作用域,bar在foo中,this.a右查询,就会查询bar的作用域中的a,但是没有就向上面找,上面foo中有a,所以调用。可是事实呢,结果是找不到,说明this.a进行右查询之后压根没有往上,或者往上后查找失败。(学了后面知道,是没有往上,因为bar中的this不是指向bar作用域,而是全局,所以最顶层没有,也不往上,直接undefined。至于是为什么bar中的this以及foo中的this均为window,请看后面的章节)
第2章 this全面解析
2.1调用位置
调用位置就是函数在代码中被调用的位置,不是声明的位置。
function foo() {
function bar() {
}
bar();//在foo中声明,所以bar的调用位置在foo中
}
foo();//在全局中声明,所以foo内的调用位置在window上
2.2绑定规则
- (1)默认绑定
this绑定在window上。这个时候满足:查看函数调用位置时候,函数是光秃秃的直接调用,比如前面2.1中的bar,尽管是在foo中调用,可以打印看看bar中this是不是指向window - (2)隐式绑定
this绑定到某个对象上。这个时候满足,这个对象的一个属性是一个函数,并且调用这个函数属性时候,是通过对象.函数名()或者对象函数名调用。否者会出现隐式丢失。
var a = 3;//我是全局的3
function foo(){
console.log(this.a);
}
var obj = {
a:2,//我是对象的2
foo:foo
};
obj.foo();//2,this绑定的是obj
obj["foo"]();//2,this绑定的是obj
var bar = obj.foo;//注意,这里可不是调用,后面没有(),这里只是拿出来
bar();//结果是3,明明bar就是foo函数,
//但是注意,这里并不是我们说的调用方法的两种之一
//所以呀,绑定丢失。而这里,
//bar的调用位置是光秃秃的直接调用,
//所以这里this绑定到window上咯
- (3)显式绑定
this绑定到某个对象上,但是其变种不会产生隐式丢失。这种方法很粗暴,直接使用call(),或者apply()。这两个函数,直接修改this的绑定对象。
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};
foo.call(obj);//2。也就是把foo函数的this,绑定到obj上
可是在进行如下调用时候,也会丢失
var a = 3;//我是全局的3
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};
foo.call(obj);
//下面是调用
var bar = obj.foo;
bar()//是3,哈哈哈
显示绑定的变种的核心思想都是外面包裹一个函数,每次调用这个包裹函数就好,哪怕是在调用位置光秃秃调用,都不会绑定到全局的window,因为调用包裹函数,每次自动调用一次apply或者call。下面是变种的写法。
//变种一,直接包裹一个函数
var a = 3;//我是全局的3
var obj = {
a:2,//我是对象的2
foo:foo
};
function foo(){
console.log(this.a);
}
function bar() {
return foo.call(obj);
}
bar();//2 所以每次这样就好,哈哈哈
- (4)new绑定
首先JavaScript中new和其他语言中的new是完全不同。
JS中的new:使用new来调用构造函数,但JS中构造函数不属于某个类,也不会实例化某个类。(关于更多的解释,在第6章)
new的机制为:
创建一个全新的对象
这个新对象执行[原型]链接
这个新对象绑定到函数调用的this中
如果函数没有返回其他对象,new自动返回这个新对象
function foo(a){
this.a =a ;
}
var bar = new foo(2);
console.log(bar.a);//2
2.3优先级
不举例了,这里直接总结结论。
- new最大
- 显示
- 隐式
- 默认
2.4箭头函数
箭头函数是ES6中新东西,不遵循上面的4条绑定规则。而是根据外层作用域决定的。
var a = 3;
var obj = {
a:2
};
function foo(){
((a)=>{
//this继承foo
console.log(this.a);
})()
}
foo()//3 这里光秃秃调用,foo中this是window,所以回调中是window的a
var a = 3;
var obj = {
a:2
};
function foo(){
((a)=>{
//this继承foo
console.log(this.a);
})()
}
foo.call(obj)//2 这里显示调用,foo中this是obj,所以回调中是obj的a
第3章 对象
3.1对象定义的语法
有两种方式,一种是通过声明形式,一种是通过构造形式
//声明
var obj = {
key:value
}
//构造
var obj = new Object();
3.2类型
对象有6种基本类型
- string
- number
- boolean
- null
- undefined
- object
当然这些基本类型本身不是对象,只是说对象是根据基本类型划分有这么几个类型。不明白的可以继续看下去。
内置对象
JS中有许多特殊的对象子类型,被成为内置对象。下面的一些内置对象的名字和基本类型相似,但是不是同一个东西。下面的是实实在在的一个对象,不像前面的基本类型只是判别标准
- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
这些内置的对象好比java语言中的包装类,但是你先明白JavaScript中的类都是函数,是通过函数表达类的表象(这是后话,后面的第四章会有详细的类的讲解)
3.3对象中的内容
首先要强调一点,我们说的内容是,似乎暗示说这些值在对象内部,但是其实不是。如果是学过c语言或者c++的指针,或者明白说java中的引用。理解这一点是很容易的。在js中的对象的内容,对象中对的属性并不保存某些值,而是通过引用的方式,存放的是一些真正值的地址。
书中的3.3.2中,再次阐述,一个函数与一个对象的关系。我通读完后,觉得其实就是任然说明和前面this隐式绑定一个对象的道理一样。一个函数无论如何也不要理解为属于一个对象。我们应该理解为这个对象拥有这个函数,或者说这个对象目前是这个函数的落脚点。毕竟,在对象中,如果某个属性是函数,那么这个属性保存的值其实是这个函数的引用而已。
特别注意数组也是js中的子对象Array。然后尽管是数组按照组织下标的方式存储数据,但是你也可以为数组添加key:value的形式的内容。还记得吗,数组不是有一个length属性吗,这个就是最好的例子。
var array = ["foo","22"];
array.bar = "bar";
console.log(array.bar)//bar
现在关于引用,探讨对对象的内容的拷贝。因为引用的存在,所以出现了深浅拷贝。浅拷贝是拷贝引用,深拷贝而是彻底的进行复制一份数据。而在修改,数据时候,又牵扯到属性描述符以及setter与getter。下面的这两篇博客中部分内容对此进行了总结。
3.4对象中的遍历
for…in循环可以用来遍历对象的可枚举属性列表(包括[prototype]链),但是如何遍历属性的值???对于数组来说:
var array = [1,2,3];
for (let i = 0; i < array.length; i++) {
console.log(array[i]);
}
这可不是遍历数组的值,这是遍历数组的下标,而不是值!!!
通过这个说明了通过for…in遍历值是不行。但是对于数组而言有一些方法可以进行值的遍历,(针对数组的遍历的方法,由于不是这一节的重点,不总结进来)
但是对与其他的对象呢?ES6中就专门增加了for…of结构遍历值(当然数组也可使用这个哈)
var array = [1,2,3];
for(var v of array){
console.log(v);
}
//1
//2
//3
好神奇,成功了。可是这种for…of的核心是怎么工作的呢?
for…of会首先向被迭代对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有的返回值。
某种数据结构中必须要有@@iterator,才可以给for…of一个迭代器对象
数组中有内置的@@iterator,所以可以给for…of一个迭代器对象
var array = [1,2,3];
var it = array[Symbol.iterator]();
it.next();//{value: 1, done: false}
it.next();//{value: 2, done: false}
it.next();//{value: 3, done: false}
it.next();//{value: undefined, done: true}
- 我们使用Symbol.iterator获取@@iterator内部属性,关于点击这里Symbol,请记住引用类似的类似iterator特殊属性时候,要使用符号名array[Symbol.iterator],而不是单独一个array[iterator]
- array[Symbol.iterator]也就是@@iterator本身不是迭代器,而是一个返回迭代器的函数,所以上面的的代码中,通过后面()调用,返回一个迭代器,然后赋值给it
所以最后,让我们按照上面的2条,我们自己为一个遍历的对象实现一个迭代器
var obj = {
a:2,
b:3
}
//下面代码的意义在于给obj定义一个Symbol.iterator属性,这个属性是一个函数,用来返回迭代器
Object.defineProperty(obj,Symbol.iterator,{
enumerable:false,
writable:false,
configurable:true,
//看到了吗,这个属性值是一个函数
value:function () {
var o = this;//也就是这个对象本身,想一想为什么this是指向这个对象本身,我保证前面的this里面讲的知识,绝对可以分析出来这里的this指向哪里
var idx = 0;
var ks = Object.keys(o);//返回这个对象的键值列表
return{
next:function () {
return{
value:o[ks[idx++]],
done:(idx>ks.length)
}
}
}
}
})
//使用for...of遍历
for(var v of obj){
console.log(v);
}
- 这一章需要有一定的类的基础,不然读起来真的云里雾里。如果没有请先查阅其他资料补习。
4.1类理论
- 类/继承描述了一种代码的组织结构形式。
- 多态其实就是说父类的通用行为可以被子类用更加特殊的行为重写。(甚至相对多态允许我们从重写行为中引用基础行为)
面向对象编程强调的是数据和操作数据的行为本质上是相互关联的,因此好的设计就是把数据以及它相关的相关行为打包(封装)起来,有时候这种情况被叫做数据结构。
- 类只是一种模式,而不是必须的!当然java,c#这些语言,没得选,只能使用类模式。
- JavaScript中并不是必须采用的类模式。现在JavaScript中的“类”,也只是近似类。记住一点,js的“类”和他们都不一样,这也是加双引号的原因。
4.2类的机制
书中表达的观点是,类和实例对象之间的关系看作之间关系而不是间接关系更好。因为所谓的类的关系,都是复制而已。具体怎么复制,以及细节,看下面。
构造函数就是复制的关键点。他的目的就是完成复制的关键。术语叫做初始化对象。
构造函数的特点
- 使用new来调用构造函数
- 函数名与类名相同
- new的过程,请看前面讲解this时候笔记
4.3类的继承
下面的书中的例子,是我觉得这个章节中最最最精彩的部分
下面的伪代码例子说明了2点:
- 1、在js中的继承就是复制!!!
- 2、js中的多态!!!
- 3、js中的相对多态!!!
//下面几个类均不含构造方法
class Vehicle{ //交通工具类
engines = 1 //交通工具的属性
ignition(){//交通工具的方法
output("Turning on my engine , Vehicle!")
}
drive(){//交通工具的方法
ignition();
output("Steering and moving forward")
}
}
class Car inherits Vehicle{ //一个继承交通工具类的汽车类
wheels = 4//汽车的属性
drive(){//这里就是子类重写父类的方法
inherited:drive();//这里就是子类引用父类基础行为,这就是相对多态
output("Steering and moving forward")//这里就是子类更加特殊的行为,这就是多态
}
}
class SpeedBoat inherits Vehicle{ //一个继承交通工具类的快艇类
engines = 2//快艇的属性
ignition(){//快艇的方法 这里就是子类重写父类的方法
output("Turning on my engine ,SpeedBoat!")//这里就是子类更加特殊的行为,这就是多态
}
drive(){//快艇的方法 子类重写父类的方法
inherited:drive();//这里就是子类引用父类基础行为,这就是相对多态
output("SpeedBoat through the water with ease")//这里就是子类更加特殊的行为,这就是多态
}
}
好啦,上面的2、3点都写在注释中了,可是“在js中的继承就是复制!!!”,我们还没有得到解释。
仔细看一个有趣的点:
- (1)SpeedBoat中有一个drive方法对吧
- (2)这个是引用的方法对吧
- (3)所以SpeedBoat中的drive方法来自父类,自然里面的代码是这样
drive(){//交通工具的方法
ignition();
output("Steering and moving forward")
}
- (4)这个时候drive里面继续调用ignition(),确实没毛病
- (5)回顾一下前3点:这个SpeedBoat中的drive来自Vehicle,这个drive调用ignition
- (5)问题来了,SpeedBoat 与 Vehicle均有ignition,调用那个的呢?
- (6)答案是SpeedBoat
- (7)这说明了什么?
在js中的继承就是复制!!! 调用的是SpeedBoat中的,而不是Vehicle,说明了他们两个类没有所谓的关系,SpeedBoat就是Vehicle的一个复刻品,只不过后面这个复刻品有了一些自己的新东西(多态)。如果不是复制,为啥从Vehicle继承的drive,不去找Vehicle中调用ignition,而是去SpeedBoat中?直接从SpeedBoat使用ignition,说明SpeedBoat已经复制了Vehicle的一切,就使用自己,而不是去父类中找。
抱歉,鉴于我的表达能力,上面的话说的有些绕,不明白请看原书第一版的p131-p133
4.4混入
上面解释了js中的继承就是复制,这里混入就是讲解几种种不同复制方式
- 显示混入
- 隐式混入
这里就不总结前两个了,问题比较多,感觉不怎么使用到。这里提前补充一个使用原型进行混入,看不懂可以跳过这段
function Animal(name) {
this.name = name
}
Animal.prototype.showName = function () {
console.log("我的名字是" + this.name)
}
function Dog(name,color) {
//强制吧animal中的this,绑定当new出来的对象,
//不知道为啥this指向new出来的对象的同学
//请看前面this部分,以及new的几个过程
Animal.call(this,name)//只能继承属性
this.color = color
}
Dog.prototype = new Animal()
Dog.prototype.constructor = Dog
第5章 原型
性能优化
1.webpack打包文件体积过大?(最终打包为一个js文件)
2.如何优化webpack构建的性能
3.移动端的性能优化
4.Vue的SPA 如何优化加载速度
5.移动端300ms延迟
6.页面的重构
所有的知识点都有详细的解答,我整理成了280页PDF《前端校招面试真题精编解析》。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】