https://lanhai1.github.io/archives/ 个人博客
一、JavaScript执行三步骤
1.1 语法分析
通篇扫描 检测低级语法错误
1.2 预编译
1.2.1 什么是预编译?
将声明式函数和变量 提升到当前作用域的最顶端
声明式变量只提升 声明变量 不提升赋值
声明式函数是将整个函数提升
1.2.2 全局预解析和局部预解析 “四步”
1.2.2.1 全局预解析
- 创建GO对象 (Global Object => 全局对象) GO => window
- 找变量声明 将变量作为GO对象的属性名 值为undefined
- 将变量值统一
- 在函数体里面找函数声明 值赋予函数体 找声明的函数作为GO的属性名 再将对应的函数体统一
1.2.2.2 局部预解析(函数)
- 创建AO对象(activation Object => 活跃对象) 执行期上下文(作用域)
- 找形参和变量声明 将变量和形参名作为AO属性名 值为undefined
- 将实参值和形参统一(赋值)
- 在函数体里面找函数声明 值赋予函数体 找声明的函数作为AO的属性名 再将对应的函数体统一
1.2.3 注意
函数提升的优先级比变量提升的要高
而且不会被变量声明覆盖
但会被变量赋值覆盖!!!
1.2.4 测试题目
console.log(a) // fn
var a = 123;
function a() {
}
console.log(a) //123
预编译后
var a;
function a() {}
console.log(a) // fn
a = 123;
console.log(a) // 123
1.3 解释执行
解释一行执行一行
二、编程思想
2.1 面向过程
所有的事都是亲力亲为 注重过程
2.2 面向对象
2.2.1 什么是面向对象?
提需求 找对象 对象解决 注重结果
面向对象其实就是面向过程的高度封装
详解
根据需求 抽象出相关的对象 总结对象的特征和行为 把特征变成属性 把行为变成方法 然后定义构造函数 实例化对象 通过对象调用属性和方法 完成相关的需求
2.2.2 面向对象的三大特征
2.2.2.1 封装
就是包装 把一些重用的内容进行包装 在需要的时候直接调用
把一个值存在一个变量中 把一些重用的代码放在一个函数中 把好多相同功能的函数放在一个对象中 把好多功能的对象放在一个js文件夹中
2.2.2.2 继承
类与类之间的关系(一个类拥有另一个类的属性和方法)
es6之前没有类的概念 可以通过构造函数的原型来模拟继承
2.2.2.2.1 继承的作用
继承就是为了共享数据
2.2.2.2.2 继承的三种方式
- 混入式继承
forin 去遍历要被继承的对象 把遍历出来的所有属性赋值给继承的对象
Son[key] = Father[key]
缺点 : 有多少个对象就要遍历多少次 - 替换原型继承
准备构造函数 再把这个构造函数对应的原型对象替换成要被继承的类
Son.prototype = Father
缺点 : 会改变原来构造函数的指向 - 混合式继承 ✅
用 forin 遍历要被继承的对象 再把这些成员加到要被继承的构造函数的原型里
Son.prototype[key] = Father[key]
2.2.2.3 多态
同一个行为 针对不同的袖 产生不同的结果
一种行为 多种实现 如函数中的arguments
三、原型对象
3.1 什么是原型?
本质上也是一个对象
当声明一个构造函数 浏览器就会自动创建一个对应的原型对象 prototype
3.1.2 什么是对象?
具体特指的某一个事物 有特征(属性) 和 行为(方法) 一堆无序属性的集合
3.2 原型对象的作用
保存一些这个构造函数实例化出来的对象的一些共同的方法和属性
构造函数名.prototype.sayHi = () => {}
3.3 在原型对象中创建变量和方法的好处
不会造成变量名污染和内存浪费
3.4 如何访问原型中的变量和方法?
构造函数名.prototype
3.5 如何修改原型对象属性的值?
- 构造函数名.prototype.属性 = 新数据 (给程序员使用的)
- 实例对象.__proto__.属性 = 新数据(已废弃 不要使用__proto__ 是给浏览器使用的)
3.6 实例化后访问构造函数中属性和方式的规则
如果构造函数中有就访问构造函数中的
如果构造函数中没有就访问原型对象的
如果都没有 变量则undefined 方法则报错
3.5 实例化对象为什么可以访问原型对象中的方法和属性?
实例化对象是通过里面的 __proto__ 去找原型对象中的方法和属性 因为 __proto__ 里面保存的就是原型对象 所以当访问一个构造函数中没有的属性和方法时候就会通过 __proto__ 指向的原型对象中去寻找
3.6 如何替换原型?
构造函数.prototype = { }
注意这样替换原型 需要在对象中 设置构造器指向的构造函数
3.7 实例化对象指向的是哪一个原型?
- 如果是替换前创建的实例化对象 那么指向原来的原型对象
- 如果是替换后创建的实例化对象 那么指向新的原型对象
注意
原型对象替换后不会影响原来创建的实例化对象 但是后面创建的实例化对象就改变了
替换原型不会覆盖之前的原型对象 只是重新创建了一个对象 让构造函数指向新的原型对象
四、构造函数
4.1 为什么要使用构造函数?
可以方便我们快速创建同一类型的对象
4.2 构造函数的命名规范
首字母大些 大驼峰式命名法 函数名取名词
4.3 new关键字的四件事
- 在内存中申请一块空间 存储创建的对象
- 将构造函数的this 替换成 new 出来的实例化对象
- 为改变后的this设置对应的属性和方法
- 把创建的对象返回
4.4 使用构造函数的好处
不用自己创建对象 不用自己设置返回值 不会出现全局变量的污染
4.5 为什么说函数也是一个对象?
- 函数名.prototype 说明函数也是一个对象
- 作为构造函数 它可以实例化出一个对象
4.6 什么是实例成员?
- 由实例化对象才能调用
- 先实例化 再调用
-
let date = new Date() => date.get...()
date就是一个实例成员
4.7 什么是静态成员?
- 由构造函数名直接调用
- 方便我们调用方法 不需要实例化 直接可以调用
-
Math.ceil()
Math就是一个静态成员
五、构造函数和原型对象和实例化对象的关系
- 当声明一个构造函数的时候 浏览器就会自动创建一个这个构造函数对应的原型对象(prototype)
- 原型对象(prototype)中的 构造器(constructor) 指向了构造函数
- 通过构造函数 实例化出来的对象通过__proto__指向原型对象(prototype)
六、原型链
6.1 什么是原型链?
实例对象与原型对象之间的关系 通过实例对象的\_\_proto__相连接
6.2 原型链有什么好处?
实现继承 数据共享 节约内存 避免命名污染
6.3 全部对象最终都会继承自 Object.prototype 吗?
不会 不能说是全部对象 可以说是绝大多数对象
Object.create(原型对象 || null)
6.4 原型链图释
七、闭包
7.1 为什么要有闭包?
局部变量的垃圾回收机制
: 局部变量在它所在的大括号(函数)结束后 就会立即被销毁 所以在函数外面是无法访问在函数内声明的变量 不过可以通过闭包 在函数外部来访问函数内部的数据
7.2 什么是闭包?
- 闭包是一个函数 是一种可以访问其他函数内部数据的函数
- 函数A中有函数B 函数B访问函数A中的变量或数据 此时就形成了闭包
7.3 闭包的作用
- 可以延长局部变量的声明周期
- 缓存数据(即是缺点也是优点 数据没有及时释放) 延长作用域链
7.4 闭包的三种写法
- 返回函数法
function a(){
let num = 1;
function b(){ //闭包
num++;
}
return b;
}
- 引用函数法
let res ;
function a(){
let num = 1;
function b(){ //闭包
num++;
}
res = b;
}
- window引用法
function a(){
let num = 1;
function b(){ //闭包
num++;
}
window.b = b;
}
7.5 闭包和全局变量保存局部变量的区别
全局变量去保存局部变量 不是延长局部变量的生命周期 而是相对于把局部变量的值复制了一份给全局变量 局部变量当执行到大括号(出了函数作用域) 还是会被销毁
闭包 是完全延长了局部变量的生命周期 后面通过闭包访问那个局部变量 访问的还是本来的局部变量
八、深拷贝和浅拷贝
8.1 深拷贝
把一个对象中的所有属性和方法一个一个找到 并且在另一个对象中开辟相应的空间 一个一个存储到另一个对象中
8.2 浅拷贝
就是复制 相对于把一个对象中的所有内容复制了一份给另一个对象 或者说是把一个对象的地址给了另一个对象 他们的指向相同
九、沙箱(黑盒)
沙箱也就黑盒 在虚拟的环境中模拟真实世界的数据
模拟世界的数据的结果和真实世界的数据结果是一样的 但是不会影响真实世界的数据
十、递归
10.1 什么是递归?
函数自己调用自己
10.2 递归的好处
让代码变的简洁
10.3 如何递归
- 找规律
- 找出口 => 结束条件
10.4 递归的返回过程
先执行的最后才被返回(最后才被执行完)
=> 所以递归的效率不高
10.5 优化递归性能问题
10.5.1 为什么递归的效率不高?
递归很多在左边的都已经算过了
但是在右边又会重新计算一次
10.5.2 解决方法
通过一个对象保存已经递归计算完成的结果
在递归的时候首先判断对象中是否有这个递归结果
有则直接返回结果
没有则计算出结果后保存到对象中
// 优化斐波那契数列
var count = 0;
var obj = {};
function fibNumber(n) {
count++;
// 先看之前有没有算出来过
if (obj[n]) {
// 如果有直接取出来返回
return obj[n];
} else {
if (n == 1 || n == 2) {
return 1;
}
obj[n] = fibNumber(n - 1) + fibNumber(n - 2);
return obj[n];
}
}
十一、this指向
- 普通函数(直接通过函数名调用)中的this是window
- 构造函数通过new关键字调用的函数 this是 实例化的对象
- 定时器函数中的this是window
- 对象中的函数的this 如果是通过对象.函数名调用的 则this就是对象
十二、改变this指向的三种方法
12.1 apply
函数.apply(改变后的this指向对象,[参数1,参数2,...,参数n])
12.2 call
函数.call(改变后的this指向对象,参数1,参数2,...,参数n)
12.2.1 apply 和 call 的区别
传递参数的方式不一样 call直接逗号隔开传递 apply将传递的实参用数组传递 apply 和 call 都会直接调用函数
12.3 bind
函数.bind(改变后的this指向对象,参数1,参数2,...,参数n)
12.3.1 bind 和 其他两个的区别
bind 不会立刻调用函数 是将修改后的this绑定后的函数返回 可以用变量接收后再调用
十三、in 关键词
- 可以循环遍历对象
key 是 属性名
- 可以循环遍历数组
key 是 下标
- 可以判断属性名或方法名是否在对象里存在
判断的是用双引号则表示去找这个属性名
判断 不加引号则表示 是找这个变量的值
let obj = { age: 1 }
"age" in obj => true
age in obj => undefined in obj => false
let age = "age"
age in obj => "age" in obj => true
- 可以判断是否这个下标的值存在或方法名是否在数组里存在
let arr = [1,1,1]
0 in arr => true
"push" in arr => true // 数组中有这个方法
十四、instanceof 关键词
- 对象 instanceof 构造函数
- 判断构造函数的原型对象 在不在这个对象的原型链上
十五、delete 关键词
- 删除不规则声明的变量
只能删除 没用 var 关键词直接声明的变量
不能删除 用 var 关键词声明的变量
- 删除对象的成员(属性和方法)
delete obj.name
delete obj.sayHi
十六、短路运算符
短路只存在于 与&& 和 或|| 中
16.1 &&
一假则假
左边式子为假就短路
16.2 ||
一真则真
左边式子为真就短路
16.3 什么是短路?
如果&&和||中通过了左边的式子已经得到了结果
那么右边的式子就不会再执行 这就叫短路
16.4 运算最后结果是什么?
运算结果不一定是true和false
如果没有短路 运算结果就是右边表达式的结果
如果短路了 运算结果就是左边表达式的结果
如果是做比较则返回boolean值
如果是做运算则返回运算结果