JS核心概念学习指导
# 执行上下文
简而言之,其实就是评估和执行Javascript代码环境的抽象概念
上下文是一个英文语境context,其实建议大家换成中文语境,就是环境的意思。
执行任意一句代码,都需要一个执行时的环境。
**执行上下文简单理解:**就是一个隐形的对象,这个对象上记录了程序当前执行所依赖的环境因素
# 执行上下文的分类
- 全局执行上下文
- 函数执行上下文
- eval执行上下文
- 模块执行上下文
javascruot运行时首先会进入全局环境,对应就会生成全局上下文。
代码中都会存在函数,那么调用函数,就会进入函数执行环境。
代码中函数有多个,对应的函数执行上下文,就会存在多个,主要讨论的是这个函数执行上下文,我们都通过栈来管理执行上下文,一般称为执行栈,或者函数调用栈
# 栈的数据结构

# 执行栈
全局执行上下文就是最大的代码片段。包括了程序中所有的代码,其中包括函数执行上下文分割的每一个小片的代码片段。函数的代码片段上都有执行上下文
而我们的执行栈,其实就是存放执行上下文的地方。
浏览器按照栈的执行顺序,依次执行。
function foo(){
function bar(){
return "I am bar";
}
return bar();
}
foo();

function fn3(){
return "hello world"
}
function fn2(){
fn3()
}
function fn1(){
fn2();
}
fn1();
伪代码:
// 创建执行栈
const ECStack = [];
ECStack.push(全局执行上下文)
ECStack.push(fn1执行上下文)
ECStack.push(fn2执行上下文)
// 执行fn3之后,没有其他内容了,开始出栈
ECStack.push(fn3执行上下文)
// fn3出栈
ECStack.pop();
// fn2出栈
ECStack.pop();
// fn1出栈
ECStack.pop();
面试题:
写法一:
function foo(){
function bar(){
return "I am bar";
}
return bar();
}
foo();
写法二:
function foo(){
function bar(){
return "I am bar";
}
return bar;
}
foo()();
写法一:
ECStack.push(foo的上下文)
ECStack.push(bar的上下文)
ECStack.pop() // bar出栈
ECStack.pop() // foo出栈
写法二:
ECStack.push(foo的上下文)
ECStack.pop() // foo出栈
ECStack.push(bar的上下文)
ECStack.pop() // bar出栈
# VO(variable object)
为了好理解,你可以直接把vo理解为全局上下文环境
VO用于存储当前执行环境所拥有的变量以及函数
其实最简单的理解,在浏览器环境,全局上下文,你就可以理解为,就是window
var a = 12;
console.log(this.a, window.a);
function b(){}
this.b();
this === window
# AO(activation object)
函数上下环境比较特殊:
AO = VO + arguments + params
分为分析(预编译)和执行的两个阶段:
1、如果当前上下文是函数上下文,首先分析函数所有的形参
- 将形参名称与对应的值绑定到AO上,并将值挂到对应为止的arguments上
- 如果没有对应实参的形参,值为undefined
2、函数声明
- 如果遇到函数声明语句,将函数的名称与该函数的引用挂到当前上下文AO上
- 如果AO身上已经存在该函数名称相同的表示符号,则覆盖
3、变量声明
- 如果遇到了
var声明的变量,将变量名与undefined挂载到当前上下文上 - 如果AO上已经存在于该变量名相同的标识符,则忽略
面试题
function foo(a){
var b = 2;
function c(){}
var d = function(){}
b = 3;
}
foo(1);
执行完分析阶段之后,函数foo执行上下文身上AO是什么情况:
AO = {
arguments:{
0:1,
length:1
}
a:1
b:undefined
c:function c(){}
d:undefined
}
最终当foo函数开始执行的时候,将上面AO对象的初始状态进行处理,然后根据代码的状况发生对象状态的变化
AO = {
arguments:{
0:1,
length:1
}
a:1
b:3
c:function c(){}
d:function d(){}
}
# 面试题1:
function A(a,b){
/*
AO = {
arguments={
0:1
1:2
length:2
}
a:1
b:function b(){}
}
*/
console.log(a,b)
var b = 123;
console.log(a,b);
function b(){
var d = 123;
}
}
A(1,2)
AO = {
arguments={
0:1
1:2
length:2
}
a:1
b:function b(){}
}
# 面试题2
var g = 123;
var a = 2;
function A(a,b){
console.log(a,b,g);
var b = 123;
function b(){}
var a = function(){}
console.log(a,b);
}
var g = 456;
A(1,2);
# 面试题3:
var foo = 1;
function bar(){
/*
AO = {
argument:{}
foo:undefined
}
*/
console.log(foo);
if(!foo){
var foo = 10;
}
console.log(foo);
}
bar();
# 面试题4:
var a = 1;
function b(){
console.log(a);
a = 10;
return;
function a(){}
}
b();
console.log(a);
# 面试题5:
console.log(foo); //function c
var foo = "A";
console.log(foo); // A
var foo = function(){
console.log("B");
}
console.log(foo); // function B
foo(); // B
function foo(){
console.log("C")
}
console.log(foo); // function B
foo(); // B
# 面试题6:
var foo = 1;
function bar(a){
var a1 = a;
var a = foo;
function a(){
console.log(a);
}
a1();
}
bar(3);
# 作用域
全局作用域
函数作用域
块级作用域(ES6 let const)
- 声明变量不会提升
- 禁止重复声明
# 作用域链
var a = 100;
function fn(){
var b = 200;
console.log(a);
console.log(b);
}
fn();
# 静态作用域(词法作用域)
如果要去找到某个变量,要到创建这个函数的那个域去取值,注意这里强调的是创建,而不是调用
这个其实就是所谓的词法作用域
var x = 10;
function fn() {
console.log(x);
}
function show(f) {
var x = 20;
f();
}
show(fn);
# 作用域链的构建过程
可以通过console.dir看到函数中有[[Scopes]]
function foo(){
function bar(){
......
}
}
当函数创建时,各自的[[Scopes]]为:
// 伪代码
foo.[[Scopes]]= [
globalContext.VO
]
bar.[[Scopes]]= [
fooContext.AO
globalContext.VO
]
// 示例函数
var scope = "global scope";
function checkScope(){
var scope2 = "local scope";
return scope;
}
checkScope();
- 当遇到
function checkScope()声明语句的时候,保存当前作用域链到其内部的属性[[Scopes]]上
checkScope.[[Scopes]] = [
globalContext.VO
]
- 当遇到
checkScope()执行的时候,创建该函数的执行上下文,并且激活AO
ECStack = [
checkScopeContext
globalContext
]
- 分析AO对象内容,并进入准备工作
checkScopeContext = {
Scope:checkScope.[[Scopes]]
}
- 创建AO对象中的内容
checkScopeContext = {
AO:{
arguments:{
length:0
},
scope2:undefined
}
Scope:checkScope.[[Scopes]]
}
- 将AO压入到函数作用域链的顶端
checkScopeContext = {
AO:{
arguments:{
length:0
},
scope2:undefined
}
Scope:[AO, checkScope.[[Scopes]]]
}
- 分析工作结束,开始执行函数,AO给值
checkScopeContext = {
AO:{
arguments:{
length:0
},
scope2:'local scope'
}
Scope:[AO, checkScope.[[Scopes]]]
}
- checkScope执行完成,销毁函数
ECStack = [
globalContext
]
# 闭包
一般情况下,当函数内部定义了另外一个函数,内部函数使用了外部函数的变量,并且内部函数直接返回的情况下,就凸显了闭包的作用,因为延长了变量的生命周期。在这种情况下,内部函数可以访问外部函数的变量,因为他们共享了同一个作用域链
闭包:词法作用域 + 作用域链 + 执行上下文所产生的必然的结果
function fn1(){
let a = 1;
function fn2() {
let b = 2;
console.log(a, b);
}
return fn2;
}
let f = fn1();
f(); // 1 2
// fn2.[[scope]] = fn1.Scope //----> [fn1.AO, globalContext.VO]
// fEC = {
// AO: {
// arguments: { ...},
// b:2
// }
// Scope:[f.AO, fn1.AO, globalContext.VO]
// }
for(var i = 0; i < btns.length; i++) {
btns[i].onclick = (function a(i){
return function b() {
alert(i);
}
})(i)
}
btns[0]()
btns[1]()
btns[2]()
// 比如执行到btns[1],对于父函数a
a.[[scope]] = [globalEC.VO]
aEC = {
AO:{
arguments:{'0':1,length:1}
i:1
}
Scope:[aEC.AO, globalContext.VO]
}
b.[[scope]] = aEC.Scope = [aEC.AO,globalContext.VO]
bEC = {
AO:{
arguments:{}
},
Scope:[bEC.AO, aEC.AO,globalContext.VO]
}
# 函数柯里化
function outerFunction(x) {
return function innerFunction(y) {
return x + y;
}
}
// 比如这个代码可以一开始先执行
// 对于外部,可能看到的就只有closure函数
const closure = outerFunction(5);
const num = closure(8);
console.log(num);
// 验证邮箱
function validateInput(validateFn,successFn,errorFn) {
return function (input) {
if (validateFn(input)) {
successFn(input);
}
else {
errorFn(input);
}
}
}
function isEmail(input) {
return input.includes('@');
}
function handleSuccess(input) {
console.log(`输入邮箱:${input},格式正确`);
}
function handleError(input) {
console.log(`输入邮箱:${input},格式错误`);
}
const validate = validateInput(isEmail, handleSuccess, handleError)
validate("xxx163.com")
# 惰性求值
// 模拟耗时操作
function expensiveOperation(n) {
console.log('进行了耗时的操作1!');
console.log('进行了耗时的操作2!');
console.log('进行了耗时的操作3!');
return n * 10
}
function isBigNumber(n) {
return n >= 100;
}
function lazyEvaluation(condition, expensiveOperation) {
let result;
return function (n) {
// 条件满足,进行运算
if (condition(n)) {
// result没有值就进行耗时的运算,如果有值,直接返回第一次运行的值
if (!result) {
result = expensiveOperation(n);
}
return result;
}
console.log(n + "----不满足条件")
return undefined
}
}
const lazyFn = lazyEvaluation(isBigNumber, expensiveOperation)
console.log(lazyFn(50))
console.log(lazyFn(100))
console.log(lazyFn(200))
console.log(lazyFn(300))
# this
# 全局this的指向
如果是严格模式下,全局this指向的就是undefined
浏览器全局this指向window
nodejs环境下,全局this指向空对象{}
# 函数内部的this指向
既然是函数内部的this指向,肯定是函数调用的时候才有this指向
1、函数如何被调用的
2、this指向和执行上下文相关
- new 调用 ----》 this指向新对象
- 直接调用 ----》 全局对象
- 对象调用 ------》 调用的对象
- call,apply,bind -----》 第一个参数
# 箭头函数:
箭头函数没有this,如果说在箭头函数中使用this,这个this其实就是闭包的调用
# 什么是类,什么是对象
绕口令:
具有相同特性(属性,数据元素)和行为(功能)的对象的抽象,就是类。
对象的抽象是类,类的实例化就是对象
类:其实就是一种类型,js有各种各样的基础类型,但是基础类型描述不了复杂的事务,所以,我们需要创建自己的复杂类型,用来描述具体的业务需要。复杂的类型,其实总是由简单的类型所组成的。
类型并不能直接使用,它只是一种对相同属性和行为的描述。要使用,就需要通过类来实例化对象。
const obj1 = {}
本质上和下面的代码没有区别
const obj2 = new Object();
obj1.name = "aaa";
obj1.show = function(){}
我们使用js,总是习惯用动态的操作给对象添加属性和功能
但是这种做法在典型的面向对象的语言中,是错误的,必须要现有类的概念,然后通过类实例化模板对象
然后才能通过对象进行操作,不能动态的给对象添加属性和功能
# 构造函数
在JavaScript中,面向对象的基础,其实并不是基于”类“的,而是基于构造函数(constructor)和原型链(prototype)的
为什么JavaScript有原型和原型链?
在典型的面向对象语言中,面向对象的三大特性:封装,继承和多态,js中的原型和原型链就是为了实现面向对象的继承关系。有了继承关系,才能够让js 的对象,比较方便的向上追溯源头。
function Animal(){
this.name = "Animal;
this.showName = function(){
console.log(this.name)
}
}
const a = new Animal();
一般有一个不成文的规定,那就是构造函数的函数名首字面大写,如果是普通函数,首字面小写
# 普通函数的二义性
为什么ES6要添加箭头函数和class语法糖?
目的就是为了区分普通函数和类
因为之前的构造函数,并不能很好的区分普通函数和构造函数
# new
const a1 = new Animal();
const a2 = new Animal;
console.log(a1);
console.log(a2);
new命令本身就可以执行构造函数,所以,如果构造函数没有参数的话,可以不用带括号
const a3 = Animal();
console.log(a3); // undefined
这样子写的话,构造函数就变成了普通函数,并不会生成实例对象,this这个时候代表全局对象,
如果要防止这种情况可以有下面的解决方案:
function Animal() {
"use strict"
this.name = "Animal"
this.showName = function(){
console.log(this.name)
}
}
另外呢,如果使用ESM模块化的话,代码自动会变成use strict
也可以使用判断:
function Animal() {
if (!(this instanceof Animal)) {
return new Animal();
}
this.name = "Animal"
this.showName = function(){
console.log(this.name)
}
}
还可以使用new命令的属性
function Animal() {
if(!new.target){
throw new Error("必须使用 new 命令生成实例");
}
this.name = "Animal"
this.showName = function(){
console.log(this.name)
}
}
# new命令的原理
使用new命令的步骤:
1、创建一个空对象,作为将要返回的对象实例
2、将这个空对象的原型,指向构造函数的prototype属性
3、将这个空对象赋值给函数内部的this关键字
4、开始执行构造函数内部的代码
所以,构造函数内部,this指的是一个新生成的空对象,所有针对this 的操作,都会发生在这个空对象上。
构造函数之所以称之为构造函数,其实就只在构造操作一个空对象(this对象),将其构造成你所需要的样子
function Animal() {
this.name = "Animal"
this.showName = function(){
console.log(this) // Animal
console.log(this.__proto__ === Animal.prototype) // true
}
}
const a = new Animal();
console.log(a); // Animal
a.showName();
如果构造函数有返回值,分为两种情况
返回基本数据类型,没有影响
返回对象,this会指向返回的对象类型
function Animal() {
this.name = "Animal"
this.showName = function(){
console.log(this)
console.log(this.__proto__ === Animal.prototype)
}
return {id:1,name:"jack"}
}
const a = new Animal();
console.log(a); // 这里的a会指向返回的对象
a.showName(); // error
function show() {
function test() {
console.log("hello")
}
}
const s = new show();
console.log(s); // {}
console.log(typeof s); // object
s.test(); //error
除了通过构造函数创建对象,也可以直接通过对象创建对象
# Object.create()
var p1 = {
name: "jack",
age: 18,
showName: function () {
console.log(this.name)
}
}
var p2 = Object.create(p1);
console.log(p2.name);
console.log(p2.age);
create这个函数,其实是通过prototype的赋值,给另外一个对象
Object.create = function(o){
function F(){};
F.prototype = o;
return new F();
}
# 原型
为什么有原型和隐式原型:JS没有记录类型的元数据,因此,需要一个内容去记录它。
所以才有了原型和隐式原型,目的就是为了确定其类型
# prototype
在javascript中,所有的函数都有prototype的属性,这个prototype其实就是一个空对象
我们一般把这个对象就称为函数的原型,
# __proto__
__proto__是每个对象都有的一个隐式原型
# 原型链
由于原型prototype本身是对象,因此,它也有隐式原型,指向的规则不变,这样一来,从某个对象触发,依次往上寻找隐式原型的指向,将形成一个链条,这个链条就叫做原型链

- 原型的本质是对象
- 所有的函数都有原型属性prototype
- prototype默认包含一个属性constructor,该属性指向函数本身
- 所有的对象都有隐式原型,
__proto__ - 隐式原型指向该对象的构造函数原型prototype
- 在查找对象成员属性或者方法的时候,如果对象本身没有该成员,则会到隐式原型上查找
- 所有函数的隐式原型都指向Object的prototype
- 两个特殊情况:
- Function的隐式原型指向自己的原型
- Object原型的隐式原型指向null
# 面试题
var F = function () { };
Object.prototype.a = function () {};
Function.prototype.b = function () { };
var f = new F();
console.log(f.a)
console.log(f.b)
console.log(F.a)
console.log(F.b)
答案:
[Function (anonymous)]
undefined
[Function (anonymous)]
[Function (anonymous)]
function A(){}
function B(a){
this.a = a;
}
function C(a){
if(a){
this.a = a;
}
}
A.prototype.a = 1
B.prototype.a = 1
C.prototype.a = 1
console.log(new A().a)
console.log(new B().a)
console.log(new C(2).a)
# ESMASciprt6中的类
传统的构造函数有二义性
属性和原型方法定义是分离的,降低了可读性
原型成员可以被直接枚举
默认情况下,构造函数可以被当做普通函数
# 类的特点
1、类声明不会被提升,与let,const一样
2、类中所有代码默认在严格模式下
3、类的所有方法都是不可被枚举的(enumerable:false)
4、类的方法不能作为构造函数
5、类的构造器必须使用new来调用
class Computer {
constructor(name) {
this.name = name;
}
show() {
console.log(`这是一台${this.name}电脑`)
}
static staticShow() {
console.log("这是静态方法")
}
}
const a = new Computer('联想');
a.show();
console.log(Computer.prototype)
console.log(a.__proto__)
for (let prop in a) {
console.log(prop)
}
Computer.prototype.show();
Computer.staticShow();
# 类的继承
两个新的关键字
- extends
- Super
class Animal {
constructor(name) {
if (new.target === Animal) {
throw new Error('不要直接创建Animal对象实例,应该通过子类创建');
}
this.name = name;
}
say() {
console.log('name--' + this.name);
}
}
class Dog extends Animal {
constructor(name,age) {
super(name)
this.age = age;
}
}
const dog = new Dog('小狗',3);
console.log(dog);
如何使用构造函数实现继承?
function Animal(name) {
this.name = name;
}
Animal.prototype.show = function () {
console.log("name---" + this.name);
}
function Dog(name, age) {
Animal.call(this, name);
this.age = age;
}
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
const d = new Dog("小黑", 3);
d.show();
console.log(d);
console.log(d.__proto__);
# Object常用的API
# 1、Object.is
其实基本和严格相等 (===)一致,只是有一些特殊的相等处理
- NaN和NaN相等
- +0和-0不相等
console.log(NaN === NaN)// false
console.log(Object.is(NaN, NaN))// true
console.log(+0 === -0) // true
console.log(Object.is(+0, -0)) // false
# 2、Object.assign
用来混入对象,相当于浅拷贝
let a = {
id: 1,
name: "jack"
}
let b = {
age: 20
}
const c = Object.assign({}, a, b);
console.log(a)
console.log(b)
console.log(c)
ES7还有更方便的做法
const d = { ...a, ...b }
console.log(d)
# 3、Object.setPrototypeOf
可以设置某个对象的隐式原型
let a = {
id: 1,
name: "jack"
}
let b = {
age: 20
}
Object.setPrototypeOf(b, a);
for (let key in b) {
console.log(key)
}
age
id
name
# 4、Object.getPrototypeOf
查找一个对象的原型对象
let a = {
id: 1,
name: "jack"
}
let b = {
age: 20
}
// Object.setPrototypeOf(b, a);
// for (let key in b) {
// console.log(key)
// }
// console.log(Object.getPrototypeOf(b));
// 通过下面的打印,其实大家可以看出,简写的let b = {},实际上就是let b = new Object();
// console.log(Object.getPrototypeOf(b).constructor.name);
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
let p = new Person("jack", 20);
console.log(Object.getPrototypeOf(p).constructor.name)
Object.setPrototypeOf(b, p);
console.log(Object.getPrototypeOf(b).constructor.name);
# 5、keys,values,entries
let obj = { name: "jack", age: 20 };
console.log(Object.keys(obj));
for (let key of Object.keys(obj)){
console.log(key, obj[key])
}
console.log(Object.values(obj));
for (let value of Object.values(obj)){
console.log(value)
}
console.log(Object.entries(obj));
for (let [key, value] of Object.entries(obj)){
console.log(key, value)
}
# 7、Object.fromEntries
可以将键值对(包括类似于双层数组的Object.entries())类型转换为Object
let authors = [["0", "天蚕土豆"], ["1", "唐家三少"], ["2", "我吃西红柿"], ["3", "火星引力"]];
let obj1 = Object.fromEntries(authors);
console.log(obj1);
let map = new Map();
map.set("name", "斗破苍穹");
map.set("author", "天蚕土豆");
map.set("类型", "玄幻");
map.set("price", "100");
let obj2 = Object.fromEntries(map);
console.log(obj2);
把对象的值翻倍
let obj = {
x: 1,
y: 2,
z: 3
}
let obj2 = Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, value * 2]));
console.log(obj2);
# 8、Object.defineProperty
定义属性描述符:
属性描述符一共有6个:
- value:设置属性值,默认Undefined
- writable:属性值是否可写。默认为true
- enumerable:是否可枚举。是否可以使用for...in或者Object.keys()进行遍历访问。默认为true
- configurable: 是否可设置属性特性,默认为true,如果设置为false,将无法删除该属性,不能修改属性的值,也不能修改属性的属性描述符
- get: 取值函数,默认Undefined
- set: 存值函数,默认Undefined
有些属性不能一起出现,出现value或者wriable,就不能出现get或者set,反之一样
let obj = {
name: "张三",
age: 18,
score: 90,
_type:"admin"
}
Object.defineProperty(obj, "sex", {
value: "男",
writable: false,
enumerable: false,
configurable: true
})
obj.sex = "女";
console.log(obj.sex)
Object.defineProperty(obj, "tel", {
get(val) {
console.log("get---->" + val)
},
set(val) {
console.log("set---->" + val)
}
})
Object.defineProperty(obj, "type", {
get() {
console.log("get---->" + this._type)
return this._type
},
set(val) {
console.log("set---->" + val)
this._type = val
}
})
// get set其实就是参考的后端语言的写法
// class Admin {
// private int id;
// public void setId(int id) {
// this.id = id;
// }
// public int getId() {
// return this.id;
// }
// }
console.log(obj.type)
obj.type = "user"
console.log(obj.type)
# 9、Object.defineProperies
给对象添加或者修改多个属性描述符
let obj = {
name: "张三",
age: 18,
score: 90,
}
Object.defineProperties(obj, {
tel: {
value: "123456",
writable: true,
enumerable: true,
configurable: true
},
sex: {
value: "男",
writable: true,
enumerable: true,
configurable: true,
}
})
console.log(obj)
# Promise
具体见代码...