this指向

1.函数在调用时,JavaScript会默认给this绑定一个值;

2.this的绑定和定义的位置(编写的位置)没有关系;

3.this的绑定和调用方式以及调用的位置有关系;

4.this是在运行时被绑定的;

默认绑定

严格模式下, 独立调用的函数中的this指向的是undefined

 1    // "use strict"
 2
 3    // 定义函数
 4    // 1.普通的函数被独立的调用
 5    function foo() {
 6      console.log("foo:", this)
 7    }
 8    foo() // window
 9
10
11    // 2.函数定义在对象中, 但是独立调用
12    var obj = {
13      name: "why",
14      bar: function() {
15        console.log("bar:", this)
16      }
17    }
18    obj.bar() // {name: 'why', bar: ƒ}
19    var baz = obj.bar
20    baz() // window
21
22
23    // 3.高阶函数
24    function test(fn) {
25      fn()  // fn=obj.bar   fn()
26    }
27
28    test(obj.bar) // window
29
30    // 4.严格模式下, 独立调用的函数中的this指向的是undefined

隐式绑定

是通过某个对象发起的函数调用

 1    // 隐式绑定
 2    function foo() {
 3      console.log("foo函数:", this)
 4    }
 5    var obj = {
 6      bar: foo
 7    }
 8    obj.bar()  // obj {bar: ƒ}
 9    var bar = obj.bar
10    bar()  // window

new绑定

1.创建一个全新的对象;

2.这个新对象会被执行prototype连接;

3.这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成);

4.如果函数没有返回其他对象,表达式会返回这个新对象;

1    function foo() {
2      this.name = "why"
3      console.log("foo函数:", this)
4    }
5
6    new foo()

显示绑定 call,apply,bind

隐式绑定有一个前提条件:

  • 必须在调用的对象内部有一个对函数的引用(比如一个属性);
  • 如果没有这样的引用,在进行调用时,会报找不到该函数的错误;
  • 正是通过这个引用,间接的将this绑定到了这个对象上;

如果我们不希望在 对象内部 包含这个函数的引用,同时又希望在这个对象上进行强制调用,该怎么做呢?

  • JavaScript所有的函数都可以使用call和apply方法。
  • 第一个参数是相同的,要求传入一个对象; 这个对象的作用是什么呢?就是给this准备的。 在调用这个函数时,会将this绑定到这个传入的对象上。
  • 后面的参数,apply为数组,call为参数列表;
 1    // 显式绑定
 2    var obj = {
 3      name: "why"
 4    }
 5
 6    function foo() {
 7      console.log("foo函数:", this)
 8    }
 9
10    // 执行函数, 并且函数中的this指向obj对象
11    // obj.foo = foo
12    // obj.foo()
13    // 执行函数, 并且强制this就是obj对象
14    foo.call(obj)  //       foo函数: Object
15    foo.call(123)  //   foo函数: Number  包装类型
16    foo.call("abc") //   foo函数: String  包装类型
 1
 2    // call/apply
 3    function foo(name, age, height) {
 4      console.log("foo函数被调用:", this)
 5      console.log("打印参数:", name, age, height)
 6    }
 7
 8    // ()调用
 9    // foo("why", 18, 1.88)
10
11    // apply
12    // 第一个参数: 绑定this
13    // 第二个参数: 传入额外的实参, 以数组的形式
14    // foo.apply("apply", ["kobe", 30, 1.98])
15
16    // call
17    // 第一个参数: 绑定this
18    // 参数列表: 后续的参数以多参数的形式传递, 会作为实参
19    foo.call("call", "james", 25, 2.05)
 1
 2    function foo(name, age, height, address) {
 3      console.log("foo:", this)
 4      console.log("参数:", name, age, height, address)
 5    }
 6
 7    var obj = { name: "why" }
 8
 9    // 需求: 调用foo时, 总是绑定到obj对象身上(不希望obj对象身上有函数)
10    // 1.bind函数的基本使用
11    // var bar = foo.bind(obj)
12    // bar() // this -> obj
13
14    // 2.bind函数的其他参数(了解)
15    var bar = foo.bind(obj, "kobe", 18, 1.88)
16    bar("james") // 对应 address参数

内置函数绑定

 1    // 内置函数(第三方库): 根据一些经验
 2    // 1.定时器
 3    setTimeout(function() {
 4      console.log("定时器函数:", this) // window
 5    }, 1000)
 6
 7    // 2.按钮的点击监听
 8    var btnEl = document.querySelector("button")
 9    btnEl.onclick = function() {
10      console.log("btn的点击:", this) // btnEl对象
11    }
12
13    // btnEl.addEventListener("click", function() {
14    //   console.log("btn的点击:", this)  //btnEl对象
15    // })
16
17    // // 3.forEach
18    var names = ["abc", "cba", "nba"]
19    names.forEach(function(item) {
20      console.log("forEach:", this)
21    }, "aaaa") // 第二个参数 绑定对象  内置string对象  String{'aaaa'}

绑定优先级

 1
 2    // function foo() {
 3    //   console.log("foo:", this)
 4    // }
 5
 6    // 比较优先级:
 7
 8    // 1.显式绑定绑定的优先级高于隐式绑定
 9    // 1.1.测试一:apply高于默认绑定
10    // var obj = { foo: foo }
11    // obj.foo.apply("abc")
12    // obj.foo.call("abc")
13
14    // 1.2.测试二:bind高于默认绑定
15    // var bar = foo.bind("aaa")
16    // var obj = {
17    //   name: "why",
18    //   baz: bar
19    // }
20    // obj.baz()
21
22
23    // 2.new绑定优先级高于隐式绑定
24    // var obj = {
25    //   name: "why",
26    //   foo: function() {
27    //     console.log("foo:", this) // foo{}
28    //     console.log("foo:", this === obj) // false
29    //   }
30    // }
31    // new obj.foo()
32
33
34    // 3.new/显式
35    // 3.1. new不可以和apply/call一起使用
36
37    // 3.2. new优先级高于bind
38    // function foo() {
39    //   console.log("foo:", this) // foo{}
40    // }
41    // var bindFn = foo.bind("aaa")
42    // new bindFn()
43
44
45    // 4.bind/apply优先级
46    // bind优先级高于apply/call
47    function foo() {
48      console.log("foo:", this) // String{'aaa'}
49    }
50    var bindFn = foo.bind("aaa")
51    bindFn.call("bbb")

显式绑定null/undefined

1    function foo() {
2      console.log("foo:", this)
3    }
4    foo.apply("abc")
5    foo.apply(null) // window
6    foo.apply(undefined) //window

间接函数引用

 1    // 2.情况二: 间接函数引用
 2    var obj1 = {
 3      name: "obj1",
 4      foo: function() {
 5        console.log("foo:", this)
 6      }
 7    }
 8    var obj2 = {
 9      name: "obj2"
10    };
11
12    // {}[]()
13
14    // obj2.foo = obj1.foo
15    // obj2.foo() // {name: 'obj2', foo: ƒ}
16    (obj2.foo = obj1.foo)() // window , 赋值(obj2.foo = obj1.foo)的结果是foo函数

箭头函数

箭头函数是ES6之后增加的一种编写函数的方法,并且它比函数表达式要更加简洁:

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

(): 函数的参数

{}: 函数的执行体

代码块才会形成作用域, 对象里面没有作用域

 1
 2    // 1.之前的方式
 3    function foo1() {}
 4    var foo2 = function(name, age) {
 5      console.log("函数体代码", this, arguments)
 6      console.log(name, age)
 7    }
 8
 9    // 2.箭头函数完整写法
10    var foo3 = (name, age) => {
11      console.log("箭头函数的函数体")
12      console.log(name, age)
13    }
14
15    // 3.箭头函数的练习
16    // 3.1. forEach
17    var names = ["abc", "cba", "nba"]
18    names.forEach((item, index, arr) => {
19      console.log(item, index, arr) // arr = ["abc", "cba", "nba"]
20    })
21    // 3.2. setTimeout
22    setTimeout(() => {
23      console.log("setTimeout")
24    }, 3000)

箭头函数简写

 1
 2    // var names = ["abc", "cba", "nba"]
 3    // var nums = [20, 30, 11, 15, 111]
 4
 5    // 1.优化一: 如果箭头函数只有一个参数, 那么()可以省略
 6    // names.forEach(item => {
 7    //   console.log(item)
 8    // })
 9    // var newNums = nums.filter(item => {
10    //   return item % 2 === 0
11    // })
12
13    // 2.优化二: 如果函数体中只有一行执行代码, 那么{}可以省略
14    // names.forEach(item => console.log(item))
15
16    // 一行代码中不能带return关键字, 如果省略, 需要带return一起省略(下一条规则)
17    // var newNums = nums.filter(item => {
18    //   return item % 2 === 0
19    // })
20
21    // 3.优化三: 只有一行代码时, 这行代码的表达式结果会作为函数的返回值默认返回的
22    // var newNums = nums.filter(item => item % 2 === 0)
23    // var newNums = nums.filter(item => item % 2 === 0)
24
25
26    // 4.优化四: 如果默认返回值是一个对象, 那么这个对象必须加()
27    // 注意: 在react中我会经常使用 redux
28
29    // var arrFn = () => ["abc", "cba"]
30    // var arrFn = () => {} // 注意: 这里是{}执行体
31    // var arrFn = () => ({ name: "why" })
32    // console.log(arrFn())
33
34    // 箭头函数实现nums的所有偶数平方的和
35    var nums = [20, 30, 11, 15, 111]
36    var result = nums.filter(item => item % 2 === 0)
37                     .map(item => item * item)
38                     .reduce((prevValue, item) => prevValue + item)
39    console.log(result)

箭头函数没有this

是会找外层作用域的,本身箭头函数里面没有this

 1    // 1.普通函数中是有this的标识符
 2    // function foo() {
 3    //   console.log("foo", this)
 4    // }
 5
 6    // foo()
 7    // foo.apply("aaa")
 8
 9    // 2.箭头函数中, 压根没有this
10    // var bar = () => {
11    //   console.log("bar:", this) // 作用域找, window
12    // }
13    // bar()
14    // 通过apply调用时, 也是没有this
15    // bar.apply("aaaa")
16
17    // console.log("全局this:", this)
18    // var message = "global message"
19
20    // 3.this的查找规则
21    var obj = {
22      name: "obj",
23      foo: () => {
24        var bar = () => {
25          console.log("bar:", this) // 会找到最外层的作用域 window , 对象不是作用域, 代码块才有作用域
26        }
27        return bar
28
29      }
30    }
31    var fn = obj.foo()
32    fn.apply("bbb")

箭头函数this的应用

 1 // 网络请求的工具函数
 2    function request(url, callbackFn) {
 3      var results = ["abc", "cba", "nba"]
 4      callbackFn(results)
 5    }
 6
 7    // 实际操作的位置(业务)
 8    var obj = {
 9      names: [],
10      network: function() {
11        // 1.早期的时候
12        // var _this = this
13        // request("/names", function(res) {
14        //   _this.names = [].concat(res)
15        // })
16
17        // 2.箭头函数写法
18        request("/names", (res) => {
19          this.names = [].concat(res)
20        })
21      }
22    }
23
24    obj.network()
25    console.log(obj)

面试题

 1var name = "window";
 2
 3var person = {
 4  name: "person",
 5  sayName: function () {
 6    console.log(this.name);
 7  }
 8};
 9
10function sayName() {
11  var sss = person.sayName;
12
13  sss(); // 绑定: 默认绑定, window -> window
14
15  person.sayName(); // 绑定: 隐式绑定, person -> person
16
17  (person.sayName)(); // 绑定: 隐式绑定, person -> person
18
19  (b = person.sayName)(); // 术语: 间接函数引用, window -> window
20}
21
22sayName();
 1var name = 'window'
 2
 3
 4// {} -> 对象
 5// {} -> 代码块
 6var person1 = {
 7  name: 'person1',
 8  foo1: function () {
 9    console.log(this.name)
10  },
11  foo2: () => console.log(this.name),
12  foo3: function () {
13    return function () {
14      console.log(this.name)
15    }
16  },
17  foo4: function () {
18    // console.log(this) // 第一个表达式this -> person1
19    // console.log(this) // 第二个表达式this -> person2
20    // console.log(this) // 第三个表达式this -> person1
21    
22    return () => {
23      console.log(this.name)
24    }
25  }
26}
27
28var person2 = { name: 'person2' }
29
30
31// 开始题目:
32person1.foo1(); // 隐式绑定: person1
33person1.foo1.call(person2); // 显式绑定: person2
34
35person1.foo2(); // 上层作用域: window
36person1.foo2.call(person2); // 上层作用域: window
37
38person1.foo3()(); // 默认绑定: window
39person1.foo3.call(person2)(); // 默认绑定: window,  return 执行默认绑定,作用域近
40person1.foo3().call(person2); // 显式绑定: person2
41
42person1.foo4()(); // person1
43person1.foo4.call(person2)(); // person2
44person1.foo4().call(person2); // person1
 1var name = 'window'
 2
 3/*
 4  1.创建一个空的对象
 5  2.将这个空的对象赋值给this
 6  3.执行函数体中代码
 7  4.将这个新的对象默认返回
 8*/
 9function Person(name) {
10  this.name = name
11  this.foo1 = function () {
12    console.log(this.name)
13  },
14  this.foo2 = () => console.log(this.name),
15  this.foo3 = function () {
16    return function () {
17      console.log(this.name)
18    }
19  },
20  this.foo4 = function () {
21    return () => {
22      console.log(this.name)
23    }
24  }
25}
26
27// person1/person都是对象(实例instance)
28var person1 = new Person('person1')
29var person2 = new Person('person2')
30
31
32// 面试题目:
33person1.foo1() // 隐式绑定: person1
34person1.foo1.call(person2) // 显式绑定: person2
35
36person1.foo2() // 上层作用域查找: person1
37person1.foo2.call(person2) // 上层作用域查找: person1
38
39person1.foo3()() // 默认绑定: window
40person1.foo3.call(person2)() // 默认绑定: window
41person1.foo3().call(person2) // 显式绑定: person2
42
43person1.foo4()() // 上层作用域查找: person1(隐式绑定)
44person1.foo4.call(person2)() //  上层作用域查找: person2(显式绑定)
45person1.foo4().call(person2) // 上层作用域查找: person1(隐式绑定)
 1var name = 'window'
 2
 3/*
 4  1.创建一个空的对象
 5  2.将这个空的对象赋值给this
 6  3.执行函数体中代码
 7  4.将这个新的对象默认返回
 8*/
 9function Person(name) {
10  this.name = name
11  this.obj = {
12    name: 'obj',
13    foo1: function () {
14      return function () {
15        console.log(this.name)
16      }
17    },
18    foo2: function () {
19      return () => {
20        console.log(this.name)
21      }
22    }
23  }
24}
25
26var person1 = new Person('person1')
27var person2 = new Person('person2')
28
29person1.obj.foo1()() // 默认绑定: window
30person1.obj.foo1.call(person2)() // 默认绑定: window
31person1.obj.foo1().call(person2) // 显式绑定: person2
32
33person1.obj.foo2()() // 上层作用域查找: obj(隐式绑定)
34person1.obj.foo2.call(person2)() // 上层作用域查找: person2(显式绑定)
35person1.obj.foo2().call(person2) // 上层作用域查找: obj(隐式绑定)

浏览器原理

渲染页面原理

异步加载css,期间还是会执行dom tree过程 ,css加载完毕,

当有了DOM Tree和 CSSOM Tree后,就可以两个结合来构建Render Tree了

接着是在渲染树(Render Tree)上运行布局(Layout)以计算每个节点的几何体。

接着是将每个节点绘制(Paint)到屏幕

https://www.html5rocks.com/en/tutorials/internals/howbrowserswork

回流和重绘

理解回流reflow:

  • 第一次确定节点的大小和位置,称之为布局(layout)。
  • 之后对节点的大小、位置修改重新计算称之为回流。

什么情况下引起回流呢?

  • 比如DOM结构发生改变(添加新的节点或者移除节点);
  • 比如改变了布局(修改了width、height、padding、font-size等值)
  • 比如窗口resize(修改了窗口的尺寸等)
  • 比如调用getComputedStyle方法获取尺寸、位置信息;

理解重绘repaint

  • 第一次渲染内容称之为绘制(paint)。 之后重新渲染称之为重绘。

什么情况下会引起重绘呢?

  • 比如修改背景色、文字颜色、边框颜色、样式等;

回流一定会引起重绘,所以回流是一件很消耗性能的事情。

在开发中要尽量避免发生回流

1.修改样式时尽量一次性修改

比如通过cssText修改,比如通过添加class修改

2.尽量避免频繁的操作DOM

我们可以在一个DocumentFragment或者父元素中将要操作的DOM操作完成,再一次性的操作;

3.尽量避免通过getComputedStyle获取尺寸、位置等信息;

4.对某些元素使用position的absolute或者fixed并不是不会引起回流,而是开销相对较小,不会对其他元素造成影响。

合成

绘制的过程,可以将布局后的元素绘制到多个合成图层中。 这是浏览器的一种优化手段;

默认情况下,标准流中的内容都是被绘制在同一个图层(Layer)中的;

而一些特殊的属性,会创建一个新的合成层( CompositingLayer ),并且新的图层可以利用GPU来加速绘制; 因为每个合成层都是单独渲染的;

那么哪些属性可以形成新的合成层呢?常见的一些属性:

  • 3D transforms
  • video、canvas、iframe
  • opacity 动画转换时;
  • position: fixed
  • will-change:一个实验性的属性,提前告诉浏览器元素可能发生哪些变化;
  • animation 或 transition 设置了opacity、transform;

分层确实可以提高性能,但是它以内存管理为代价,因此不应作为 web 性能优化策略的一部分过度使用

script元素和页面解析的关系

浏览器在解析HTML的过程中,遇到了script元素是不能继续构建DOM树的; 它会停止继续构建,首先下载JavaScript代码,并且执行JavaScript的脚本;

只有等到JavaScript脚本执行结束后,才会继续解析HTML,构建DOM树

为什么要这样做呢?

这是因为JavaScript的作用之一就是操作DOM,并且可以修改DOM;

如果我们等到DOM树构建完成并且渲染再执行JavaScript,会造成严重的回流和重绘,影响页面的性能;

所以会在遇到script元素时,优先下载和执行JavaScript代码,再继续构建DOM树;

在目前的开发模式中(比如Vue、React),脚本往往比HTML页面更“重”,处理时间需要更长;

所以会造成页面的解析阻塞,在脚本下载、执行完成之前,用户在界面上什么都看不到;

为了解决这个问题,script元素给我们提供了两个属性(attribute):defer和async。

defer属性

defer 属性告诉浏览器不要等待脚本下载,而继续解析HTML,构建DOM Tree。

脚本会由浏览器来进行下载,但是不会阻塞DOM Tree的构建过程;

如果脚本提前下载好了,它会等待DOM Tree构建完成,在DOMContentLoaded事件之前先执行defer中的代码;

所以DOMContentLoaded总是会等待defer中的代码先执行完成。

  • 另外多个带defer的脚本是可以保持正确的顺序执行的。
  • 从某种角度来说,defer可以提高页面的性能,并且推荐放到head元素中;
  • 注意:defer仅适用于外部脚本,对于script默认内容会被忽略。
1<script src="./js/test.js" defer></script>
2<!-- 1.下载需要很长的事件, 并且执行也需要很长的时间 -->
3<!-- 总结一: 加上defer之后, js文件的下载和执行, 不会影响后面的DOM Tree的构建 -->
4<script>
5// 总结三: defer代码是在DOMContentLoaded事件发出之前执行
6window.addEventListener("DOMContentLoaded", () => {
7console.log("DOMContentLoaded")
8})
9</script>

async属性

async 特性与 defer 有些类似,它也能够让脚本不阻塞页面。

  • async是让一个脚本完全独立的:

  • 浏览器不会因 async 脚本而阻塞(与 defer 类似);

  • async脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本;

  • async不会能保证在DOMContentLoaded之前或者之后执行;

  • defer通常用于需要在文档解析后操作DOM的JavaScript代码,并且对多个script文件有顺序要求的;

  • async通常用于独立的脚本,对其他脚本,甚至DOM没有依赖的;

js中有操作demo 用defer

1  <script src="./js/test.js" async></script>

js运行原理

初始化全局对象

js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)

  • 该对象 所有的作用域(scope)都可以访问;
  • 里面会包含Date、Array、String、Number、setTimeout、setInterval等等;
  • 其中还有一个window属性指向自己;

执行上下文

Execution Contexts

js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈. 跟go 的差不多

先执行的是全局的代码块:

  • 全局的代码块为了执行会构建一个 Global Execution Context(GEC)
  • GEC会 被放入到ECS中 执行;

GEC被放入到ECS中里面包含两部分内容

  • 在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值;这个过程也称之为变量的作用域提升(hoisting)
  • 在代码执行中,对变量赋值,或者执行其他的函数;

vo对象

Variable Object

每一个执行上下文会关联一个VO(Variable Object,变量对象),变量和函数声明会被添加到这个VO对象中

当全局代码被执行的时候,VO就是GO对象了

函数如何被执行

在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称FEC), 且压入到EC Stack中。

因为每个执行上下文都会关联一个VO,那么函数执行上下文关联的VO是什么呢?

  • 当进入一个函数执行上下文时,会创建一个AO对象(Activation Object);
  • 这个AO对象会使用arguments作为初始化,并且初始值是传入的参数;
  • 这个AO对象会作为执行上下文的VO来存放变量的初始化;

函数中的变量,也有作用域提升的概念, ao对象

1    // 1.函数中有自己的message
2     var message = "Global Message"
3     function foo() {
4        console.log(message)  // undefined
5        var message = "foo message"
6     }
7    foo()

作用域和作用域链

当进入到一个执行上下文时,执行上下文也会关联一个作用域链(Scope Chain)

  • 作用域链是一个对象列表,用于变量标识符的求值;
  • 当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象;

定义的时候就确定作用域链了。

 1    // 2.函数中没有自己的message
 2    var message = "Global Message"
 3
 4    debugger
 5
 6    //  global 作用域链  。 定义的时候就确定作用域链了。先找自己的ao对象的属性,没有则找scope 链表一次遍历。
 7    function foo() {
 8      console.log(message)
 9    }
10
11    foo()
12
13
14    var obj = {
15      name: "obj",
16      bar: function() {
17        var message = "bar message"
18        foo() // message 也是 Global Messag
19      }
20    }
21
22    obj.bar()

闭包

在JavaScript中,函数是非常重要的,并且是一等公民:

闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表);

从广义的角度来说:JavaScript中的函数都是闭包;

从狭义的角度来说:JavaScript中一个函数,如果访问了外层作用域的变量,那么它是一个闭包;

闭包因为有作用域的关系,他的内存不会释放掉。

一些后面不用的,可以手动=null 进行释放。