一、程序运行机制
1)编译阶段:
1、分词
2、构建ast
3、代码生成----转换成一段可执行的机器指令。
2)js运行阶段
1、预编译阶段(由编译器将js编译成可执行的代码)
- 进行变量声明
- 进行变量提升,但其值是undefined
- 所有非表达式的函数声明,进行提升
2、代码执行阶段
一共有三个角色:编译器、执行引擎、作用域。
二、作用域
1、JS作用域及作用域链
作用域就是根据名称,去查找变量的规则。
作用域分为词法作用域/静态作用域和动态作用域/上下文。词法作用域就是定义在词法阶段的作用域。上下文就是在运行时的作用域。在闭包的时候上下文和作用域不一致。
var a=2
以上代码编译器先去问作用域,是不是有了一个a在当前作用域中?
作用域:是,编译器就会忽略这个声明,然后继续往下;
不是,编译器会在当前的作用域中,产生一个新的变量,命名为a;
然后,生成a=2这句话所能够被引擎执行的代码。
runtime(运行时)
引擎在执行时,会问作用域,是不是已经有一个a,在当前的作用域中?
如果是:赋值为2
如果不是:去上一层的作用域中查找。
如果找到了,就直接赋值为2,如果没找到,引擎就会抛出一个异常。
一个函数里面有函数,最外层的函数的生命周期是最久的,它们的地址和值就是栈的形式,先进后出。它们的变量作用域查找,是从内向外的一个方式查找。
let a = 'global';
console.log(a);
function course() {
let b = 'zhuawa';
console.log(b);
session();
function session() {
let c = 'this';
console.log(c);
teacher();
function teacher() {
let d = 'yy';
console.log(d);
// 作用域查找
console.log(b);
}
}
}
course();
三、this上下文
this是在执行时动态读取上下文决定的,不是在定义时决定。
箭头函数:this永远指向定义它的对象的上一级,不管以什么方式调用。this改变只能说它外层的对象被不同的方式调用,外层对象的this被改变了
//都是指打印的this
//箭头函数
function far() {
this.bar=23;
const foo = {
bar: 10,
fn: ()=> {
console.log(this.bar);
console.log(this);
}
};
let xv = foo.fn;//如果这里是foo.fn(),执行far(),this指向windows,new出来的指向far{bar:23}对象
xv()
}
far();//this指向windows
new far();//this指向far{bar:23}这个对象
//普通函数
function far() {
this.bar=23;
const foo = {
bar: 10,
fn: function() {
console.log(this.bar);
console.log(this);
}
};
let xv = foo.fn;//这里改成foo.fn(),不管是far()执行还是new出来都是指向foo这个对象的
xv();//这就是普通函数的直接调用,指向windows
}
far();//this指向windows
new far();//this指向windows
//剪头函数
function far() {
this.bar=23;
const foo = {
bar: 10,
fn: ()=> {
console.log(this.bar);
console.log(this);
}
};
this.xv = foo.fn;
this.xv()
}
far();//this指向windows
new far();//this指向far{bar:23,xv:f()}这个对象
//普通函数
function far() {
this.bar=23;
const foo = {
bar: 10,
fn: function() {
console.log(this.bar);
console.log(this);
}
};
this.xv = foo.fn;
this.xv()
}
far();//this指向windows,因为far()内部this指向windows,xv属于this属性,这个xv方法内的this是普通函数自然指向调用它的对象外层的this
new far();//this指向far{bar:23,xv:f()}这个对象,同理
由上面可知:
普通函数:
- 函数直接调用,this指向windows
function foo() {
console.log('函数内部的this:', this);
}
foo();//this指向windows
2)隐式绑定 - this指向调用堆栈的上一级
function fn() {
console.log('隐式绑定:', this.a);
}
const obj = {
a: 1
}
obj.fn = fn;//obj属性fn的值指向fn()方法的地址
obj.fn();//a 为1,this指向obj
如:
const o1 = {
text: 'o1',
fn: function() {
return this.text;
}
}
const o2 = {
text: 'o2',
fn: function() {
return o1.fn();
}
}
const o3 = {
text: 'o3',
fn: function() {
let fn = o1.fn;
return fn();
}
}
console.log(o1.fn());//o1
console.log(o2.fn());//01
console.log(o3.fn());//undefined,this指向全局,因为它没有被某个对象值引用,是独立直接调用
如果要改变this指向,如o2.fn调用指向o2,
1)、人为改变this,使用call,apply,bind改变this指向
2)、不改变,即下面的方法,隐式
const o2 = {
text: 'o2',
fn: o1.fn//将o2的fn属性指向o1.fn地址,这样就是以o2对象调用o1.fn
}
显式绑定(bind | apply | call)
function foo() {
console.log('函数内部的this:', this);
}
foo();
foo.call({a: 1});
foo.apply({a: 1});
const bindFoo = foo.bind({a: 1});
bindFoo();
call、apply、bind的区别:
1)传参不同,apply是数组,call是逗号隔开,bind是逗号隔开
2)直接返回不同,最终执行返回相同,bind是返回一个函数,需要调用,其他是直接调用
new 关键字出来的对象里的this指向这个对象。
class Course {
constructor(name) {
this.name = name;
console.log('构造函数中的this', this);
}
test() {
console.log('类方法中的this', this);
}
asyncTest() {
console.log('异步方法外', this);
setTimeout(function() {
console.log('异步方法中的this', this);
}, 100)
}
}
const course = new Course('this');
course.test();
course.asyncTest();
这里的异步函数settimeout传入匿名function执行,效果和全局执行函数效果相同,指向windows,把function改为独立于上下文的箭头函数即可
bind原理,手写bind
function sum(a, b, c) {
console.log(a, b, c, this);
return a + b + c;
}
Function.prototype.newBind = function(){
//bind返回一个方法
//传参不变
//返回原函数执行结果
const _this = this;
const args = Array.prototype.slice.call(arguments);
//arguments是一个伪数组,需要用array原型链方法slice处理变成真的数组
const newThis = args.shift();
return function(...innerArgs){
return _this.apply(newThis,[...args,...innerArgs);
}
}
手写called方法:
function called(context,...rest){
context.fn = this;
if(context){
const result = context.fn(...rest);
delete context.fn;
return result;
} else {
this(...rest);
}
}
apply还可以用于多传参数组化,如:
Math.max(1,2,3);
const arr = [1,2,3];
let max = Math.max.apply(this,arr);
this指向的优先级为new>显式>隐式>默认
function fn(){
console.log(this);
}
//隐式
const obj = {fn}
obj.fn;//this指向obj
//显式,显式》隐式
obj.fn.bind(111);//this为111
function foo(a){
this.a = a;
}
const obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1);//{a:2}
let bar2 = new bar();
bar2(3);
console.log(obj1);//{a:2}
//new > 显式
console.log(bar2);//{a:3}
3、闭包
闭包就是函数的定义作用域和执行作用域不一致,内部函数引用了外部的变量,当外部函数执行完时,该变量不会销毁。
闭包是一个函数和它周围状态的引用捆绑在一起的组合
1)函数作为返回值的场景:
function mail(){
let content = '';
return function(){
console.log(content);
}
}
const event = mail();
event();//函数外部可以访问函数内部变量content
2)函数作为参数
let content;
function eventloop(fn){
content = 1;
fn();
}
function mail(){
console.log(content)
}
eventloop(mail);
3)函数嵌套
let counter = 0;
function outer(){
function inner(){
counter++;
console.log(counter);
}
return inner;
}
4)异步执行的闭包
for(var i = 0; i<5;i++){
setTimeout(()=>{
console.log(i);
},100)
}
//以上执行,会打印5遍,每遍i的值都是5;因为等异步定时到时,在这个作用域里i已经循环完了为5
//修正:
for(var i = 0; i<5;i++){
!function(i){//立即执行方法里是可以访问外部变量的
setTimeout(()=>{
console.log(i);
},100)
}(i)//用立即执行的方法形成闭包,这是一个独立作用域,每次循环传入i,在相应的作用域内,i就是当时传入的那个值
//还有一个种方式就是将var改成有块级作用域的let,这样也会形成一个独立作用域
}
5)立即执行遇上块级作用域
let count = 0;
(function immediate(){
if(count == 0){
let count = 1;
console.log(count);//1,往最近查找就是1
}
console.log(count);//0
})()
6)拆分执行,多个闭包
function createIncrement() {
let count = 0;
function increment() {
count++;
}
let message = `count is ${count}`;
function log() {
console.log(message);
}
return [increment, log];
}
const [increment, log] = createIncrement();
increment();
increment();
increment();
log();//这里打印出来message为count is 0
//但是,如果在log里直接打印count,count的值是3;因为message是一个基本类型的变量,在createIncrement执行时它被赋值了,这个时候是count的初始值,之后message都不会改变,所以是0,同理,由于count是基本类型,如果在message后面紧接着将count赋值给一个变量singer = count;在log里打印singer,也是0;
//假设count是一个引用类型,由于message是一个基本类型,所以还是打印的是count的初始值,但是在message后面紧接着将count赋值给一个变量singer = count;在log里打印singer,那就是三次操作之后的结果;
7)实现私有变量
const stack = {
items:[],
push:function(a){
items.push(a)
}
}
function createStack(){
items = [];//私有变量,外界只能通过push访问
return {
push:function(a){
items.push(a)
}
}
}
4、一个this🈯️向的题
var number = 5;
var obj = {
number:3,
fn1:(function(){//自执行,this指向windows/global
var number;
this.number *= 2;
number = number * 2;
number = 3;
return function(){
var num = this.number;
this.number *= 2;
console.log(num);//10 3
number *= 3;
console.log(number);//9 27
}
})()
}
var fn1 = obj.fn1;
fn1.call(null);
obj.fn1();
console.log(window.number);//20