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 进行释放。