资讯详情

理解JS的三座大山

在这里插入图片描述 如图所示,JS三座山:

  • 同步、异步
  • 作用域、闭包
  • 原型,原型链

1. 同步、异步

  1. JavaScript是单线程语言
  2. Event Loop(事件循环)是JavaScript的执行机制

最初设计JS用于浏览器验证表单控制DOM如果元素是脚本语言,js多线程,所以两个线程同时对一DOM元素相互冲突,浏览器的分析器无法执行。

如果js没有异步,只能自上而下执行。如果上一行分析时间长,下面的代码就会被堵塞。 对于用户来说,堵塞被认为是卡死,导致用户体验差。例如,它正在进行中ajax请求时,如果不返回数据后面的代码,则无法执行

  1. 首先判断JS是同步还是异步,同步进入主线程运行,异步进入event table.

  2. 异步任务在event table当符合触发条件时(触发条件可能是延迟或延迟)ajax回调),被推入event queue

  3. 进入主线程后,将执行同步任务,直到主线程是免费的event queue检查是否有可执行的异步任务,如有,则推入主线程。

    那怎么知道主线程执行栈是空的呢?js引擎存在过程将继续检查 主线程是否为空,一旦为空,就会去检查是否有函数等待调用

    包含整个script代码块,setTimeout, setIntval Promise , process.nextTick

    在划分宏任务和微任务时没有提到async/ await的本质就是Promise

    setTimeout(function() { 
                   console.log('4') })  new Promise(function(resolve) { 
                   console.log('1') // 同步任务     resolve() }).then(function() { 
                   console.log('3') }) console.log('2') 执行结果: 1-2-3-4 1. 作为宏任务,此代码进入主线程。 2. 先遇到setTimeout,然后将回调函数注册后分发到宏任务event queue. 3. 接下来遇到Promise, new Promise立即执行,then函数分发到微任务event queue 4. 遇到console.log(), 立即执行
    5. 整体代码script作为第一个宏任务执行结束, 查看当前有没有可执行的微任务,执行then的回调。(第一轮事件循环结束了,我们开始第二轮循环)
    6. 从宏任务的event queue开始,我们发现了宏任务event queue中setTimeout对应的回调函数,立即执行。
    
    
    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')
        })
    })
    1.整体script作为第一个宏任务进入主线程,遇到console.log(1)输出1
    
    遇到setTimeout, 其回调函数被分发到宏任务event queue中。我们暂且记为setTimeout1
    3.遇到process.nextTick(),其回调函数被分发到微任务event queue中,我们记为process1
    4.遇到Promise, new Promise直接执行,输出7.then被分发到微任务event queue中,我们记为then1
    又遇到setTimeout,其回调函数被分发到宏任务event queue中,我们记为setTimeout2.
    现在开始执行微任务, 我们发现了process1和then1两个微任务,执行process1,输出6,执行then1,输出8, 第一轮事件循环正式结束, 这一轮的结果输出1,7,6,8.那么第二轮事件循环从setTimeout1宏任务开始
    5. 首先输出2, 接下来遇到了process.nextTick(),统一被分发到微任务event queue,记为process2
    8new Promise立即执行,输出4,then也被分发到微任务event queue中,记为then2
    6. 现在开始执行微任务,我们发现有process2和then2两个微任务可以执行输出3,5. 第二轮事件循环结束,第二轮输出2,4,3,5. 第三轮事件循环从setTimeout2哄任务开始
    10。 直接输出9,跟第二轮事件循环类似,输出9,11,10,12
    7. 完整输出是1,7,6,8,2,4,3,5,9,11,10,12(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)
    

    用来优化promise的回调问题,被称为是异步的终极解决方案

    async函数会返回一个Promise对象,如果在函数中return一个直接量(普通变量),async会把这个直接量通过Promise.resolve()封装成Promise对象。如果你返回了promise那就以你返回的promise为准。await是在等待,等待运行的结果也就是返回值。await后面通常是一个异步操作(promise),但是这不代表await后面只能跟异步才做,await后面实际是可以接普通函数调用或者直接量。 async相当于 new Promise,await相当于then

    如果await后面跟的不是一个promise,那await后面表达式的运算结果就是它等到的东西,如果await后面跟的是一个promise对象,await它会’阻塞’后面的diamante,等着promise对象resolve, 然后得到resolve的值作为await表达式的运算结果。但是此"阻塞"非彼“阻塞”,这就是await必须用在async函数中的原因。 async函数调用不会造成"阻塞",它内部所有的“阻塞”都被封装在一个promise对象中异步执行(这里的阻塞理解成异步等待更合理)

    每个async方法都返回一个promise, await只能出现在async函数中

    单一的promise链并不能发现async/await的有事,但是如果需要处理由多个promise组成的then链的时候,优势就能体现出来了(Promise通过then链来解决多层回调的问题,现在又用async/awai来进一步优化它)

2. 作用域、闭包

  • 闭包是指有权访问另外一个函数作用域中的变量的函数(红宝书)
  • 闭包是指那些能够访问自由变量的函数。(MDN)其中自由变量, 指在函数中使用的,但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另外一个函数作用域中的变量。)

  • 说起闭包,就必须要说说作用域,ES5种只存在两种作用域:
  1. 函数作用域。
  2. 全局作用域 当访问一个变量时, 解释器会首先在当前作用域查找标示符,如果没有找到, 就去父作用域找, 直到找到该变量的标示符或者不在父作用域中,
  3. 这就是作用域链,每一个子函数都会拷贝上级的作用域, 形成一个作用域的链条。
let a = 1;

function f1() { 
        
    var a = 2

  function f2() { 
        
       var a = 3;
       console.log(a); //3
   }
}

在这段代码中,

  • f1的作用域指向有全局作用域(window) 和它本身,
  • 而f2的作用域指向全局作用域(window)、 f1和它本身。
  • 而且作用域是从最底层向上找, 直到找到全局作用域window为止,
  • 如果全局还没有的话就会报错。闭包产生的本质就是,
  • 当前环境中存在指向父级作用域的引用。
function f2() { 
        
    var a = 2

    function f3() { 
        
        console.log(a); //2
    }
    return f3;
}
var x = f2();
x();

这里x会拿到父级作用域中的变量,

因为在当前环境中,含有对f3的引用, f3恰恰引用了window、 f3和f3的作用域。 因此f3可以访问到f2的作用域的变量。那是不是只有返回函数才算是产生了闭包呢?回到闭包的本质,只需要让父级作用域的引用存在即可。

var f4;

function f5() { 
        
    var a = 2
    f4 = function () { 
        
        console.log(a);
    }
}
f5();
f4();

让f5执行,给f4赋值后,等于说现在f4拥有了window、f5和f4本身这几个作用域的访问权,还是自底向上查找,最近是在f5中找到了a,因此输出2。在这里是外面的变量f4存在着父级作用域的引用, 因此产生了闭包,形式变了,本质没有改变。

  • 返回一个函数。
  • 作为函数参数传递。
  • 在定时器、 事件监听、 Ajax请求、 跨窗口通信、 Web Workers或者任何异步中,只要使用了回调函数, 实际上就是在使用闭包。 IIFE(立即执行函数表达式) 创建闭包, 保存了全局作用域window和当前函数的作用域。
	var b = 1;
	function foo() { 
        
	    var b = 2;
	
	    function baz() { 
        
	        console.log(b);
	    }
	    bar(baz);
	}
	
	function bar(fn) { 
        
	    // 这就是闭包
	    fn();
	}
	// 输出2,而不是1
	foo();
	// 以下的闭包保存的仅仅是window和当前作用域。
	// 定时器
	setTimeout(function timeHandler() { 
        
	   console.log('111');
	}, 100)
	
	// 事件监听
	// document.body.click(function () { 
        
	// console.log('DOM Listener');
	// })
	
	// 立即执行函数
	var c = 2;
	(function IIFE() { 
        
	    // 输出2
	    console.log(c);
	})();

for (var i = 1; i <= 5; i++) { 
        
    setTimeout(function timer() { 
        
        console.log(i)
    }, 0)
}  // 6 6 6 6 6 6
// 为什么会全部输出6? 如何改进, 让它输出1, 2, 3, 4, 5?

  • 因为setTimeout为宏任务, 由于JS中单线程eventLoop机制, 在主线程同步任务执行完后才去执行宏任务。
  • 因此循环结束后setTimeout中的回调才依次执行, 但输出i的时候当前作用域没有。 往上一级再找,发现了i,此时循环已经结束,i变成了6,因此会全部输出6。
	for (var i = 0; i < 5; i++) { 
        
	    (function (j) { 
        
	        setTimeout(() => { 
        
	            console.log(j)
	        }, 1000);
	    })(i)
	}

for (var i = 0; i < 5; i++) { 
        
	    setTimeout(function (j) { 
        
	        console.log(j)
	    }, 1000, i);
	}

  • let使JS发生革命性的变化, 让JS有函数作用域变为了块级作用域,
  • 用let后作用域链不复存在。 代码的作用域以块级为单位,
for (let i = 1; i <= 5; i++) { 
        
    setTimeout(function timer() { 
        
        console.log(i)
    }, 2000)
}

3.原型、原型链

JS中所有函数都会有prototype属性,只有函数才有

其所有的属性和方法都能被构造函数的实例对象共享访问

代码如下:

function Person(name){ 
        
		this.name = name
	}
	Person.prototype.sayHello(){ 
        
		console.log('sayHello')
	}
	let p1 = new Person();
	let p2 = new Person();
	console.log(p1.sayHello) //sayHello
	console.log(p2.sayHello) //sayHello

JS中constructor存在每个函数的prototype属性中,其保存了指向该函数的引用

Person.prototype.constructor ==Person   //true

JS中对象都会有个内置属性,即__proto__,(隐式原型链的属性),一般情况下执行创建它的构造函数的prototype的属性,另外函数比较特殊,也会有该属性

p1.__proto__ == Person.prototype

JS 引擎查找摸个属性时,先查找对象本身是否存在该属性,如果不存在就会在原型链上一层一层进行查找

[] instanceof Array   //[].__proto__ == Array.prototype

Object.prototype.toString.call([])  //[Object Array]

Array.isArray([]) //true

[].constructor ==Array

下面代码输出什么

Object instanceof Function //true
Function instanceof Object // true

	function Person(name){ 
        
		this.name = name
	}
	Person.prototype.sayHello(){ 
        
		console.log('sayHello')
	}
	function Boy(){ 
        };
	Boy.prototype = new Person();
	let b1 = new Boy();
	b1.sayHello() //sayHello

	function Person () { 
        .........}
	
	person = new Person ()
	
	res = person instanceof Person
	
	res  // true

	function Person () { 
        ........}
	
	Person.prototype.type = 'object n'
	
	person = new Person ()
	
	res = person.type
	
	res  // object n

实例通过__proto__访问到原型 person.proto=== Person.prototype

原型通过constructor属性访问构造函数 Person.prototype.constructor === Person

person.proto.constructor === Person

在读取一个实例的属性的过程中,如果属性在该实例中没有找到,那么就会循着 proto 指定的原型上去寻找,如果还找不到,则寻找原型的原型:

  1. 实例上寻找
function Person() { 
        }

    Person.prototype.type = "object name Person";

    person = new Person();

    person.type = "我是实例的自有属性";

    res = Reflect.ownKeys(person); //尝试获取到自有属性

    console.log(res);

    res = person.type;

    console.log(res); //我是实例的自有属性(通过原型链向上搜索优先搜索实例里的)
  1. 原型上寻找
function Person() { 
        }

    Person.prototype.type = "object name Person";

    person = new Person();

    res = Reflect.ownKeys(person); //尝试获取到自有属性

    console.log(res);

    res = person.type;

    console.log(res); //object name Person
  1. 原型的原型上寻找
function Person() { 
        }

    Person.prototype.type = "object name Person";

    function Child() { 
        }

    Child.prototype = new Person();

    p = new Child();

    res = [p instanceof Object, p instanceof Person, p instanceof Child];

    console.log(res); //[true, true, true] p同时属于Object,Person, Child

    res = p.type; //层层搜索

    console.log(res); //object name Person (原型链上搜索)

    console.dir(Person);

    console.dir(Child);
  1. 原型链上搜索
  • 原型同样也可以通过 proto 访问到原型的原型,比方说这里有个构造函数 Child 然后“继承”前者的有一个构造函数 Person,然后 new Child 得到实例 p;

  • 当访问 p 中的一个非自有属性的时候,就会通过 proto 作为桥梁连接起来的一系列原型、原型的原型、原型的原型的原型直到 Object 构造函数为止;

  • 原型链搜索搜到 null 为止,搜不到那访问的这个属性就停止:

function Person() { 
        }

  Person.prototype.type = "object name Person";

  function Child() { 
        }

  Child.prototype = new Person();

  p = new Child();

  res = p.__proto__;

  console.log(res);         //Person {}

  res = p.__proto__.__proto__;

  console.log(res);         //Person {type:'object name Person'}

  res = p.__proto__.__proto__.__proto__;

  console.log(res);         //{.....}

  res = p.__proto__.__proto__.__proto__.__proto__;

  console.log(res);         //null

  • js中,继承是一种允许我们在已有类的基础上创建新类的机制;它可以使用现有类的所有功能,并在无需重新编写
  • 原来的类的情况下对这些功能进行扩展。

  • 提高代码的重用性、较少代码的冗余

  • 原型链继承,
  • 借用构造函数继承,
  • 组合式继承(原型链+构造函数),
  • 原型式继承,
  • 寄生式继承,
  • 寄生组合式继承
function Person(name){ 
        
	this.name = name;
	this.sum=function(){ 
        
		alert('this.name',this.name)
	}
}
Person.prototype.age = 100
  1. 原型链继承

    利用原型链的特点继承,让实例的原型等于父类的实例

    实例可以继承父类的构造个函数,实例的构造函数,父类的原型

    不能向父类传递参数,由于实例的原型等于父类的实例,那么改变父类的属性,实例的属性也会跟着改变

function child(){ 
        
	this.name="xiaoming"
}
child.prototype = new Person()
let child1 = new Child()
child1.name //xiaoming
child1.age //100
child1 instanceof Person //true
  1. 借用构造函数继承

    使用call/apply将父类的构造函数引入子类函数

    可以祢补原型链继承的缺点,可以向父类传递参数,只继承父类构造函数的属性

    不能复用,每次使用需要重新调用,每个实例都是父类构造函数的副本,比较臃肿

	function child(){ 
        
		Person.call(this,'xiaoming')
	}
	let child1 = new child()
	child1.name //xiaoming
	child1.age //100
	child1 instanceof Person //false
  1. 组合式继承

标签: 连接器主线sub连接器78pj95组合式连接器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台