执行上下文

原则是先进后出,Js 的任何代码动作都是在执行上下文中进行的。

image.png

  1. / 引用 慕课手记 的图示来示例一下:
  2. console.log(1);
  3. function pFn() {
  4. console.log(2);
  5. (function cFn() {
  6. console.log(3);
  7. }());
  8. console.log(4);
  9. }
  10. pFn();
  11. console.log(5);
  12. // 输出:1 2 3 4 5
  13. //解析:
  14. //首先进入 window.onload 全局环境 页面加载事件 打印 1 ,全局环境压入栈底
  15. //然后执行函数 pFn 进入执行栈 打印 2
  16. //然后执行闭包函数 cFn 进入执行栈 打印 3 , cFn 函数体执行完, 出栈
  17. //紧跟着 打印 4 , pFn 函数体执行完, 出栈
  18. //最后打印 5 ,全局环境出栈

结论:

JavaScript单线程,所有的代码都是自上而下执行。
浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的低部。
每进入一个函数,就会为这个函数创建一个执行上下文,并且把它压入栈顶,执行完,就出栈,等待垃圾回收
最后全局执行上下文出栈,有且只有一个。

this的指向

this指向调用函数的那个对象,this的值是在指向的时候才能确认,定义时不能确认。

this在构造函数中:

  1. var x = 1;
  2. function test() {
  3. console.log(this.x);
  4. }
  5. test(); // 1
  6. // 结论:this 指向全局 window

this在普通对象中:

  1. var x = 0 ;
  2. function test() {
  3. console.log(this.x);
  4. }
  5. var obj = {};
  6. obj.x = 1;
  7. obj.m = test;
  8. obj.m(); // 1
  9. // 结论: this 指向 obj

New一个新的构造函数:

  1. var x = 1;
  2. function test() {
  3.  this.x = 2;
  4. }
  5. var obj = new test();
  6. obj.x // 2
  7. x //1
  8. // 结论 this 指向 new 出来的构造函数的实例对象, 也就是 obj

闭包中的this:

  1. var x = 1;
  2. var object = {
  3. x: 2,
  4. getX: function() {
  5. return function() {
  6. return this.x;
  7. };
  8. }
  9. }
  10. conosole.log( object.getX()() ); // 1
  11. // 分析:object.getX() , 是调用对象 object 里的函数 getX()
  12. // 此时 this 按照上边所说应该是指向对象 object
  13. // 我们把 object.getX()(); 拆开来写就是
  14. // var fun = object.getX(); return 一个匿名函数 赋值给变量 fun
  15. // 匿名函数 fun() 调用, 此时的执行环境是全局, 所有指向 window
  16. // 所以执行结果是 1

总结:

  • this是Js语言的一个关键字
  • this在函数执行的时候才能确定,因为 this 是执行上下文的一部分,执行上下文需要在函数体代码执行之前确定,而不是定义的时候
  • this总是指向调用函数的那个对象
  • 注意:严格模式略有不同

其实 this 也是可以改变的,通过 bind,call,apply , 具体使用方式自行度娘

闭包

闭包是什么?

闭包就是能够读取其他函数内部变量的函数。在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

简单点说:就是定义在一个函数内部的函数,就形成了一个闭包。

闭包的用途:

1,在外部读取函数内部的变量
2,这些变量始终保存在内存中(变相的储存,不被污染)

闭包优缺点:

优点:可以访问函数内部的变量,并且不受污染,始终保存在内存中
缺点:就是闭包会使函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成页面的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部清除。

看看以下代码输出什么?

  1. for(var i = 0; i < 10; i++) {
  2. setTimeout(() => {
  3. console.log(i)
  4. }, 0)
  5. }

答案:输出10个10

如果要输出0到9应该怎么做?
方法1:val改成let,因为let有块级作用域之分

  1. for(let i = 0; i < 10; i++) {
  2. setTimeout(() => {
  3. console.log(i)
  4. }, 0)
  5. }

方法2:使用闭包

  1. for(var i = 0; i < 10; i++) {
  2. function(i){
  3. setTimeout(() => {
  4. console.log(i)
  5. }, 0)
  6. }(i)
  7. }

原型,原型链

原型:在JavaScript中原型是一个prototype对象,用于表示类型之间的关系。
原型链:JavaScript 一切皆对象,对象与对象存在着继承关系,通过prototype原型指向父对象,一直到Object,这样就形成了一条链条,专业术语称原型链。

  1. //假设对象 obj1 的父类是 对象obj2,那么 obj1 与 obj2 的关系就是
  2. obj1.prototype => obj2
  3. //这样就形成了一条链条
  4. obj1.prototype => obj2.prototype => Object => null // 原型链

prototype 原型

每一个函数创建的时候就为其自动创建一个 prototype 属性,子类会继承这个这属性,比如:

  1. // 要注意:prototype是函数才会有的属性
  2. function Person() {} // 构造函数 Person
  3. Person.prototype.name = 'Hisen'; //构造函数原型挂载一个属性 name 赋值为 Hisen
  4. var person = new Person(); // 基于构造函数创建一个实例对象 person
  5. console.log(person.name) // Hisen

图例:image.png

proto

每一个JavaScript对象都具有的一个属性,叫proto,这个属性会指向该实例对象构造函数的原型

  1. function Person() {}
  2. var person = new Person();
  3. console.log(person.__proto__ === Person.prototype); //true

图例:image.png

constructor

每个原型都有一个constructor属性指向关联的构造函数

  1. function Person() {}
  2. console.log(Person === Person.prototype.constructor); //true

图例:image.png

原型链

当读取实例对象的属性时,如果找不到,就会查找与实例对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层对象 Object 为止 , 还是找不到宣布放弃,返回 null .

  1. function Person() {}
  2. Person.prototype.name = 'Hisen';
  3. var person = new Person(); // new 一个子类
  4. person.name = 'Hisen child'; // 给子类添加属性 name 并赋值
  5. console.log(person.name) // Hisen child
  6. delete person.name; //删除 person.name ;
  7. console.log(person.name) // Hisen 则会找原型中的name,也就是父类 Person 原型的 name

可以看到删除 person.name 之后,会往上一层查找,也就是 person.proto
注解一下:( person.proto == Person.prototype )
也就是从父类的原型中去查找 ( Person.prototype ) , 此时找到了 Hisen , 打印 Hisen
那如果父类的原型中 ( Person.prototype )也没有呢 ? 原型的原型又是什么呢 ?

要记住:原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它,那就是:

  1. var obj = new Object();
  2. obj.name = 'Hisen'
  3. console.log(obj.name) // Hisen

所以原型对象是通过Object构造函数生成的,结合之前所讲,实例的proto指向构造函数的prototype,所以我们再更新下图例:

image.png

如上图中,那Object.prototype的原型呢?
null,就是null,所以查到Object.prototype就可以停止查找了,最终图例:

image.png

image.png

面向对象封装、继承

面向对象有三大特性,封装、继承和多态。

原型链继承

一个对象的实例赋值给另一个构造函数的原型

借用构造函数继承

利用call()或apply()实现

组合继承

原型链和借用构造函数的组合,原型链实现共享的属性和方法继承,构造函数实现实例属性的继承

原型式继承

寄生式继承

寄生组合继承

js的单线程

JavaScript的一个语言特性(也是这门语言的核心)就是单线程。什么是单线程呢?简单地说就是同一时间只能做一件事,当有多个任务时,只能按照一个顺序一个完成了再执行下一个。

为什么JS是单线程

  • JS最初被设计用在浏览器中,作为浏览器脚本语言,JavaScript的主要用途是与用户互动以及操作DOM,如果浏览器中的JS是多线程的,会带来很复杂的同步问题
  • 比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准 ?
  • 所以为了避免复杂性,JavaScript从诞生起就是单线程,为了提高CPU的利用率,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以这个标准并没有改变JavaScript单线程的本质;

函数防抖与节流

其实函数防抖和节流都是对web性能的优化方案,主要是对网页中频繁触发,频率较快的事件进行一个优化。例如:滚动条事件,输入框输入事件,鼠标移动事件等。

防抖

作用:onscroll,oninput等时时触发的问题

实现原理:通过setTimeout设置时间,在触发事件后,在一定时间内没有再次触发事件,处理函数才会执行,如果在设定的时间内又触发了事件,那就重新计时。

通俗解释:张三在电梯里,电梯每5秒关闭电梯门,当电梯过了3秒时,将要关闭电梯门时,李四进来了,这时候电梯又重新计时,经过5秒后关闭电梯门,如果又过了3秒时,王五进来了,电梯又会重新计时,再经过5秒且这5秒内没有人再进入,电梯就开始正常升降了。

函数防抖其实是在规定时间内,频繁触发该事件,以最后一次触发为准;
实现方式就是创建一个定时器,每次执行的时候,清除旧定时器,并创建一个新的定时器重新记录时间

  1. //参数:要执行的函数和间隔的毫秒数
  2. function debounce(fn, time) {
  3. var timer = null; // 声明 timer
  4. return function() {
  5. clearTimeout(timer) // 清除定时器
  6. timer = setTimeout(function() { // 创建定时器赋给局部变量 timer
  7. fn.apply(this)
  8. }, time)
  9. }
  10. }
  11. // 在规定时间内,触发多次,以最后那一次为准,
  12. // 因为每一次触发,旧的定时还没有执行就被清除了,又创建了新的定时器

执行上下文/this的指向/闭包/原型/继承/防抖/节流 - 图8


节流

作用:onscroll,onkeyup,onkeydown,resize,onkeypress等主要解决高频事件。

原理:单纯的降低频率,保证一段时间内,只执行一次就达到节省资源的目的。

函数节流其实是在规定时间内,事件只被触发一次 ;
实现方式就是通过时间戳,当前的时间戳 - 最后一次执行的时间戳 > 设置规定时间,则生效一次;也就说在频繁触发的情况下,该事件触发的频率会降低。

  1. // 参数:要执行的函数和毫秒数
  2. function throttle(fn, time) {
  3. var lastTime = 0; // 初始化最后一次执行时间
  4. return function() {
  5. var nowTime = Date.now(); // 获取当前时间毫秒数
  6. if (nowTime - lastTime > time) { // 当前时间毫秒数 - 最后一次执行时间毫秒数 > 设置规定时间
  7. fn.call(this);
  8. lastTime = nowTime; // 更新最后一字执行时间毫秒数
  9. }
  10. }
  11. }
  12. // 每一次执行,当前时间就会减去最后一次执行时间
  13. // 如果大于设置时间,就触发一次,有效的节省了事件触发频率。

例:onkeyup每键入一个字母就会触发一次,如果触发后要请求数据,那就要执行很多次请求了
节流前:
执行上下文/this的指向/闭包/原型/继承/防抖/节流 - 图9

节流后:执行上下文/this的指向/闭包/原型/继承/防抖/节流 - 图10

函数柯里化

函数柯里化简单的理解可以是:把函数多个参数转换成单个参数的链式调用,其实有点像分段返回:

  1. // 普通方式
  2. function add (x, y, z) {
  3. return x + y + z;
  4. }
  5. add(1, 2, 3) // 6
  6. // 柯里化方式
  7. function currying (x) {
  8. console.log("x = " + x) // do something
  9. return function(y) {
  10. console.log("y = " + y) // do something
  11. return function(z) {
  12. return x + y + z
  13. }
  14. }
  15. }
  16. currying(1)(2)(3) // 6
  17. // x = 1
  18. // y = 2
  19. // 6

从上面的例子可以看出柯里化的好处展现的淋漓尽致,上面分别在不同的位置打印了对应的值,结果显而易见,相当于分段返回当前结果,那可以做些什么事情呢 ? 就不言而喻了,比如:当需要通过前两个参数的结果去做一件事情,然后还需要使用最后的结果做一件事情,是不是就可以分别触发对应的函数了。