javascript面试题
1.js判断类型
1、typeof
所以typeof常用于检测基本类型,对象数组 null返回的都是object。
console.log(typeof 100); // number
console.log(typeof true); // boolean
console.log(typeof 'string'); // string
console.log(typeof []); // object
console.log(typeof function () { }); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object
2、instanceof
谁是谁的实例,不能检测基本类型,所以instancof常用于检测复杂类型,其内部运行机制是判断在构造函数的原型是否出现在实例的原型链上。
A instanceof B 即在A的原型链上是否能找到B.prototype
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function () { } instanceof Function); // true
console.log({} instanceof Object); // true
3、constructor
null、undefined没有construstor方法,但是constructor的指向是可以被改变,所以不安全
console.log((100).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('hello').constructor === String); // true
console.log(({}).constructor === Object); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log((null).constructor); // Cannot read property 'constructor' of null
console.log(undefined.constructor); // Cannot read property 'constructor' of undefined
4、Object.prototype.toString.call:几乎全部类型都可以判断,但是不能判断实例。实例都会返回[object Object]
2.普通函数和箭头函数的区别
1、普通函数
可以通过bind、call、apply改变this指向;可以使用new
2、箭头函数
- a.本身没有this指向,它的this在定义的时候继承自外层第一个普通函数的this,箭头函数外层没有普通函数时,在浏览器中this指向window,node中this指向{};
- b.不能通过bind、call、apply改变this指向,使用new调用箭头函数会报错,因为箭头函数没有constructor;
- c.箭头函数中也没有arguments, 当箭头函数中使用arguments会从上层作用域中查找,在浏览器中全局作用域下是没有arguments,在node中,全局作用域下是有arguments
- 总之使用箭头函数的时候需要考虑一下this的指向问题,有些地方是不能使用箭头函数,比如vue的mthods和生命周期,只能使用普通函数,这样this才指向当前实例。
3.栈和堆的区别
1、栈
自动分配相对固定大小的内存空间,并由系统自动释放
2、堆
动态分配内存,内存大小不一,也不会自动释放
3、基本类型都是存储在栈中,每种类型的数据占用的空间的大小是确定的,并由系统自动分配和释放。内存可以及时回收。
4、引用类型的数据都是存储在堆中。准确说是栈中会存储这些数据的地址指针,并指向堆中的具体数据。
4.new操作符到底做了什么(重点)
var fn = new Fun()
1.var obj = {} // 创建一个全新的对象
2.继承构造函数的原型:obj.__proto__ = Fun.prototype
3.改变this指向:Fun.call(obj)
4.如果构造函数没有返回其他对象,那么将返回这个新创建的对象( 需要排除 null
)
实现一个new
function _new(fn, ...arg) {
const obj = Object.create(fn.prototype);
const ret = fn.apply(obj, arg);
// 根据规范,返回 null 和 undefined 不处理,依然返回obj,不能使用
// typeof result === 'object' ? result : obj
return ret instanceof Object ? ret : obj;
}
终极版本的new :
function myNew(func, ...args) {
// 1. 判断方法体
if (typeof func !== 'function') {
throw '第一个参数必须是方法体';
}
// 2. 创建新对象
const obj = {};
// 3. 这个对象的 __proto__ 指向 func 这个类的原型对象
obj.__proto__ = Object.create(func.prototype);
// 为了兼容 IE 可以让步骤 2 和 步骤 3 合并
// const obj = Object.create(func.prototype);
// 4. 通过 apply 绑定 this 执行并且获取运行后的结果
let result = func.apply(obj, args);
// 5. 如果构造函数返回的结果是引用数据类型,则返回运行后的结果
// 否则返回新创建的 obj
const isObject = typeof result === 'object' && result !== null;
const isFunction = typeof result === 'function';
return isObject || isFunction ? result : obj;
}
5.for..in 和 object.keys的区别
for in 会遍历对象自身的可枚举属性和继承的可枚举属性。这意味着如果对象的原型链上有属性,它们也会被遍历。
Object.keys只会遍历对象自身的可枚举属性
15.forEach和map的区别
forEach没有返回值,map方法会创建一个新的数组并返回。
forEach
方法是无法中断的,而map
方法可以使用return
语句中断遍历。
map
方法可以链式调用其他数组方法,比如filter
、reduce
等。而forEach
方法不能链式调用其他数组方法。
16.forEach和for的区别
for循环中可以使用return、break等来中断循环
forEach对数组的每一个元素执行一次提供的函数(不能使用return、break等中断循环),不改变原数组,无返回值undefined。
遍历速度:for更快;forEach每次都要创建一个函数来调用,for不需要。
for in 和for of的区别
区别1:for in 遍历得到的是key;for of 遍历得到的是value;
区别2:
for in用于可枚举数据,如数组,对象、字符串即enumerable为true;
for of用于可迭代的数据(是否有Symbol.iterator属性值 有next方法),如数组、 Map、Set、字符串;
区别3: for of不能遍历对象; for in不能遍历Map、Set和generator
迭代器和可迭代对象
迭代器是一个对象内部必须实现next方法并且next方法返回{value:’’, done: false/true}
可迭代对象,内部必须实现[Symbol.iterator]方法 返回迭代器
可迭代对象:
应用场景:new Set(iterable) Array.from(iterable) for of 遍历的是可迭代对象
6.数组去重
1.es6的set去重
const numbers = [2,3,4,6,6,7,5,32,3,4,5]
console.log([...new Set(numbers)])
2.indexOf/includes
var arr = [1, 1, 8, 8, 12, 12, 15, 15, 16, 16];
function unique(arr) {
var array = [];
for (var i = 0; i < arr.length; i++) {
if (!array.includes(arr[i])) { array.push(arr[i]) }
}
return array
}
console.log(unique(arr))
3.filter 条件为:源数组.indexOf(当前元素) === 当前元素的索引
var arr = [1, 1, 8, 8, 12, 12, 15, 15, 16, 16];
function unlink(arr) {
return arr.filter(function (item, index, arr) {
//利用indexOf返回的是指定的元素在数组中首次出现的位置
return arr.indexOf(item) === index;
});
}
console.log(unlink(arr));
4.reduce配合includes
var arr = [1, 1, 'true', 'true', true, true, 15, 15];
function unique(arr) {
return arr.reduce((prev, cur) => prev.includes(cur) ? prev : [...prev, cur], []);
}
console.log(unique(arr));
有一次面试碰到的有意思的提问是:不使用数组 API
进行去重。其实就是在变相考你对数组的api原理的理解。
以下简单的实现了includes和push方法。数组的api具体实现可以查看https://mp.csdn.net/mp_blog/creation/editor/122448550
// includes的伪代码
Array.prototype.myIncludes = function (param) {
for (let i = 0; i < this.length; i++) {
if (this[i] === param) return true;
}
return false
}
// push的伪代码
Array.prototype.myPush = function () {
for (var i = 0; i < arguments.length; i++) {
//arguments代表实参的集合
//this 指向的是arr数组
this[this.length] = arguments[i];
}
//由于push 返回值 是一个数组的长度 所以 我们来个return;
return this.length;
};
var arr = [1, 1, 8, 8, 12, 12, 15, 15, 16, 16];
function unique(arr) {
var array = [];
for (var i = 0; i < arr.length; i++) {
if (!array.myIncludes(arr[i])) { array.myPush(arr[i]) }
}
return array
}
console.log(unique(arr))
7.数组的常用方法
改变原数组的方法: push pop unshift(数组开头添加) shift(数组开头删除) sort reverse splice
splice向数组中添加或删除元素(改变原数组),然后返回被删除的元素
不改变原数组的方法:
slice:截取数组中某一段元素 、concat 、join
所以数组的浅拷贝的方法有concat、slice还有扩展运算符 但是它们只是对第一层进行深拷贝,如果有第二层它们不会深拷贝
8.es6常用的语法
对象字面量的增强、let const定义变量、解构赋值、模版字符串、扩展运算符、箭头函数、promise
9.this的指向
请查看:this
10.深度拷贝
JSON.stringify 会丢失函数 undefined 时间 正则
Lodash的cloneDeep方法
for in 要想只迭代自身的属性 需要搭配hasOwnProperty
自己实现深拷贝:判断类型的过程
function deepClone(obj) {
// undefined、null
if (obj == null) return obj;
// 简单类型 string、number、boolean、symbol
if (typeof obj !== 'object') return obj;
// 日期 正则
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Date) return new Date(obj);
// 数组或者对象
const cloneObj = new obj.constructor;
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key]);
}
}
return cloneObj;
}
终极版本的(带循环引用、Map、Set的解决方案):
前期知识铺垫:
首先,Map的“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
其次,WeakMap
只接受对象作为键名(null
除外),不接受其他类型的值作为键名。它的键名所引用的对象都是弱引用,不计入垃圾回收机制。
function deepClone(obj, hash = new WeakMap()) {
// 基本类型 和 null 直接返回
if (typeof obj !== "object" || obj == null) return obj;
// 日期 正则
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// Map Set Array Object
if (hash.has(obj)) return hash.get(obj);
let traget = {};
hash.set(obj, traget);
// Map key可以为任意值 可迭代对象 可以用for of 遍历 不能用for in遍历
if (obj instanceof Map) {
traget = new Map();
for (let [k, v] of obj) {
let k1 = deepClone(k, hash);
let v1 = deepClone(v, hash);
traget.set(k1, v1);
}
}
if (obj instanceof Set) {
traget = new Set();
for (let v of obj) {
traget.add(deepClone(v));
}
}
if (obj instanceof Array) {
traget = obj.map((item) => deepClone(item, hash));
}
// 对象
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
traget[key] = deepClone(obj[key], hash);
}
}
return traget;
}
11.instanceof的原理
内部机制是通过原型链实现的,(沿着__proto__这条链查找 如果能找到函数的原型就返回true 否则返回false)
function _instanceof(left, right) {
let rightVal = right.prototype
let leftVal = left.__proto__
// 若找不到就到一直循环到父类型或祖类型
while (true) {
if (leftVal === null) {//Object.prototype.__proto__=null //通过__proto__向上进行查找,最终到null结束
return false
}
if (leftVal === rightVal) {
return true
}
leftVal = leftVal.__proto__ // 获取祖类型的__proto__
}
}
function Person() { }
let p1 = new Person
console.log(_instanceof(p1, Object))
12.Let const var的区别
- 块级作用域: 块作用域由
{ }
包括,let和const具有块级作用域,var不存在块级作用域。其中for、switch、if的{}都是块级作用域
// 块级作用域对let/const有效 对var无效
{
let age = 18;
var address = 'shanghai'
}
console.log(address) //shanghai
console.log(age) //age is not defined
- 变量提升(作用域提升): var存在变量提升,let和const不存在变量提升,即变量只能在声明之后使用,否则会报错。
注意📒:如果在let声明的变量之前访问会报错,这不意味着let、const的声明变量的只有在代码执行的阶段才会创建,let声明的变量会在声明语句执行之前创建,但在此之前不可访问,直到被赋值之后才能被正常访问;这种行为是为了在代码中更好地捕捉潜在的错误,并强制开发者在变量声明之后才使用变量,避免了变量提升带来的混乱和不可预测性。虽然被创建出来,但是不能被访问,所以不能称之为作用域提升。
// let/const 没有作用域提升
console.log(name) // name被创建出来了,但是不能访问
let name = “yql”
- 给全局添加属性: 浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。
那let、const定义的这些变量会保存在哪里呢?JS引擎在解析的时候,其实会有自己的实现, 比如v8中其实是通过VariableMap的一个hashmap来实现它们的存储的,这里面不仅存储let、 const定义的变量还有var定义的变量。同时var定义的变量,浏览器也会将他们绑定在window 上。
- 重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。
- 暂时性死区: 在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。
- 初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。
13.防抖节流
14数组扁平化
1.flat
let arr = [1, 2, [3, 4, [5, 6, [7, 8]]]]
console.log(arr.flat(Infinity))
2.toString split
let arr = [1, 2, [3, 4, [5, 6, [7, 8]]]]
console.log(arr.toString()) // '1,2,3,4,5,6,7,8'
//split方法用于把一个字符串分割成字符串数组。 所以经过split之后每一项都是字符串
arr.toString().split(',').map(item => +item)
注意:如果数组里有对象就不适用;
3.while递归
concat 参数为数组或值
let arr = [1, 2, [3, 4, [5, 6, [7, 8]]]]
//遍历
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr)
}
console.log(arr)
4.reduce
let arr = [1, 2, [3, 4, [5, 6, [7, 8]]]]
function flat(arr) {
return arr.reduce((result, cur) => {
return result.concat(Array.isArray(cur) ? flat(cur) : cur)
}, [])
}
console.log(flat(arr));
5.递归
let arr = [1, 2, [3, 4, [5, 6, [7, 8]]]]
function flat(arr) {
let result = [];
for (let item of arr) {
if (Array.isArray(item)) {
result = result.concat(flat(item))
} else {
result.push(item)
}
}
return result;
}
17.什么是闭包?为什么要使用闭包?怎么实现一个闭包
维基百科里:闭包跟函数的最大区别在于,当捕捉闭包的时候,它的自由变量会在捕捉的时候被确定,这样即使脱离了捕捉的上下文,它也能照常运行。
MDN上对闭包的定义:一个函数与其周围状态的引用捆绑在一起,这样的组合就是闭包。也就是说,闭包可以让你访问上层作用域的变量。
上面概念比较难理解,可以看下面的:
一个函数,如果他可以访问上层作用域的自由变量,那么这个函数就是一个闭包;
从广义上来说(可以访问的角度),所有的函数都是闭包;
从狭义的的角度来说(访问了的角度),一个函数,如果访问了上层作用域里的自由变量,即使在其所在函数执行结束后仍然可以使用这个自由变量,那么他就是一个闭包;
好处:这些变量的值始终保持在内存中,不会在外层函数调用后被自动清除 所以可以重复赋值和使用。
坏处:可能会造成内存泄露;闭包中可能会持有对上层作用域中变量的引用,如果闭包不再使用但仍然存在,这些上层作用域中的变量也无法被回收。确保在不再需要使用闭包时,解除对外部变量的引用,例如将闭包设置为 null。
举例:比如防抖和节流都有用到闭包
18setTimeout和setInterval的区别
setTimeout:到达指定的时间之后,才会调回调函数,只会执行一次
setInterval:每次间隔指定的时间之后,都会调回调函数
尽量不要使用setInterval:
1.它会无视代码错误
如果回调里面有错误,它还是会不管不顾的到点都执行
2.无视网络延迟
假如你想每隔一段时间调ajax,看看有没有新的数据,因为受网速等影响,响应特别慢的时候,它还是会定时请求,最后会造成ajax阻塞
3.不保证执行
如果你的回调函数逻辑比较复杂,但是你的下次执行时间又到了,这时候不保证回调中的逻辑都会被执行到
轮询用setTimout实现
function getData() {
return new Promise((resolve,reject) => {
setTimeout(() => {
resolve({data:666})
},500)
})
}
// 轮询
async function start () {
const { data } = await getData() // 模拟请求
console.log(data)
timerId = setTimeout(start, 1000)
}
start ()
19.CommonJS 和 ES6 Modules的区别
CommonJS规范
- 导出:
module.exports = {}
、exports.xxx = 'xxx'
- 导入:
require('XXX')
module.exports才是真正的导出者!!!
- 如果main.js导出是用的exports.xx=xx没有用到module.exports,那么有下面的关系:
module.exports = exports = require(‘main.js’) 三者指向同一个内存地址
2.如果main.js中导出用的是module.exports = {} ,那么:
module.exports = require(‘main.js’) 两者指向同一个内存地址(不带exports玩了)
3.如果main.js中导出用的是module.exports = {} 和exports.xx=xx,参照module.exports={}
ES6 Modules规范
1.导出:export const name = ‘’yql
导入: import {name} from ‘xx.js’
2.导出:export { name,age}
导入:import {name, age} from ‘xx.js’
导入:import * as obj from ‘xx.js’
3.导出:export default function () {} 一个模块里面export default只能用一次
导入:import foo from ‘xx.js’
4.export和import结合使用
export {name, age} from ‘xx.js’
export * from ‘xx.js’
ES6 Modules
和 CommonJS
区别:
CommonJS
模块是运行时加载,ES6 Modules
是编译时输出接口- CommonJs是同步加载模块的(再没加载完,后面的代码会阻塞),Es module是异步加载模块
CommonJS
可以动态加载js,ES6 Modules
默认只能放在顶层作用域中引入CommonJS
输出是值的拷贝;ES6 Modules
输出的是值的引用,被输出模块的内部的改变会影响引用的改变CommonJs
导入的模块路径可以是一个表达式,因为它使用的是require()
方法;而ES6 Modules
只能是字符串CommonJS this
指向当前模块,ES6 Modules
的this
指向undefined
ES6 Modules
中没有这些顶层变量:arguments
、require
、module
、exports
、__filename
、__dirname
链表和数组的区别
1.从内存上来说:数组占用的是一块连续的内存区,而链表在内存中,是分散的,需要通过指针next连接起来。
2.对于插入、删除,增加元素,数组中后面的元素位置都得往后移一格,而链表只需要改变下指针方向即可
3.对于访问,数组只需要用下标进行访问,而链表访问只能通过头指针依次向下查找。
类数组和数组
一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果
类数组不具有数组所具有的方法,所以也不能使用数组的方法。
类数组,只是一个普通的对象,他的原型是Object;而真实的数组是Array类型。
<script>
function aaa(name,age) {
console.log(arguments)
}
aaa('yql',19)
let arr = [1,2,3]
console.log(arr)
</script>
常见的类数组转换为数组的方法有这样几种:
1.通过 Array.from 方法来实现转换:
Array.from(arrayLike);
2.通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
[].slice.call(arrayLike);
3.通过展开运算符
var arr = [...arrayLike]
类型转换
显示类型转换:
1.Number 转换不了就返回NaN
var a = undefined;
var b = null;
var c = '123c'
console.log(Number(a)) // NaN
console.log(Number(b)) //0
console.log(Number(c)) // NaN
2.String
3.toString null和undefined不能用
var a = undefined;
console.log(a.toString()) // Cannot read properties of undefined (reading 'toString')
4.Boolean 除了假值0, false ‘’ undefined null NaN 会返回false,其他都返回true
5.ParseInt 转换不了就返回NaN
parseInt(string, radix) 解析一个字符串并返回指定基数的十进制整数,radix
是 2-36 之间的整数,表示被解析字符串的基数。
var a = undefined;
var b = true;
var c = '123abc';
console.log(parseInt(a)) //NaN
console.log(parseInt(b)) //NaN
console.log(parseInt(c)) //123
隐式类型转换
1.isNaN 内部用了Number
2.++/— +/-(正负) 内部用了Number
3.+(加号)两侧有一个是string类型的就将另一个也变成string类型的然后做拼接
Undefined既不大于0也不小于0也不等于0
Null既不大于0也不小于0也不等于0
Undefined == null (在两个等号的时候)
NaN谁都不等于包括NaN
求下面代码的执行结果
let a = { n: 1}
let b = a;
a.x = a = {n: 2}
console.log(a.x)
console.log(b.x)
tip:连续赋值从后向前执行 .的优先级大于=
let n1, n2
n1 = n2 = 100;
// 相当于
// n2 = 100;
// n1 = n2;
let a = {n: 1}
a.x = a = {n:2}
// 拆解成
// a.x = undefined;
// let x = a.x x变量是假象的,实际执行时不会有
// x = a = {n:2}
即a与x没有关系了
求代码执行结果2
js对象的key的数据类型,只能是字符串和Symbol类型,其他类型会被转换成字符串(使用toString()方法)
let a = {}, b = "123", c = 123;
a[b] = "b";
a[c] = "c";
console.log(a[b]); // c
let a = {}, b = Symbol("123"), c = Symbol("123");
a[b] = 'b'
a[c] = 'c'
console.log(a[b]) // b
let a = {} , b = {key:'123'}, c= {key: '456'}
a[b] = 'b'
a[c] = 'c'
console.log(a[b]) // c
扩展:Map的key可以是任意类型,weakMap的key只能是引用类型(null也不行)不能是值类型(如果给它值类型会主动报错)
预编译
js在执行的前一刻会进行预编译:
预编译也分为2个时间点:第一个是在JavaScript代码执行之前;第二个是在函数执行之前
JavaScript代码执行之前:首先会创建一个全局对象,即GO(Global Object)对象,然后将所有声明的全局变量、未使用var的变量放到GO对象中,并且赋值为undefined
函数执行前的预编译:
1.函数执行前的一瞬间,生成AO活动对象{},并将其添加到作用域链的顶端。
2.找形参和变量申明,将变量和形参名作为AO属性名,值为undefined
3.将实参和形参相统一
4.在函数体内找函数声明,作为AO的属性名,值赋予函数体
function fn(a) {
console.log(a); //function a(){}
var a = 123;
console.log(a); // 123
function a() {};
console.log(a); // 123
var b = function () {};
console.log(b); // function () {}
function d() {};
}
var name = 'yql';
GO: {
setTimeout,
window: this;
name: undefined --> yql
}
AO {
a: undefined --> function a(){} --->123
b: undefined ---> function () { }
d: function d() {}
}
fn(1)
预编译结束之后就开始一行一行执行代码:
v8为了执行代码,引擎内部会有一个执行上下文栈(函数调用栈)
为了让全局代码能够正常执行,需要创建全局执行上下文(全局代码需要执行时才会创建),执行上下文里面包含VO、[[scope]]、this、执行的代码
函数要想执行,会创建一个函数的执行上下文,它定义了一个函数执行时的环境。放到函数调用栈中进行执行。记住函数的每一次执行都是重新创建执行上下文。当函数执行完会从函数调用栈中移除。
作用域
函数在定义时就确定了其作用域(包括父级作用域),而不是在执行时,跟它的调用位置无关。分为全局作用域和局部作用域以及块级作用域。作用域定义了变量的可访问范围,它决定了哪些代码可以访问变量,哪些代码不能访问变量。通过使用作用域,可以控制变量的可见性和生命周期,避免命名冲突和数据泄漏等问题。比如,如果你在一个函数内部声明了一个变量,那么这个变量就只能在这个函数内部被访问,外部的代码无法访问到它。这个函数就创建了一个局部作用域,其中的变量只在这个函数内有效。另一方面,如果你在全局范围内声明了一个变量,那么这个变量就可以在整个代码中被访问,包括函数内部。这个全局范围就是全局作用域,其中的变量在整个代码中都是可见的。
下面的代码中foo执行完打印出的是“Hello Global”,而不是“Hello Bar”
特别注意:当函数的参数有默认值的时候,会形成一个参数作用域;
// 当函数的参数有默认值时,会形成自己的作用域
var x = 0;
function foo(
x,
y = function () {
x = 3; // 这里改的是参数作用域里的x
console.log(x);
}
) {
console.log(x); // 访问的是函数作用域里的值 实参和形参相统一 所以为4
debugger;
var x = 2;
y();
console.log(x);
}
foo(4);
console.log(x);
作用域链
特别注意的是,每个函数中都有一个 [[scopes]] 属性,表示该函数的作用域链。
请谨记:函数的作用域链是在创建时就确定了,JS 引擎会创建函数时,在该对象上添加一个名叫作用域链的属性,该属性包含着当前函数的作用域以及父作用域,一直到全局作用域。
函数在执行时,JS 引擎会创建执行上下文,该执行上下文会包含函数的作用域链,其次包含函数内部定义的变量、参数等。在执行时,会首先查找当前作用域下的变量,如果找不到,就会沿着作用域链中查找,一直到全局作用域。
function a() {
function b() {
var b = 234;
}
var a =123;
b();
console.log(a)
}
var glob =100;
a();
下面是看成哥的劳动成果,觉得有利于大家理解作用域链:
原型链
所有对象都有一个隐式原型__proto__,其中函数还有一个显示原型prototype
原型链:每一个对象都有一个原型,原型也是对象,所以它也拥有自己的原型,所有的这些原型会形成一个原型链(prototype chain),当查找对象的属性时,先在当前对象上进行查找,如果没找到,就去它的原型对象(__proto__)上查找,找到就返回值,否则一直查找到顶层的原型(Object.prototype,Object.prototype.__proto__ = null),还是没找到的话就返回undefined
A.__proto__的值是B.prototype代表A是由B创建出来的, 可以推出所有函数的__proto__都是Function.prototype
下图便于理解原型链:
作用域链和原型链的区别:
区别:1. 作用域链是对于变量而言,原型链是对于对象的属性。
2. 作用域链顶层是window,原型链顶层是Object。
call、apply、bind的源码实现
请参考:javaScript深入之call、apply、bind的实现_一鹿有你~的博客-CSDN博客
promise的源码实现
参考资料
https://juejin.cn/post/6890716797436166152