JavaScript重要知识点梳理
变量类型的转换与比较
题:== 是如何进行比较的?== 与 === 的区别是什么?
[]==![]
输出什么?
== 比较流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jnVjC6Uk-1605010725370)(media/16045936766013/16046002934990.jpg)]
-
判断类型是否一致,一致则判断值的大小是否一致
-
不一致则进行隐式转换
-
判断两个值是否为 null 和 undefined,如果是就返回 true
-
继续判断是否为 string 和 number,如果是,则把 string 转换为 number 再比较大小
-
不是则判断其中一方是否为 Boolean,如果是就将 Boolean 转换为 number 再判断
-
再判断如果一方是 object,另一方是 string、number、symbol,则把object 转换为原始类型再判断
toString 与 valueOf 转换:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XGYXiT4S-1605010725374)(media/16045936766013/16046015806413.jpg)]
=== 的比较流程:
-
严格判断,直接判断类型是否相同,不同返回 false
-
相同再比较大小,不隐式转换
-
如果两边都是引用类型,则判断的两个的引用内存地址是否一致
原型与原型链
在规范里,原型 prototype 被定义为:给其它对象提供共享属性的对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5yA3lj1q-1605010725377)(media/16045936766013/16046285291294.jpg)]
由上图我们可以看到:
- 我们定义了一个 obj 对象,并对它申明了一个 a 属性。但当我们打印obj的时候,发现他有它有
__proto__
属性,__proto__
属性的值也是一个对象。(问题:__proto__
属性是干嘛的,怎么来的?)这意味着obj对象被隐式(不是自己创建)挂载了另一个对象的引用,放到了obj对象的__proto__
属性中。
__proto__属性
obj.__proto__ === Object.getPrototypeOf(obj) // true
提示:Object.getPrototypeOf():返回指定对象的原型(内部[[Prototype]]属性的值)
由上面代码可以知道,__proto__
属性其实隐式挂载了对象的原型。
__proto__
的由来:
-
ECMAScript 规范说 prototype 应当是一个隐式引用
-
通过 Object.getPrototypeOf(obj) 间接访问指定对象的 prototype 对象
-
通过 Object.setPrototypeOf(obj, anotherObj) 间接设置指定对象的 prototype 对象。
-
部分浏览器提前开了
__proto__
的口子,使得可以通过obj.__proto__
直接访问原型,通过obj.__proto__ = anotherObj
直接设置原型。 -
ECMAScript 2015 规范只好向事实低头,将
__proto__
属性纳入了规范的一部分
由此我们可以知道:
-
表面看对象里存在一个
__proto__
属性,实际上,它只是开发者工具为了方便让开发者查看原型,故意渲染出来的虚拟节点。虽然跟对象的其它属性并列,但并不在该对象中。 -
__proto__
属性既不能被 for in 遍历出来,也不能被 Object.keys(obj) 查找出来 -
当我们访问对象的
obj.__proto__
属性时,其实访问的是Object.prototype
对象上__proto__
属性 -
所有的引用类型(数组、对象、函数),__proto__属性值指向它的构造函数的prototype属性值
constructor
constructor属性是对象所独有的,它是一个对象指向一个函数,这个函数就是该对象的构造函数。
- 每一个对象都有其对应的构造函数,本身或者继承而来。
- 单从constructor这个属性来讲,只有prototype对象才有。
- 每个函数在创建的时候,JavaScript 会同时创建一个该函数对应的prototype对象,而
函数创建的对象.__proto__ === 该函数.prototype
,该函数.prototype.constructor===该函数本身
- 故通过函数创建的对象即使自己没有constructor属性,它也能通过__proto__找到对应的constructor
- 所以任何对象最终都可以找到其对应的构造函数
原型
JavaScript 中万物皆对象,而对象皆出自构造函数
-
只有对象类型才有原型概念
-
普通对象(即使用对象字面量或者Object构造器创建的对象)的原型为__proto__属性,此属性其实是个访问器属性,并不是真实存在的属性
-
普通函数有2个属性,一个是是__proto__(与普通对象类似),还有一个是函数专有的prototype属性
-
不是所有的对象都会有原型,比如对象原型Object.prototype的原型Object.prototype.__proto__就指向null
-
箭头函数虽然属于函数,由Function产生,但是没有prototype属性没有构造器特性,所以也就没有所谓的constructor,就不能作为构造器使用
原型链
__proto__是对象所独有的,并且__proto__是一个对象指向另一个对象,也就是他的原型对象。我们也可以理解为父类对象。它的作用就是当你在访问一个对象属性的时候,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的对象(父类对象)上查找,如果父类对象依旧不存在这个属性,那么就会去其父类的__proto__属性所指向的父类的父类上去查找。以此类推,直到找到 null。而这个查找的过程,也就构成了我们常说的原型链。
作用域与作用域链
作用域是指程序源代码中定义变量的区域:作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
静态作用域与动态作用域的区别:
- 静态作用域:函数的作用域在函数定义的时候就决定了
- 动态作用域:函数的作用域是在函数调用的时候才决定的
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
// 结果是 ???
静态作用域执行过程:
执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。
动态作用域执行过程:
执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。
JavaScript采用的是静态作用域,所以这个例子的结果是 1。
ES6 之前 JS 没有块级作用域。只有全局作用域和函数作用域。(看以下两个例子理解)
if (true) {
var number = 1
}
console.log(number) // 1
// 把var改成let,输出什么
var a = 100
function fn() {
var a = 200
console.log('fn', a)
}
console.log('global', a)
fn()
全局作用域就是最外层的作用域,如果我们写了很多行 JS 代码,变量定义都没有用函数包括,那么它们就全部都在全局作用域中。这样的坏处就是很容易撞车、冲突。
作用域注意点:
-
window对象的所有属性拥有全局作用域
-
内层作用域可以访问外层作用域,反之不行
-
var声明的变量,在除了函数作用域之外,在其他块语句中不会创建独立作用域
-
let和const声明的变量存在块语句作用域,且不会变量提升
-
同作用域下不能重复使用let、const声明同名变量,var可以,后者覆盖前者
-
for循环的条件语句的作用域与其循环体的作用域不同,条件语句块属于循环体的父级作用域
// 以下语句使用let声明不报错,说明为不同作用域 for (let i = 0; i < 5; i++) { let i = 5 }
作用域链
作用域链也就是所谓的变量查找的范围
在当前作用域引用变量时,如果没有此变量,则会一路往父级作用域查找此变量,直到全局作用域,如果都没有,在非严格情况下会自动声明,所以是undefined,在严格条件下则会报错。
var a = 100
function fn() {
var b = 200
console.log(a)
console.log(b)
}
fn()
执行上下文
执行 JavaScript 代码的环境
先看两个例子:
var foo = function () {
console.log('foo1');
}
foo(); // foo1
var foo = function () {
console.log('foo2');
}
foo(); // foo2
function foo() {
console.log('foo1');
}
foo(); // foo2
function foo() {
console.log('foo2');
}
foo(); // foo2
第一个例子中的变量提升,和第二个例子中的函数提升
JS 脚本执行之前,会先解析,解析的时候会先创建一个 全局执行上下文 环境。先把代码中即将执行的(内部函数的不算,因为你不知道函数何时执行)变量、函数声明都拿出来。变量先暂时赋值为undefined,函数则先声明好可使用,然后再开始正式执行程序。
下面执行的结果:
console.log(a) // undefined
var a = 100
fn('zhangsan') // 'zhangsan' 20
function fn(name) {
age = 20
console.log(name, age)
var age
}
console.log(b); // 这里报错
// Uncaught ReferenceError: b is not defined
b = 100;
在解析阶段会将函数与变量提升,且函数的优先级比var声明的变量高:
console.log(foo) // foo(){}
function foo() {}
var foo = 5
// 另一个例子
var foo = 5
function foo() {} // 函数声明在解析阶段已经结束,在执行阶段没有效果
console.log(foo) // 5
执行上下文注意点:
- 执行上下文在运行时确定,随时可能改变
- 创建执行上下文时,会绑定当前this
- 在var声明之前,调用var声明的变量时值为undefined
闭包
闭包可以使内部函数访问到外部函数的作用域,当函数被创建时即生成闭包。
-
当你从函数内部返回一个内部函数时,返回的函数将会保留当前闭包,即当前词法环境
-
闭包的作用域链包含着它自己的作用域,以及包含它父级函数的作用域和全局作用域
function F1() {
var a = 100
return function () {
console.log(a)
}
}
var f1 = F1()
var a = 200
f1()
应用场景:使函数外部可以拿到函数内部的变量
promise
Promise 是异步编程的一种解决方案
使用方式:
const promise = new Promise(function(resolve, reject) {
// 执行异步操作
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// success
}, function(error) {
// failure
});
Promise对象有三种状态:
-
pending: 等待中,或者进行中,表示还没有得到结果
-
resolved(Fulfilled): 已经完成,表示得到了我们想要的结果,可以继续往下执行
-
rejected: 也表示得到结果,但是由于结果并非我们所愿,因此拒绝执行
这三种状态不受外界影响,而且状态只能从pending改变为resolved或者rejected,并且不可逆。
resolve和reject都为一个函数,他们的作用分别是将状态修改为resolved和rejected。
promise中的 then:
Promise对象中的then方法,可以接收构造函数中处理的状态变化,并分别对应执行。then方法有2个参数,第一个函数接收resolved状态的执行,第二个参数接收reject状态的执行。
function fn(num) {
return new Promise(function(resolve, reject) {
if (typeof num == 'number') {
resolve();
} else {
reject();
}
}).then(function() {
console.log('参数是一个number值');
}, function() {
console.log('参数不是一个number值');
})
}
fn('hahha');
fn(1234);
then方法的执行结果也会返回一个Promise对象。因此我们可以进行then的链式执行,这也是解决回调地狱的主要方式。
var fn = function(num) {
return new Promise(function(resolve, reject) {
if (typeof num == 'number') {
resolve(num);
} else {
reject('TypeError');
}
})
}
fn(2).then(function(num) {
console.log('first: ' + num);
return num + 1;
})
.then(function(num) {
console.log('second: ' + num);
return num + 1;
})
.then(function(num) {
console.log('third: ' + num);
return num + 1;
});
// 输出结果
first: 2
second: 3
third: 4
事件循环 eventLoop
我们都知道JS是单线程语言,代码是一行一行执行的。但碰到异步的时候就需要注意代码的执行顺序,会有所不一样。
setTimeout(function(){
console.log('定时器开始啦')
});
new Promise(function(resolve){
console.log('马上执行for循环啦');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('执行then函数啦')
});
console.log('代码执行结束');
前端异步场景:
- 定时 setTimeout setInterval
- process.nextTick(callback)——》node.js版的"setTimeout"
- 网络请求,如 Ajax 加载
我们的程序运行时会有同步任务与异步任务,那程序需要有一个执行机制去处理这些任务,这个机制就是Event Loop(事件循环)。如图:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ccR6ivSN-1605010725388)(media/16045936766013/16046441690548.jpg)]
-
同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
-
当指定的事情完成时,Event Table会将这个函数移入Event Queue。
-
主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
-
上述过程会不断重复,也就是常说的Event Loop(事件循环)。
用代码解释:
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:(response) => {
console.log('发送成功!');
}
})
console.log('代码执行结束');
-
ajax进入Event Table,注册回调函数success
-
执行console.log(‘代码执行结束’)
-
ajax事件完成,回调函数success进入Event Queue
-
主线程从Event Queue读取回调函数success并执行
更细的任务定义:
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval
- micro-task(微任务):Promise,process.nextTick
事件循环的顺序,决定js代码的执行顺序:
进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
分析:
-
这段代码作为宏任务,进入主线程
-
先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue
-
接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue
-
遇到console.log(),立即执行
-
整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行
-
第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行
-
结束
事件循环,宏任务,微任务的关系如图:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jnw3OYaf-1605010725394)(media/16045936766013/16046451954797.jpg)]
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})