2021-10-21 深入JavaScript高级语法(一)

1、GO AO VO

1.1、函数里面的VOAO,全局的VOGO(早期的叫法,ES5之前,不包含ES5):在源代码里面声明的变量和函数,它们是作为VO的一个属性添加上去的

1.2、新的ECMA规范里面改为 Variable Environment GE AE VE:在执行代码中声明的变量和函数,它们是作为一条环境记录(Environment Record)添加到变量环境(VE)的

2、代码解析

2.1、代码被解析时(解析的是函数、变量的定义语句,而不是函数的执行、或者变量的赋值语句),v8引擎内部会帮助我们创建一个对象(·GlobalObject -> GO·)。·GO·是在执行所有代码之前就会被提前创建好的。

2.2、在函数被解析时,会生成一个函数对象,这个对象包含了两个东西。一个是函数的父级作用域(parentScope),一个是函数的执行体。因此,函数的父级作用域跟它的定义位置有关,跟它的调用位置无关

var message = 'hello global';

// 定义在全局
function foo() {
	// 在foo函数自己的内部作用域找message属性,没找到;然后在foo的父级作用域找,父级作用域为GO,结果就打印为"hello global"
	console.log(message); 
}

function bar(){
 var meaasge = 'hello bar';
 // 调用在局部
 foo();
}

bar();

执行结果打印为:hello global

2.3、解析代码时干的事情:
把变量message作为属性挂载到GO上,注意这里代码还没执行,message的值为undefined
把函数foo做为属性挂载到GO上,并会生成一个函数对象,这个对象包含了两个东西。一个是函数的父级作用域(parentScope),一个是函数的执行体,注意这里还没有执行foo()这条代码。

var message = 'hello world';
function foo() {
	console.log('hello foo');
}
foo();

3、执行代码

3.1、v8为了执行代码,v8引擎内部会有一个 执行上下文栈(Execution Context Stack, ECStack)(函数调用栈)。

3.2、因为我们执行的是全局代码,为了全局代码能够正常的执行,需要创建 全局执行上下文(Global Execution Context)(全局代码需要被执行时才会创建)。
·
3.3、执行全局代码时,会创建一个全局执行上下文栈,v8引擎内部会帮助我们创建一个对象VO,这里VO指向GO

3.4、执行函数时,v8引擎内部会帮助我们创建一个对象VO,这时VO指向AO(对应的函数的局部作用域,保存函数里面定义的变量,即为临时变量)。函数内部的代码执行完后,函数就会弹出栈(即函数对应的执行上下文栈就会销毁),执行函数时创建的AO对象也会随之销毁。

4、在js中,函数是一等公民

表示函数的使用是非常灵活的。函数可以作为其他函数的返回值或者参数。js语法允许函数内部再定义函数。引出闭包。

5、闭包内存泄漏

已经没有使用的数据在内存里面没有被销毁调,就称之为内存泄漏。

function foo() {
	var name = 'foo';
	var age = 18;
	function bar() {
		console.log(name);
		// 从js引擎优化的角度来说,虽然foo函数的AO对象由于闭包的原因没有销毁,但是AO对象上的age属性实际是被清空释放了的,因为bar函数里面没有用到age属性
		// console.log(age);
	}
	return bar;
}

var fn = foo();
fn();

这里就会存在内存泄漏。原因:foo函数已经执行完了,但foo对应的AO对象和bar函数解析时生成的函数对象还存在在内存中。
原因是全局的GO对象上的fn属性还保留了对bar函数对象的引用,而bar函数对象里保存的parentScope又指向fooAO对象,fooAO对象里的bar属性又指向bar函数对象。

解决方式:

var fn = null;

原因:取消掉GOfn属性对bar函数对象的引用,这样下一次垃圾回收机制开始的时候,由于从根对象GO上没有路径指向bar函数对象,那么就算bar函数对象和fooAO对象还是在互相引用,但也会被垃圾回收机制回收。

6、this的指向

6.1、在全局作用域下

6.1.1、浏览器:指向window(globalObject)
6.1.2、node环境:指向{}

6.2、函数里面的this是动态绑定的,是在执行的时候确定的。

函数里面的this跟函数定义的位置无关,跟函数被调用时的方式以及调用的位置有关。

6.2.1、绑定规则一:默认绑定

默认绑定:独立函数调用 -> 指向window

例一:

function foo() {
	console.log(this); 
}

foo();

例二:

function foo1() {
	console.log(this); 
}

function foo2() {
	console.log(this); 
	foo1();
}

function foo3() {
	console.log(this); 
	foo2();
}

foo3();

例三:

var obj = {
	name: 'Tom',
	foo: function() {
		console.log(this);
	}
};

var fn = obj.foo;
fn(); // window

例四:

function foo() {
	function bar() {
		console.log(this);
	}
}

var fn = foo();
fn();

6.2.2、绑定规则二:隐式绑定。

也就是它的调用位置中,是通过某个对象发起的函数调用。隐式绑定:object.fn() -> object对象会被js引擎绑定到fn函数中的this里面。

例一:

function foo() {
	console.log(this); 
}

var obj = {
	name: 'Tom',
	foo: foo
};

obj.foo(); // 指向obj对象

例二:

var obj1 = {
	name: 'obj1',
	foo: function() {
		console.log(this);
	}
};

var obj2 = {
	name: 'obj2',
	bar: obj1.foo
};

obj2.bar(); // 指向obj2

6.2.3、绑定规则三:显式绑定。

apply/call/bind可以指定this的绑定对象。

6.2.3.1、call/apply

function foo() {
	console.log(this); 
}

var obj = {
	name: 'obj'
};

foo.call(obj); // 指向obj
foo.apply(obj); // 指向obj
foo.apply('aaa'); // 指向String构造函数

6.2.3.2、call/apply的区别:传参方式不同

function sum(num1, num2, num3) {
	console.log(num1 + num2 + num3);
}

sum.call(null, 1, 2, 3); // 多个参数逗号拼接传递
sum.apply(null, [1, 2, 3]); // 多个参数通过数组传递

6.2.3.3、bind

function foo() {
	console.log(this); 
}

var newFoo = foo.bind('aaa');
newFoo(); // 指向aaa

默认绑定跟显示绑定bind冲突:显示绑定优先级大于默认绑定。

6.2.4、new绑定

当我们通过new来调用一个函数的时候,它会自动帮我们生成一个对象。生成这个对象后,this就会指向这个对象。当我们在函数内向this添加属性的时候,就会默认把这个属性添加到生成的这个对象上。如果函数没有返回其他对象用{}包起来的对象或者数组,意思是就算显式的返回如字符串null等值,打印p1还是默认返回的那个对象),那么函数就会默认返回这个生成的对象。

function Person(name, age) {
	this.name = name;
	this.age = age;
}

var p1 = new Person('tom', 18);
console.log(p1.name, p1.age);

7、this其他补充

7.1、浏览器内部的一些默认绑定

一般情况下,调用setTimeout或者setInterval定时器方法时,定时器的回调函数里的this是一定指向window的。

setTimeout(function() {
  console.log(this); // window
}, 100);

相当于v8引擎内部实现setTimeout方法时,调用它的回调函数时是直接调用的,即默认绑定

// 伪代码
setTimeout(fn, duration) {
	fn(); // 默认绑定
}

7.2、浏览器内部的一些隐式绑定

<div class='box'>111</div>
var boxDom = document.querySelector('.box');

boxDom.onclick = function() {
  console.log(this); // 指向boxDom这个对象
}

相当于v8引擎内部实现中,调用div绑定的点击方法是通过obj.fn()的形式调用的,即隐式绑定

// 伪代码
boxDom.onClick();

7.3、浏览器内部的一些显式绑定

<div class='box'>111</div>
var boxDom = document.querySelector('.box');

boxDom.addEventListener('click', function() {
  console.log(this); // 指向boxDom这个对象
});

相当于v8引擎内部实现中,调用div监听的点击方法是通过fn.call(boxDom)的形式调用的,即显式绑定

7.4、数组:forEach/map/filter/find等 里面的this

默认情况下,数组方法的第一个参数回调函数里面的this指向window

var names = ['aaa', 'bbb', 'ccc'];
names.forEach(function(item) {
  console.log(item, this); // window
});

可以给数组方法传入第二个参数(有第二个参数的情况下,thisArg?),改变数组方法的第一个参数回调函数里面的this指向,相当于显式绑定

var names = ['aaa', 'bbb', 'ccc'];
names.forEach(function(item) {
  console.log(item, this); // String{'aaa'}
}, 'aaa');

8、this绑定规则优先级

学习了四条规则,接下来我们只需要去查找函数的调用应用了哪条规则即可,但是如果一个函数调用位置应用了多条规则,优先级谁更高呢?

8.1、 默认绑定的优先级最低

8.2、显式绑定优先级 > 隐式绑定优先级

apply()/call()绑定测试:

var obj = {
  name: 'obj',
  foo: function() {
    console.log(this)
  }
}

obj.foo(); // 隐式绑定,this指向obj对象
obj.foo.call('aaa'); // 隐式绑定和显式绑定同时存在,this指向String{'aaa'}

bind()绑定测试:

function foo() {
  console.log(this);
}

var obj = {
  name: 'obj',
  foo: foo.bind('aaa')
}

obj.foo(); // this指向String{'aaa'}

8.3、new绑定优先级 > 隐式绑定优先级

var obj = {
  name: 'obj',
  foo: function() {
    console.log(this);
  }
}

var f = new obj.foo(); // this指向foo{}这个函数对象

8.4、new绑定优先级 > 显式绑定优先级

结论:new关键字不能和apply/call一起使用。
所以,这里通过new和bind来测试:

function foo() {
  console.log(this);
}

var bar = foo.bind('aaa');
var obj = new bar(); // this指向foo{}这个函数对象

8.5、结论

new绑定 > 显式绑定(apply/call/bind) > 隐式绑定(obj.fn()) > 默认绑定(独立函数调用)

9、特殊绑定

9.1、忽略显式绑定

apply/call/bind:当传入null/undefined时,自动将this绑定成全局对象window

function foo() {
  console.log(this);
}

foo.apply(null); // this指向window
foo.apply(undefined); // this指向window

var bar = foo.bind(null);
bar(); // this指向window

9.2、间接函数引用

var obj1 = {
  name: 'obj1',
  foo: function() {
    console.log(this)
  }
};

var obj2 = {
  name: 'obj2'
}; // 注意这里一定要加分号,不然在进行js引擎在进行词法分析的时候,会认为当前语句还没结束,会报错

// 相当于js引擎会把
// {
//   name: 'obj2'
// }(obj2.bar = obj1.foo)()当成是一个整体

(obj2.bar = obj1.foo)(); // 先执行赋值表达式,再调用函数,指向window

注意,上面的代码跟(obj1.foo)()是不一样的,这里(obj1.foo)()等价于obj1.foo(),属于隐式调用

10、箭头函数 arrow function

  • 箭头函数不会绑定thisarguments属性
  • 箭头函数不能作为构造函数来使用(不能和new一起来使用,会抛出错误)

10.1、常见的简写方式

  1. 如果只有参数只有一个,()可以简写
  2. 如果函数执行体只有一行代码,那么{}也可以省略,并且它会默认将这行代码的执行结果作为返回值
var nums = [1, 2, 3];
nums.filter(item => item % 2 === 0);
  1. 如果一个箭头函数只有一行代码,并且返回一个对象,这个时候可以在{}外面包上一个()
var nums = [1, 2, 3];
var newArr = nums.map(item => ({
    id: item
}))

10.2、箭头函数中的this指向

箭头函数不使用this的四种标准规则(也就是不绑定this,即箭头函数内部作用域找不到this这个属性),而是根据外层作用域来决定this

var foo = () => {
    console.log(this);
};

foo(); // window

var obj = { foo };
obj.foo(); // window

foo.call('abc'); // window

应用举例:

var obj = {
  data: [],
  getData: function() {
  	// 发送网络请求,将结果放到上面的data属性中
  	// 在箭头函数之前的解决方案
    // var _this = this;
    // setTimeout(function() {
    //   _this.data = ['a', 'b', 'c'];
    // }, 200)

	// 箭头函数之后
    setTimeout(() => {
      this.data = ['a', 'b', 'c'];
    }, 200)
  }
};

obj.getData();

11、上层作用域的理解

var obj = {
  name:'obj',
  foo: function() {
    // 上层作用域是全局
  }
}

function Student() {
  this.foo = function() {
    // 上层作用域是Student
  }
}

注:在我们之前讲的所有课程中,只有两个地方会产生作用域。第一个是我们的全局作用域;第二个是函数作用域。对象是不产生作用域的!!!

12、this面试题

12.1、面试题一

var name = "window";

var person = {
  name: "person",
  sayName: function() {
    console.log(this.name)
  }
};

function sayName() {
  var sss = person.sayName;
  sss(); // window:独立函数调用
  person.sayName(); // person:隐式调用
  (person.sayName)(); // person:隐式调用
  (b = person.sayName)(); // window: 赋值表达式(独立函数调用)
}

sayName();

12.2、面试题二

var name = "window";

var person1 = {
  name: 'person1',
  foo1: function() {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function() {
    return function() {
      console.log(this.name)
    }
  },
  foo4: function() {
    return () => {
      console.log(this.name)
    }
  }
};

var person2 = {name: 'person2'};

person1.foo1(); // person1:隐式绑定
person1.foo1.call(person2); // person2:显示绑定优先级大于隐式绑定

person1.foo2(); // window:不绑定作用域,上层作用域是全局
person1.foo2.call(person2); // window:不绑定作用域,上层作用域是全局

person1.foo3()(); // window:独立函数调用
person1.foo3.call(person2)(); // window:独立函数调用
person1.foo3().call(person2); // person2:最终调用返回函数时,使用的是显示绑定

person1.foo4()(); // person1:箭头函数不绑定this,上层作用域this是person1
person1.foo4.call(person2)(); // person2:上层作用域被显示绑定了person2
person1.foo4().call(person2); // person1:上层找到person1

12.3、面试题三

var name = 'window';

function Person(name) {
  this.name = name;
  this.foo1 = function() {
    console.log(this.name)
  };
  this.foo2 = () => console.log(this.name);
  this.foo3 = function() {
    return function() {
      console.log(this.name);
    }
  };
  this.foo4 = function() {
    return () => {
      console.log(this.name)
    }
  }
}

var person1 = new Person('person1');
var person2 = new Person('person2');

person1.foo1(); // person1:隐式绑定
person1.foo1.call(person2); // person2:显示绑定优先级大于隐式绑定

person1.foo2(); // person1:上层作用域中的this是person1
person1.foo2.call(person2); // person1:上层作用域中的this是person1

person1.foo3()(); // window:独立函数度调用
person1.foo3.call(person2)(); // window:独立函数调用
person1.foo3().call(person2); // person2:显示绑定

person1.foo4()(); // person1:箭头函数不绑定this,上层作用域是person1
person1.foo4.call(person2)(); // person2:箭头函数不绑定this,上层作用域被显示绑定为person2
person1.foo4().call(person2); // person1:箭头函数不绑定this,上层作用域是person1

12.4、面试题四

var name = 'window';

function Person(name) {
  this.name = name;
  this.obj = {
    name: 'obj',
    foo1: function() {
      return function() {
        console.log(this.name);
      }
    },
    foo2: function() {
      return () => {
        console.log(this.name);
      }
    }
  }
}

var person1 = new Person('person1');
var person2 = new Person('person2');

person1.obj.foo1()(); // window:独立函数调用
person1.obj.foo1.call(person2)(); // window:独立函数调用
person1.obj.foo1().call(person2); // person2:显示绑定

person1.obj.foo2()(); // obj:箭头函数不绑定this,上层作用域为隐式绑定的obj
person1.obj.foo2.call(person2)(); // person2:箭头函数不绑定this,上层作用域为显示绑定的person2
person1.obj.foo2().call(person2); // obj:箭头函数不绑定this,上层作用域为隐式绑定的obj

13、手动实现call/apply/bind方法

13.1、call()方法的实现

Function.prototype.hycall = function(thisArg, ...args) {
  // 在这里可以去执行调用的那个函数(foo)
  // 问题:怎样可以获取到是哪一个函数执行了hycall
  
  // 1.获取需要被执行的函数(原理:hycall()被调用时是隐式绑定,hycall()内部的this就指向了.hycall()前面的那个对象)
  var fn = this;
  
  // 2.把thisArg转成对象类型(防止它传入的是非对象类型)
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window
  
  // 3.调用需要被执行的函数
  thisArg.fn = fn; // 把fn()函数作为一个属性绑定到我们传入的参数上
  var result = thisArg.fn(...args); // 隐式绑定,把fn()内部的this指向传入的参数(thisArg)
  delete thisArg.fn; // 调用完fn()函数后,如果不需要这个属性,可以删除掉
  
  // 4. 将最终的结果返回出去
  return result;
}

function foo() {
  console.log('foo被调用了',this)
}

function sum(num1, num2) {
  console.log('sum被调用了',this)
  return num1 + num2;
}

foo.hycall('123');

sum.hycall({}, 1,2,3);

13.2、apply()方法的实现

Function.prototype.hyapply = function(thisArg, argArray) {
  // 1.获取到要执行的函数
  var fn = this;
  
  // 2.处理绑定的thisArg
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window
  
  // 3.调用需要被执行的函数
  thisArg.fn = fn;
  argArray = argArray || [];
  var result = thisArg.fn(...argArray)
  delete thisArg.fn
  
  // 4.将最终结果返回去
  return result
  
}

function foo() {
  console.log('foo被调用', this)
}

function sum(num1, num2) {
  console.log('sum被调用', this)
  return num1 + num2
}

// 系统调用
// var result = sum.apply('abc', [20, 30])
// console.log(result)

// 自己实现的调用
foo.hyapply({});

var result = sum.hyapply('abc', [20, 30])
console.log(result)

13.3、bind()方法的实现

Function.prototype.hybind = function(thisArg, ...argArray) {
  // 1.获取到要执行的函数
  var fn = this;
  
  // 2.处理绑定的thisArg
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window
  
  function proxyFn(...args) {
    // 3.将函数放到thisArg中进行调用
    thisArg.fn = fn;
    // 特殊:对两个传入的参数进行合并
    var finalArgs = [...argArray, ...args];
    var result = thisArg.fn(...finalArgs)
    delete thisArg.fn
    
    return result
  }
  
  return proxyFn
}

function foo() {
  console.log('foo被执行', this)
}

function sum(num1, num2, num3, num4) {
  console.log(num1, num2, num3, num4)
}

// var bar = foo.hybind('abc')
// var result = bar()
// console.log(result)

var newSum = sum.hybind('abc', 10, 20)
var result = newSum(30, 40)
console.log(result)

14、arguments

14.1、arguments的基本使用

function foo(num1, num2, num3) {
  // 类数组(array-like)对象(长的像是一个数组,本质上是一个对象):arguments
  // console.log(arguments)
  
  // 常见的对arguments的操作是三个
  // 1.获取参数的长度
  console.log(arguments.length)
  
  // 2.根据索引值获取某一个参数
  console.log(arguments[3])
  
  // 3.callee获取当前arguments所在的函数
  console.log(arguments.callee)
}

foo(10, 20, 30, 40, 50)

14.2、类数组转数组:Array.prototype.slice.call()

js内部slice方法的实现:

Array.prototype.hyslice = function(start, end) {
  var arr = this;
  // ... 对start、end参数的一些处理
  start = start || 0;
  end = end || arr.length
 
  var newArray = [];
  for (var i = start; i< end; i++) {
    newArray.push(arr[i])
  }
  return newArray
}

var newArray = Array.prototype.hyslice.call(['aaa', 'bbb', 'ccc'], 1, 3)
console.log(newArray)

14.3、实现:将arguments转成array

function foo(num1, num2, num3) {
  // 1.Array.prototype.slice将arguments转成array
  var newArr1 = Array.prototype.slice.call(arguments)
  console.log(newArr1)
  
  var newArr2 = [].slice.call(arguments)
  console.log(newArr2)
  
  // 2.ES6的语法
  var newArr3 = Array.from(arguments)
  console.log(newArr3)
  
  var newArr4 = [...arguments]
  console.log(newArr4)
}

foo(10, 20, 30, 40, 50)
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值