JavaScript高级 1、作用域 1.1 局部作用域 作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问,
作用域分为:
(1)局部作用域
(2)全局作用域
局部作用域分为函数作用域和块作用域。
(1) 函数作用域
在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。
1 2 3 4 5 6 总结: 1. 函数内部声明的变量,在函数外部无法被访问 2. 函数的参数也是函数内部的局部变量 3. 不同函数内部声明的变量无法互相访问 4. 函数执行完毕后,函数内部的变量实际被清空了
(2)块级作用域
在 JavaScript 中使用 { } 包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】无法被访问
1 2 3 4 5 6 总结: 1. let 声明的变量会产生块作用域,var 不会产生块作用域 2. const 声明的常量也会产生块作用域 3. 不同代码块之间的变量无法互相访问 4. 推荐使用 let 或 const
1.2 全局作用域 <script>
标签 和.js
文件 的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。
全局作用域中声明的变量,任何其它作用域都可以被访问
1 2 3 4 5 注意: 1. 为 window 对象动态添加的属性默认也是全局的,不推荐! 2. 函数中未使用任何关键字声明的变量为全局变量,不推荐!!! 3. 尽可能少的声明全局变量,防止全局变量被污染
1.3 作用域链 下面代码的执行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <script> let a = 1 ; let b = 2 ; function f ( ){ lets a = 1 ; function g ( ){ a = 2 ; console .log (a); } g (); } f ();</script>
1 2 3 4 作用域链本质上是底层的变量查找机制。 在函数被执行时,会优先查找当前函数作用域中查找变量 如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域
1 2 3 4 5 总结: 1. 嵌套关系的作用域串联起来形成了作用域链 2. 相同作用域链中按着从小到大的规则查找变量 3. 子作用域能够访问父作用域,父级作用域无法访问子级作用域
1 2 3 4 作用域链查找的规则是什么? 会优先查找当前函数作用域中查找变量 查找不到则会依次逐级查找父级作用域直到全局作用域
1.4 闭包 概念:一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域
简单理解:闭包 = 内层函数 + 外层函数的变量
先看个简单的代码:
1 2 3 4 5 6 7 8 function outer ( ){ const a = 1 function f ( ){ console .log (a); } f () } outer ()
1 2 3 4 5 6 7 8 9 10 function outer ( ) { let a = 100 function fn ( ) { console .log (a) } return fn } const fun = outer () fun ()
以上代码,经常被简化成如下的形式:
1 2 3 4 5 6 7 8 function outer ( ) { let a = 100 return function ( ) { console .log (a) } } const fun = outer () fun ()
闭包应用:实现数据的私有
比如,我们要做个统计函数调用次数,函数调用一次,就++
大家可能想到的就是通过全局变量来实现
1 2 3 4 5 6 7 let i = 1 function fn ( ) { i++ console .log (`函数被调用了${i} 次` ) } fn ()fn ()
但是,这个i
是一个全局变量,很容易被修改。
1 2 3 4 5 6 7 8 9 10 11 function count ( ) { let i = 0 function fn ( ) { i++ console .log (`函数被调用了${i} 次` ) } return fn } const fun = count () fun ()
这样通过闭包的形式实现了数据的私有,无法直接修改count
1.5 变量提升 变量提升是 JavaScript
中比较“奇怪”的现象,它允许在变量声明之前即被访问(仅存在于var
声明变量)
1 2 3 4 5 6 7 注意: 1. 变量在未声明即被访问时会报语法错误 2. 变量在var声明之前即被访问,变量的值为 undefined 3. let/const 声明的变量不存在变量提升 4. 变量提升出现在相同作用域当中 5. 实际开发中推荐先声明再访问变量
1 2 3 4 5 6 7 8 9 10 <script> console .log (str +"world" ) var str = "hello" var str; console .log (str +"world" ) str = "hello" <script>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <script> function fn ( ) { console .log (num) var num = 10 } fn () </script>
2、函数进阶 2.1 函数提升 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <script> var fun </script>
2.2 函数参数 2.2.1 动态参数 产品需求: 写一个求和函数
不管用户传入几个实参,都要把和求出来
实参:
问题是形参应该怎样写?
arguments
是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <script> function getSum ( ) { let sum = 0 for (let i = 0 ; i < arguments .length ; i++) { sum += arguments [i] } console .log (sum) } getSum (2 , 3 , 4 ) getSum (1 , 2 , 3 , 4 , 2 , 2 , 3 , 4 ) </script>
1 2 3 4 5 总结: arguments 是一个伪数组,只存在于函数中 arguments 的作用是动态获取函数的实参 可以通过for循环依次得到传递过来的实参
2.2.2 剩余参数 产品需求: 写一个求和函数
不管用户传入几个实参,都要把和求出来
实参:
形参应该怎样写?
剩余参数允许我们将一个不定数量的参数表示为一个数组
1 2 3 4 5 6 7 <script> function getSum (a, b, ...arr ) { console .log (arr) } getSum (2 , 3 ) getSum (1 , 2 , 3 , 4 , 5 ) </script>
问题是与arguments
有什么不同?
1 2 3 1. ... 是语法符号,置于最末函数形参之前,用于获取多余的实参 2. 借助 ... 获取的剩余实参,是个真数组
开发中提倡多使用剩余参数
2.2.3 展开运算符 与剩余参数数书写类似的是展开运算符。
展开运算符(…),将一个数组进行展开
典型运用场景: 求数组最大值 **(最小值 )**、合并数组等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <script> const arr1 = [1 , 2 , 3 ] console .log (Math .max (...arr1)) console .log (Math .min (...arr1)) const arr2 = [3 , 4 , 5 ] const arr = [...arr1, ...arr2] console .log (arr) </script>
说明:展开运算符不会修改原有的数组
2.3 箭头函数 2.3.1 基本语法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <script> </script>
2.3.2 箭头函数参数 1.普通函数有arguments 动态参数
2.箭头函数没有 arguments 动态参数 ,但是有 剩余参数 ..args
1 2 3 4 5 6 7 8 9 10 11 12 <script> const getSum = (...arr ) => { let sum = 0 for (let i = 0 ; i < arr.length ; i++) { sum += arr[i] } return sum } const result = getSum (2 , 3 , 4 ) console .log (result) </script>
2.3.3 箭头函数this
引入箭头函数的目的是更简短的函数写法并且不绑定this
,箭头函数的语法比函数表达式更简洁
箭头函数更适用于那些本来需要匿名函数的地方
在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的this值, 非常令人讨厌。
KilgourNote
箭头函数中的this指的是定义箭头函数时的上下文
例一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const btn = document .querySelector ("#p" );btn.addEventListener ("click" , f); function f ( ) { let obj2 = { userName : "zhangsan" , sayHi : () => { console .log (this ); }, }; obj2.sayHi (); }
例二
1 2 3 4 5 6 7 8 9 10 11 12 13 function outer ( ) { return () => { console .log (this ); }; } let obj = { method : outer, }; let arrowFunction = obj.method ();arrowFunction ();
例三
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function traditionalFunction ( ) { console .log (this ); } const arrowFunction = ( ) => { console .log (this ); }; traditionalFunction (); arrowFunction (); const obj = { traditionalMethod : traditionalFunction, arrowMethod : arrowFunction, }; obj.traditionalMethod (); obj.arrowMethod ();
箭头函数不会创建自己的this
,它只会从自己的作用域链的上一层沿用this。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <script> const obj = { uname : '张三' , sayHi : function ( ) { console .log (this ) let i = 10 const count = ( ) => { console .log (this ) } count () } } obj.sayHi () </script>
在开发中【使用箭头函数前需要考虑函数中 this 的值】,事件回调函数使用箭头函数时,this 为全局的 window,因此DOM事件回调函数为了简便,还是不太推荐使用箭头函数
3、解构赋值 解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值。
分为:
数组解构
对象解构
3.1 数组解构 1 2 3 4 基本语法: 1. 赋值运算符 = 左侧的 [] 用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量 2. 变量的顺序对应数组单元值的位置依次进行赋值操作
以上要么不好记忆,要么书写麻烦,此时可以使用解构赋值的方法让代码更简洁
1 2 3 4 5 let a = 1 let b = 2 ; [b, a] = [a, b] console .log (a, b)
变量多 单元值少的情况:
1 2 变量的数量大于单元值数量时,多余的变量将被赋值为 undefined
变量少 单元值多的情况:
利用剩余参数解决变量少 单元值多的情况:
剩余参数返回的还是一个数组
防止有undefined传递单元值的情况,可以设置默认值:
按需导入,忽略某些返回值:
3.2 对象解构 1.基本语法:
1 2 3 4 5 1. 赋值运算符 = 左侧的 {} 用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量 2. 对象属性的值将被赋值给与属性名相同的变量 3. 注意解构的变量名不要和外面的变量名冲突否则报错 4.对象中找不到与变量名一致的属性时变量值为 undefined
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const obj = { uname : '张三' , age : 18 } const { uname, age } = {age : 18 , uname : '张老师' } console .log (uname) console .log (age)
可以从一个对象中提取变量并同时修改新的变量名
1 2 3 4 5 6 const { uname : username, age } = { uname : '张三' , age : 18 } console .log (username) console .log (age)
数组对象解构
1 2 3 4 5 6 7 8 9 10 const pig = [ { uname : '佩奇' , age : 6 } ] const [{ uname, age }] = pig console .log (uname) console .log (age)
多级对象解构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <script> const person = [ { name : '佩奇' , family : { mother : '猪妈妈' , father : '猪爸爸' , sister : '乔治' }, age : 6 } ] const [{ name, family : { mother, father, sister } }] = person console .log (name) console .log (mother) console .log (father) console .log (sister) </script>
下面我们看一个以后经常使用的案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 <script> const msg = { "code" : 200 , "msg" : "获取新闻列表成功" , "data" : [ { "id" : 1 , "title" : "5G商用自己,三大运用商收入下降" , "count" : 58 }, { "id" : 2 , "title" : "国际媒体头条速览" , "count" : 56 }, { "id" : 3 , "title" : "乌克兰和俄罗斯持续冲突" , "count" : 1669 }, ] } function render ({ data } ) { console .log (data) } render (msg) function render ({ data: myData } ) { console .log (myData) } render (msg) </script>
3.3 遍历数组 forEach
方法 forEach()
方法用于调用数组的每个元素,并将元素传递给回调函数
主要使用场景: 遍历数组的每个元素
语法:
1 2 3 4 5 6 7 8 9 <script> const arr = ['red' , 'green' , 'black' ] const result = arr.forEach (function (item, index ) { console .log (item) console .log (index) }) </script>
1 2 3 注意: forEach 主要是遍历数组 参数当前数组元素是必须要写的, 索引号可选
商品列表案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > 商品渲染</title > <style > * { margin : 0 ; padding : 0 ; box-sizing : border-box; } .list { width : 990px ; margin : 0 auto; display : flex; flex-wrap : wrap; padding-top : 100px ; } .item { width : 240px ; margin-left : 10px ; padding : 20px 30px ; transition : all .5s ; margin-bottom : 20px ; } .item :nth-child (4 n) { margin-left : 0 ; } .item :hover { box-shadow : 0px 0px 5px rgba (0 , 0 , 0 , 0.2 ); transform : translate3d (0 , -4px , 0 ); cursor : pointer; } .item img { width : 100% ; } .item .name { font-size : 18px ; margin-bottom : 10px ; color : #666 ; } .item .price { font-size : 22px ; color : firebrick; } .item .price ::before { content : "¥" ; font-size : 14px ; } </style > </head > <body > <div class ="list" > </div > <script > const goodsList = [ { id : '4001172' , name : '称心如意手摇咖啡磨豆机咖啡豆研磨机' , price : '289.00' , picture : 'https://yanxuan-item.nosdn.127.net/84a59ff9c58a77032564e61f716846d6.jpg' , }, { id : '4001594' , name : '日式黑陶功夫茶组双侧把茶具礼盒装' , price : '288.00' , picture : 'https://yanxuan-item.nosdn.127.net/3346b7b92f9563c7a7e24c7ead883f18.jpg' , }, { id : '4001009' , name : '竹制干泡茶盘正方形沥水茶台品茶盘' , price : '109.00' , picture : 'https://yanxuan-item.nosdn.127.net/2d942d6bc94f1e230763e1a5a3b379e1.png' , }, { id : '4001874' , name : '古法温酒汝瓷酒具套装白酒杯莲花温酒器' , price : '488.00' , picture : 'https://yanxuan-item.nosdn.127.net/44e51622800e4fceb6bee8e616da85fd.png' , }, { id : '4001649' , name : '大师监制龙泉青瓷茶叶罐' , price : '139.00' , picture : 'https://yanxuan-item.nosdn.127.net/4356c9fc150753775fe56b465314f1eb.png' , }, { id : '3997185' , name : '与众不同的口感汝瓷白酒杯套组1壶4杯' , price : '108.00' , picture : 'https://yanxuan-item.nosdn.127.net/8e21c794dfd3a4e8573273ddae50bce2.jpg' , }, { id : '3997403' , name : '手工吹制更厚实白酒杯壶套装6壶6杯' , price : '99.00' , picture : 'https://yanxuan-item.nosdn.127.net/af2371a65f60bce152a61fc22745ff3f.jpg' , }, { id : '3998274' , name : '德国百年工艺高端水晶玻璃红酒杯2支装' , price : '139.00' , picture : 'https://yanxuan-item.nosdn.127.net/8896b897b3ec6639bbd1134d66b9715c.jpg' , }, ] let str = '' goodsList.forEach (item => { const { name, price, picture } = item str += ` <div class="item"> <img src=${picture} alt=""> <p class="name">${name} </p> <p class="price">${price} </p> </div> ` }) document .querySelector ('.list' ).innerHTML = str </script > </body > </html >
3.4 筛选数组 filter 方法 KilgourNote:(数组方法总结)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <script> const arr = [1 , 2 , 3 , 4 , 5 ]; const a = arr.map ((it ) => it > 2 ); console .log (a); const b = arr.map ((it ) => it * 2 ); console .log (b); const c = arr.find ((it ) => it == 3 ); console .log (c); const d = arr.join ("_" ); console .log (d); const e = arr.reduce ((sum, it ) => sum + it, 0 ); console .log (e); const array = arr.slice (); array.forEach ((num, index, array ) => (array[index] = num + 1 )); array.forEach (function (item, index, array ) { array[index] = item + 1 ; }); console .log (array); const g = arr.filter ((item ) => item > 2 ); console .log (g); </script>
filter()
方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素
主要使用场景: 筛选数组符合条件的元素 ,并返回筛选之后元素的新数组
语法:
1 2 3 4 5 6 7 8 9 10 11 12 <script> const arr = [10 , 20 , 30 ] const newArr = arr.filter (item => item >= 20 ) console .log (newArr) </script>
说明:
1 2 3 返回值:返回数组,包含了符合条件的所有元素。如果没有符合条件的元素则返回空数组 参数:currentValue 必须写, index 可选 因为返回新数组,所以不会影响原数组
价格筛选案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>商品渲染</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } .list { width: 990px; margin: 0 auto; display: flex; flex-wrap: wrap; } .item { width: 240px; margin-left: 10px; padding: 20px 30px; transition: all .5s; margin-bottom: 20px; } .item:nth-child(4n) { margin-left: 0; } .item:hover { box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2); transform: translate3d(0, -4px, 0); cursor: pointer; } .item img { width: 100%; } .item .name { font-size: 18px; margin-bottom: 10px; color: #666; } .item .price { font-size: 22px; color: firebrick; } .item .price::before { content: "¥"; font-size: 14px; } .filter { display: flex; width: 990px; margin: 0 auto; padding: 50px 30px; } .filter a { padding: 10px 20px; background: #f5f5f5; color: #666; text-decoration: none; margin-right: 20px; } .filter a:active, .filter a:focus { background: #05943c; color: #fff; } </style> </head> <body> <div class="filter"> <a data-index="1" href="javascript:;">0-100元</a> <a data-index="2" href="javascript:;">100-300元</a> <a data-index="3" href="javascript:;">300元以上</a> <a href="javascript:;">全部区间</a> </div> <div class="list"> <!-- <div class="item"> <img src="" alt=""> <p class="name"></p> <p class="price"></p> </div> --> </div> <script> // 2. 初始化数据 const goodsList = [ { id: '4001172', name: '称心如意手摇咖啡磨豆机咖啡豆研磨机', price: '289.00', picture: 'https://yanxuan-item.nosdn.127.net/84a59ff9c58a77032564e61f716846d6.jpg', }, { id: '4001594', name: '日式黑陶功夫茶组双侧把茶具礼盒装', price: '288.00', picture: 'https://yanxuan-item.nosdn.127.net/3346b7b92f9563c7a7e24c7ead883f18.jpg', }, { id: '4001009', name: '竹制干泡茶盘正方形沥水茶台品茶盘', price: '109.00', picture: 'https://yanxuan-item.nosdn.127.net/2d942d6bc94f1e230763e1a5a3b379e1.png', }, { id: '4001874', name: '古法温酒汝瓷酒具套装白酒杯莲花温酒器', price: '488.00', picture: 'https://yanxuan-item.nosdn.127.net/44e51622800e4fceb6bee8e616da85fd.png', }, { id: '4001649', name: '大师监制龙泉青瓷茶叶罐', price: '139.00', picture: 'https://yanxuan-item.nosdn.127.net/4356c9fc150753775fe56b465314f1eb.png', }, { id: '3997185', name: '与众不同的口感汝瓷白酒杯套组1壶4杯', price: '108.00', picture: 'https://yanxuan-item.nosdn.127.net/8e21c794dfd3a4e8573273ddae50bce2.jpg', }, { id: '3997403', name: '手工吹制更厚实白酒杯壶套装6壶6杯', price: '100.00', picture: 'https://yanxuan-item.nosdn.127.net/af2371a65f60bce152a61fc22745ff3f.jpg', }, { id: '3998274', name: '德国百年工艺高端水晶玻璃红酒杯2支装', price: '139.00', picture: 'https://yanxuan-item.nosdn.127.net/8896b897b3ec6639bbd1134d66b9715c.jpg', }, ] // 1. 渲染函数 封装 function render(arr) { // 声明空字符串 let str = '' // 遍历数组 arr.forEach(item => { // 解构 const { name, picture, price } = item str += ` <div class="item"> <img src=${picture} alt=""> <p class="name">${name}</p> <p class="price">${price}</p> </div> ` }) // 追加给list document.querySelector('.list').innerHTML = str } render(goodsList) // 页面一打开就需要渲染 // 2. 过滤筛选 document.querySelector('.filter').addEventListener('click', e => { // e.target.dataset.index e.target.tagName const { tagName, dataset } = e.target // 判断 if (tagName === 'A') { // console.log(11) // arr 返回的新数组 let arr = goodsList // 如果不单击价格按钮,默认展示原有goodsList数组中的内容 if (dataset.index === '1') { arr = goodsList.filter(item => item.price > 0 && item.price <= 100) } else if (dataset.index === '2') { arr = goodsList.filter(item => item.price >= 100 && item.price <= 300) } else if (dataset.index === '3') { arr = goodsList.filter(item => item.price >= 300) } // 渲染函数 render(arr) } }) </script> </body> </html>
4、对象深入 4.1 创建对象三种方式 1 2 3 4 const person = { username :'zhangsan' }
2.利用 new Object 创建对象
1 2 3 4 5 const obj = new Object ({ uname : '张三' }) console .log (obj)
3.利用构造函数创建对象
构造函数 : 是一种特殊的函数,主要用来初始化对象
使用场景: 常规的 {…} 语法允许创建一个对象。比如我们创建了佩奇的对象,继续创建乔治的对象还需要重新写一遍,此时可以通过构造函数来快速创建多个类似的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function Pig (uname, age ) { this .uname = uname this .age = age } const p = new Pig ('佩奇' , 6 ) console .log (p) function Goods (name, price, count ) { this .name = name this .price = price this .count = count this .sayhi = function ( ) { } } const mi = new Goods ('小米' , 1999 , 20 ) console .log (mi) const hw = new Goods ('华为' , 3999 , 59 ) console .log (hw) console .log (mi === hw) mi.name = 'vivo' console .log (mi) console .log (hw)
1 2 3 4 5 构造函数在技术上是常规函数。 不过有两个约定: 它们的命名以大写字母开头。 它们只能由 "new" 操作符来执行。
补充说明:
1 2 3 4 5 6 7 说明: 1. 使用 new 关键字调用函数的行为被称为实例化 2. 实例化构造函数时没有参数时可以省略 () 3. 构造函数内部无需写return,返回值即为新创建的对象 4. 构造函数内部的 return 返回的值无效,所以不要写return 5. new Object() new Date() 也是实例化构造函数
4.2 实例成员&静态成员 实例成员
通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员。
静态成员:
构造函数的属性和方法被称为静态成员
1 2 3 4 5 说明: 1. 构造函数的属性和方法被称为静态成员 2. 一般公共特征的属性或方法静态成员设置为静态成员 3. 静态成员方法中的 this 指向构造函数本身
4.3 内置构造函数 4.3.1 Object
1 2 3 4 5 在 JavaScript 中最主要的数据类型有 6 种: 基本数据类型: 字符串、数值、布尔、undefined、null 引用类型: 对象
但是,我们会发现有些特殊情况:
1 2 const str = "zhangsan" console .log (str.length );
其实字符串、数值、布尔、等基本类型也都有专门的构造函数,这些我们称为包装类型。
JS
中几乎所有的数据都可以基于构成函数创建。
1 2 3 4 5 6 7 8 9 <script> </script>
1 2 3 4 5 引用类型 Object,Array,RegExp,Date 等 包装类型 String,Number,Boolean
Object
是内置的构造函数,用于创建普通对象。
1 2 const obj = new Object ({ uname : '张三' }) console .log (obj)
但是这种方式不常用
但是在Object
中有三个常用的静态方法。(静态方法就是只有构造函数Object可以调用的)
Object.keys
静态方法获取对象中所有属性(键)
1 2 3 4 5 6 const o = { name :'zhangsan' ,age :18 }for (let k in o ){ console .log (k) console .log (o[k]) }
以前是通过for..in
来实现,但是现在有了更加简单的方法,就是通过Object.keys
方法和Object.values
方法
现在先看一下Object.keys
方法
1 2 3 4 const o = { name :'zhangsan' ,age :18 }const arr = Object .keys (o)console .log (arr)
Object.values
静态方法获取对象中所有属性值
1 2 3 4 const o = { name :'zhangsan' ,age :18 }const arr = Object .values (o)console .log (arr)
Object. assign
静态方法常用于对象拷贝
1 2 3 4 const o = { name :'zhangsan' ,age :18 }const obj = {}Object .assign (obj,o)console .log (obj)
使用: 经常使用的场景给对象添加属性
1 2 3 const o = { name :'zhangsan' ,age :18 }Object .assign (o,{address :'北京' })console .log (o)
4.3.2 Array
Array
是内置的构造函数,用于创建数组
1 2 3 const arr1 = [];const arr = new Array (3 ,5 ) console .log (arr)
这种方式不常用
数组常见实例方法-核心方法
作用: reduce 返回函数累计处理的结果,经常用于求和等
基本语法
1 arr.reduce (function (累计值, 当前元素 ){}, 起始值)
起始值可以省略,如果写就作为第一次累计的起始值
1 2 3 4 5 6 const arr = [1 , 2 , 3 ]const result = arr.reduce (function (prev, item ) { console .log (prev) return prev + item }, 0 ) console .log ("result =" ,result);
1 2 3 4 arr.reduce (function (prev, item ) { console .log (prev) return prev + item })
可以简化成箭头函数的形式:
1 2 3 const arr = [1 , 2 , 3 ] const re = arr.reduce ((prev, item ) => prev + item) console .log (re)
1 2 3 4 5 关于累计值参数说明: 1. 如果有起始值,则以起始值为准开始累计, 累计值 = 起始值 2. 如果没有起始值, 则累计值以数组的第一个数组元素作为起始值开始累计 3. 后面每次遍历就会用后面的数组元素 累计到 累计值 里面 (类似求和里面的 sum )
使用场景:求和运算
案例:员工涨薪计算成本
需求:
(1):给员工每人涨薪 30%
(2):然后计算需要多支出的费用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <body> <script > const arr = [{ name : '张三' , salary : 10000 }, { name : '李四' , salary : 10000 }, { name : '王五' , salary : 20000 }, ] const money = arr.reduce ((prev, item ) => prev + item.salary * 0.3 , 0 ) console .log (money) </script > </body>
数组中常见的其他方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <script> const arr = [ { name : '小米' , price : 1999 }, { name : '华为' , price : 3999 }, ] const arr1 = [10 , 20 , 30 ] const flag = arr1.every (item => item >= 20 ) console .log (flag) </script>
4.3.3 String
常见实例方法
案例:
案例数据:
最终展示效果:
1 2 3 4 5 6 思路: ①:把字符串拆分为数组,这样两个赠品就拆分开了 用那个方法? ②:利用map遍历数组,同时把数组元素生成到span里面,并且返回 ③:因为返回的是数组,所以需要 转换为字符串, 用那个方法? ④:p的innerHTML 存放刚才的返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <body> <div > </div > <script > const gift = '50g的茶叶,清洗球' document .querySelector ('div' ).innerHTML = gift.split (',' ).map (item => `<span>【赠品】 ${item} </span> <br>` ).join ('' ) </script > </body >
4.4 原型 KilgourNote:
JavaScript中将一些对象中的公共元素提取出来单独放置于一个对象中作为公共对象继承。这个公共对象在JavaScript中被称为原型对象。
继承原型对象的对象通过.prototype
引用公共对象,公共对象中又一个constructor
属性指向继承公共对象的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 function Person ( ) { this .eyes = 2 ; this .head = 1 ; } function Woman ( ) {}Woman .prototype = new Person (); Woman .prototype .constructor = Woman ;Woman .prototype .baby = function ( ) { console .log ("宝贝" ); }; function Man ( ) {}Man .prototype = new Person ();Man .prototype .constructor = Man ;Man .prototype .Somking = function ( ) { console .log ("抽烟" ); }; const wm = new Woman ();console .log (wm);console .log (wm.__proto__ );const m = new Man ();console .log (m);
封装是面向对象思想中比较重要的一部分,js
面向对象可以通过构造函数实现的封装。
前面我们学过的构造函数方法很好用,但是 存在浪费内存 的问题
1 2 3 4 5 6 7 8 9 10 function Star (uname, age ) { this .uname = uname this .age = age this .sing = function ( ) { console .log ('我会唱歌' ) } } const ldh = new Star ('刘德华' , 18 )const zxy = new Star ('张学友' , 19 )
sing
方法是公共的。
我们希望所有的对象使用同一个函数,这样就比较节省内存,那么我们要怎样做呢?
通过原型来实现,原型本质就是一个对象 ,我们也称prototype
为原型对象
1 2 3 4 5 6 1、构造函数通过原型分配的函数是所有对象所 共享的。 2、JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象,所以我们也称为原型对象 3、这个对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存 4、我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。 5、构造函数和原型对象中的this 都指向 实例化的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <script> function Star (uname, age ) { this .uname = uname this .age = age } Star .prototype .sing = function ( ) { console .log ('唱歌' ) } const ldh = new Star ('刘德华' , 55 ) const zxy = new Star ('张学友' , 58 ) ldh.sing () zxy.sing () console .log (ldh.sing === zxy.sing ) </script>
4.4.1 this指向 构造函数和原型对象中的this 都指向 实例化的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <script> let that function Star (uname ) { this .uname = uname } Star .prototype .sing = function ( ) { that = this console .log ('唱歌' ) } const ldh = new Star ('刘德华' ) ldh.sing () console .log (that === ldh) </script>
构造函数和原型里面的this指向谁?
指向的是实例化的对象
4.4.2 给数组扩展方法 给数组扩展求最大值方法和求和方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <script> const arr = [1 , 2 , 3 ] Array .prototype .max = function ( ) { return Math .max (...this ) } Array .prototype .min = function ( ) { return Math .min (...this ) } console .log (arr.max ()) console .log ([2 , 5 , 9 ].max ()) console .log (arr.min ()) Array .prototype .sum = function ( ) { return this .reduce ((prev, item ) => prev + item, 0 ) } console .log ([1 , 2 , 3 ].sum ()) console .log ([11 , 21 , 31 ].sum ()) </script>
4.4.3 constructor 属性 每个原型对象里面都有个constructor 属性(constructor 构造函数)
作用: 该属性指向该原型对象的构造函数, 简单理解,就是指向我的爸爸,我是有爸爸的孩子
1 2 3 4 使用场景: 如果有多个对象的方法,我们可以给原型对象采取对象形式赋值. 但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了 此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <script> function Star ( ) { } Star .prototype = { constructor : Star , sing : function ( ) { console .log ('唱歌' ) }, dance : function ( ) { console .log ('跳舞' ) }, } console .log (Star .prototype ) </script>
4.4.4 思考 构造函数可以创建实例对象,构造函数还有一个原型对象,一些公共的属性或者方法放到这个原型对象身上
但是 为啥实例对象可以访问原型对象里面的属性和方法呢?
4.4.5 对象原型 对象都会有一个属性 __proto__
指向构造函数的 prototype
原型对象,之所以我们对象可以使用构造函数prototype
原型对象的属性和方法,就是因为对象有__proto__
原型的存在。
1 2 3 4 5 6 7 8 9 10 11 12 <script> function Star ( ) { } const ldh = new Star () console .log (ldh.__proto__ ) console .log (ldh.__proto__ .constructor === Star ) </script>
__proto__
对象原型里面也有一个 constructor属性,指向创建该实例对象的构造函数
1 2 3 4 5 6 7 8 注意: __proto__ 是JS非标准属性 [[prototype]]和__proto__意义相同 用来表明当前实例对象指向哪个原型对象prototype __proto__对象原型里面也有一个 constructor属性,指向创建该实例对象的构造函数
4.4.6 原型继承 继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,JavaScript 中大多是借助原型对象实现继承的特性。
先看一段初始代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 function Man ( ){ this .head = 1 this .eyes = 2 this .say = function ( ){} this .eat = function ( ){} } function Woman ( ){ this .head = 1 this .eyes = 2 this .say = function ( ){} this .eat = function ( ){} this .baby = function ( ){} }
以上构造函数中存在大量的重复代码。所以下面抽取公共部分的代码
把男人和女人公共的部分抽取出来放到人类里面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const People = { head :1 , eyes :2 , eat :function ( ){ } say :function ( ){} } function Man ( ){}function Woman ( ){ this .baby = function ( ){} }
实现继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const People = { head :1 , eyes :2 , eat :function ( ){ }, say :function ( ){} } function Man ( ){}Man .prototype = People Man .prototype .constructor = Man function Woman ( ){ this .baby = function ( ){} } Woman .prototype = People Woman .prototype .constructor = Woman
下面进行测试
1 2 const zs = new Man ()console .log (zs)
1 2 3 4 让男人和女人都能继承人类的一些属性和方法 把男人女人公共的属性和方法抽取出来 People 然后赋值给Man的原型对象,可以共享这些属性和方法 注意让constructor指回Man这个构造函数
但是,以上的写法是有问题的,例如:如果我们给男人添加了一个吸烟的方法,发现女人自动也添加这个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const People = { head :1 , eyes :2 , eat :function ( ){ }, say :function ( ){} } function Man ( ){}Man .prototype = People Man .prototype .constructor = Man Man .prototype .smoking = function ( ){}function Woman ( ){ this .baby = function ( ){} } Woman .prototype = People Woman .prototype .constructor = Woman
测试:
1 2 const woman = new Woman ()console .log (woman)
原因是:男人和女人都同时使用了同一个对象,根据引用类型的特点,他们指向同一个对象,修改一个就会都影响
怎样进行解决呢?
需求:男人和女人不要使用同一个对象,但是不同对象里面包含相同的属性和方法
答案:构造函数
new 每次都会创建一个新的对象
下面继续完善代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <script> function Person ( ) { this .eyes = 2 this .head = 1 } function Woman ( ) { } Woman .prototype = new Person () Woman .prototype .constructor = Woman Woman .prototype .baby = function ( ) { console .log ('宝贝' ) } const red = new Woman () console .log (red) function Man ( ) { } Man .prototype = new Person () Man .prototype .constructor = Man const zs = new Man () console .log (zs) </script>
1 2 3 4 5 6 7 通过以上的 Woman.prototype = new Person() 重新创建了一个Person对象赋值给了 Woman.prototype 原型对象 Man.prototype = new Person() 重新创建了一个Person对象赋值给了 Man.prototype 原型对象 两次new Person创建了两个不同的Person对象,但是都可以该对象中的公共的属性和方法
4.4.7 原型链 基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对象的链状结构关系称为原型链
1 2 3 4 5 6 7 1、当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。 2、如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象) 3、如果还没有就查找原型对象的原型(Object的原型对象) 4、依此类推一直找到 Object 为止(null) 5、__proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线 6、可以使用 instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <script> console .log (Object .prototype ) console .log (Object .prototype .__proto__ ) function Person ( ) { } const ldh = new Person () console .log (ldh instanceof Person ) console .log (ldh instanceof Object ) console .log (ldh instanceof Array ) console .log ([1 , 2 , 3 ] instanceof Array ) console .log (Array instanceof Object ) </script>
5、高级技巧 5.1 深浅拷贝 开发中我们经常需要复制一个对象。如果直接用赋值会有下面问题:
1 2 3 4 5 6 7 8 9 10 11 12 <script> const obj = { uname : 'zhangsan' , age : 18 } const o = obj console .log (o) o.age = 20 console .log (o) console .log (obj) </script>
浅拷贝
浅拷贝:拷贝的是地址
1 2 3 1. 拷贝对象:Object.assgin() / 展开运算符 {...obj} 拷贝对象 2.拷贝数组:Array.prototype.concat() 或者 [...arr]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <script> const obj = { uname : 'zhangsan' , age : 18 , family : { baby : '小zhang' } } const o = {} Object .assign (o, obj) o.age = 20 o.family .baby = 'zhangxiao' console .log (o) console .log (obj) </script>
如果是简单数据类型拷贝值,引用数据类型拷贝的是地址 (简单理解: 如果是单层对象,没问题,如果有多层就有问题)
问题:
1 2 3 4 5 6 直接赋值和浅拷贝有什么区别? 直接赋值的方法,只要是对象,都会相互影响,因为是直接拷贝对象栈里面的地址 浅拷贝如果是一层对象,不相互影响,如果出现多层对象拷贝还会相互影响 浅拷贝怎么理解? 拷贝对象之后,里面的属性值是简单数据类型直接拷贝值 如果属性值是引用数据类型则拷贝的是地址
5.2 深拷贝 深拷贝:拷贝的是对象,不是地址
1 2 3 4 5 常见方法: 通过递归实现深拷贝 lodash/cloneDeep 通过JSON .stringify ()实现
1 2 3 4 5 6 7 通过递归实现深拷贝 函数递归: 如果一个函数在内部可以调用其本身,那么这个函数就是递归函数 简单理解:函数内部自己调用自己, 这个函数就是递归函数 递归函数的作用和循环效果类似 由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return
1 2 3 4 5 6 7 8 9 10 11 12 <script> let i = 1 function fn ( ) { console .log (`这是第${i} 次` ) if (i >= 6 ) { return } i++ fn () } fn () </script>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <script> const obj = { uname : 'zhangsan' , age : 18 , hobby : ['乒乓球' , '足球' ], family : { baby : '小zhang' } } const o = {} function deepCopy (newObj, oldObj ) { for (let k in oldObj) { if (oldObj[k] instanceof Array ) { newObj[k] = [] deepCopy (newObj[k], oldObj[k]) } else if (oldObj[k] instanceof Object ) { newObj[k] = {} deepCopy (newObj[k], oldObj[k]) } else { newObj[k] = oldObj[k] } } } deepCopy (o, obj) o.age = 20 o.hobby [0 ] = '篮球' o.family .baby = 'laozhang' console .log (o) console .log (obj) console .log ([1 , 23 ] instanceof Object ) </script>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <body> <!-- 先引用 --> <script src ="./lodash.min.js" > </script > <script > const obj = { uname : 'zhangsan' , age : 18 , hobby : ['乒乓球' , '足球' ], family : { baby : '小zhang' } } const o = _.cloneDeep (obj) o.family .baby = '老zhang' console .log (o) console .log (obj) </script >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <script> const obj = { uname : 'zhangsan' , age : 18 , hobby : ['乒乓球' , '足球' ], family : { baby : '小zhang' } } const o = JSON .parse (JSON .stringify (obj)) o.family .baby = '123' console .log (o) console .log (obj) </script>
5.3 异常处理 throw
抛异常
1 2 3 4 5 6 7 8 9 10 11 12 <script> function fn (x, y ) { if (!x || !y) { throw new Error ('没有参数传递过来' ) } return x + y } console .log (fn ()) </script>
1 2 3 4 5 总结: 1. throw 抛出异常信息,程序也会终止执行 2. throw 后面跟的是错误提示信息 3. Error 对象配合 throw 使用,能够设置更详细的错误信息
try /catch
捕获异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <body> <p > 123</p > <script > function fn ( ) { try { const p = document .querySelector ('.p' ) p.style .color = 'red' } catch (err) { console .log ( err.message ) return } finally { alert ('弹出对话框' ) } console .log (11 ) } fn () </script > </body>
1 2 3 4 5 6 总结: 1. try...catch 用于捕获错误信息 2. 将预估可能发生错误的代码写在 try 代码段中 3. 如果 try 代码段中出现错误后,会执行 catch 代码段,并截获到错误信息 4. finally 不管是否有错误,都会执行
5.4 this
指向 5.4.1 普通函数this
指向 普通函数的调用方式决定了 this 的值,即【谁调用 this 的值指向谁】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <body> <button > 点击</button > <script > console .log (this ) function fn ( ) { console .log (this ) } window .fn () window .setTimeout (function ( ) { console .log (this ) }, 1000 ) document .querySelector ('button' ).addEventListener ('click' , function ( ) { console .log (this ) }) const obj = { sayHi : function ( ) { console .log (this ) } } obj.sayHi () </script >
5.4.2 箭头函数this
指向 箭头函数中的 this 与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在 this !
箭头函数会默认帮我们绑定外层 this 的值,所以在箭头函数中 this 的值和外层的 this 是一样的
2.箭头函数中的this引用的就是最近作用域中的this
3.向外层作用域中,一层一层查找this,直到有this的定义
注意情况1 :
在开发中【使用箭头函数前需要考虑函数中 this 的值】,事件回调函数使用箭头函数时,this 为全局的 window
因此DOM事件回调函数如果里面需要DOM对象的this,则不推荐使用箭头函数
注意情况2 *:
同样由于箭头函数 this 的原因,基于原型的面向对象也不推荐采用箭头函数
5.5 改变this
指向 JavaScript 中还允许指定函数中 this 的指向,有 3 个方法可以动态指定普通函数中 this 的指向
**1. call()了解 **
使用 call 方法调用函数,同时指定被调用函数中 this 的值
语法
1 fun.call (thisArg, arg1, arg2, ...)
1 2 3 4 thisArg:在 fun 函数运行时指定的 this 值 arg1,arg2:传递的其他参数 返回值就是函数的返回值,因为它就是调用函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <body> <script > const obj = { uname : 'zs' } function fn (x, y ) { console .log (this ) console .log (x + y) } fn.call (obj, 1 , 2 ) </script > </body>
总结
1 2 3 4 5 call的作用是? 调用函数,并可以改变被调用函数里面的this指向 2. call 里面第一个参数是 指定this, 其余是实参,传递的参数 整体做个了解,后期用的很少
2 apply()-理解 *
语法:
1 2 fun.apply(thisArg, [argsArray])
(1)thisArg
:在fun函数运行时指定的 this 值
(2) argsArray
:传递的值,必须包含在数组里面
(3) 返回值就是函数的返回值,因为它就是调用函数
(4)因此 apply 主要跟数组有关系,比如使用Math.max()
求数组的最大值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <script> const obj = { age : 18 } function fn (x, y ) { console .log (this ) console .log (x + y) } fn.apply (obj, [1 , 2 ]) const arr = [100 , 44 , 77 ] const max = Math .max .apply (Math , arr) const min = Math .min .apply (null , arr) console .log (max, min) console .log (Math .max (...arr)) </script>
1 2 3 4 call和apply的区别是? 都是调用函数,都能改变this指向 参数不一样,apply传递的必须是数组
**3. bind()-**重点
语法:
1 2 fun.bind (thisArg, arg1, arg2, ...)
1 2 3 4 5 thisArg:在 fun 函数运行时指定的 this 值 arg1,arg2:传递的其他参数 返回由指定的 this 值和初始化参数改造的 原函数拷贝 (新函数) 因此当我们只是想改变 this 指向,并且不想调用这个函数的时候,可以使用 bind,比如改变定时器内部的this指向.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <body> <button > 发送短信</button > <script > const obj = { age : 18 } function fn ( ) { console .log (this ) } const fun = fn.bind (obj) fun () document .querySelector ('button' ).addEventListener ('click' , function ( ) { this .disabled = true window .setTimeout (function ( ) { this .disabled = false }.bind (this ), 2000 ) }) </script > </body>
call apply bind 总结
1 2 3 4 5 6 7 8 9 10 11 相同点: 都可以改变函数内部的this指向. 区别点: call 和 apply 会调用函数, 并且改变函数内部this指向. call 和 apply 传递的参数不一样, call 传递参数 aru1, aru2..形式 apply 必须数组形式[arg] bind 不会调用函数, 可以改变函数内部this指向. 主要应用场景: call 调用函数并且可以传递参数 apply 经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值 bind 不调用函数,但是还想改变this指向. 比如改变定时器内部的this指向.
6、模块化 1、模块化简介 随着前端的代码量越来越多,那么需要对代码进行管理,而模块化就是代码组织管理的最主流的一种方式,它可以按照不同的功能,将代码划分为不同的功能模块。
模块化只是一种思想
2、模块化演变过程 在最早期,JavaScript
的模块化就是根据文件划分方式
来实现的,将不同的功能以及相关的数据存储到不同的文件中,我们约定每一个文件就是一个独立的模块
我们使用这个模块,就是把这个模块引入到页面中。一个script
标签对应一个模块
1 2 3 4 5 <script src="a.js" ></script> <script > method1 (); username="zhangsan" </script >
在使用的时候,直接在代码中调用全局成员,这些全局成员有可能是一个变量,也有可能是一个函数。
这种方式的缺点也非常明显,所有的模块都是在全局的范围内工作,并没有独立的私有空间,这样导致模块内的成员会在模块外被任意的访问或者修改。而且模块多了以后,很容易出现命名冲突的问题,同时也无法管理模块之间的依赖关系。
所以说,早期的模块化完全依靠的是约定,而这种情况在项目增大以后,就会出现各种问题。
下面进入说一下第二阶段:命名空间方式
我们约定每个模块只暴露一个全局的对象,模块的所有成员都挂载到这个对象的下面。在第一个阶段基础上(也是单独创建一个文件),把每个模块包裹成一个全局对象的方式来实现。
1 2 3 4 5 6 var moduleA={ name :'zhangsan' , method1 :function ( ){ console .log ('username:' ,this .name ) } }
使用的方式
1 2 3 4 5 6 7 8 9 10 11 12 <html> <head > </head > <body > <script src ="a.js" > </script > <script > moduleA.method1 () moduleA.name ="lisi" </script > </body > </html>
在使用的时候,需要通过全局对象的名称来获取内部成员,这就类似于命名空间的方式。
但是,这种方式仍然没有私有空间,所以说模块内的成员依然可以在模块外被访问修改,另外模块的依赖关系也没有得到解决。
第三阶段:立即执行函数的方式
通过立即执行函数的方式为模块提供私有空间。具体做法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ;(function ($ ){ var name="zhangsan" function method1 ( ){ cosnole.log (this .name ) } function method2 ( ){ console .log ('method2' ) } window .moduleA ={ method1 :method1, method2 :method2 } })(jQuery)
将模块中的每个成员都放到函数所提供的私有作用域内,对需要暴露给外部的成员,将其挂载到全局对象window
中。这种方式实现了私有成员的概念,在外部无法访问模块内的成员。
1 2 3 4 5 6 7 8 9 10 11 12 13 <html> <head > </head > <body > <script src ="a.js" > </script > <script > moduleA.method1 () moduleA.method2 () console .log (moduleA.name ) </script > </body > </html>
这种方式,也解决了模块的依赖关系,例如,我们定义的模块需要jQuery
,这里就可以直接传递jQuery
.
以上方式,就是早期在没有工具和规范的情况下,对模块化的落地方式。
3、模块化规范的出现 前面介绍的模块化方式,都是通过约定的方式来实现的,这样在实际的应用中会出现相应的一些差异,所以这里就需要有模块化的规范与标准。
同时,前面定义的模块,在使用的时候都是通过srcipt
标签的手动方式进行引入的。
1 <script src='a.js'></script>
这也就是说,模块的加载,不受代码的控制。这样维护起来也非常的麻烦,而且还容易出现问题。
例如,在html
代码中忘记引入模块,或者是一个模块我们不需要了,但是在html
代码中,却还引入该模块,这样都会出问题。
所以,我们需要通过一些代码来帮我们加载模块。
所以说,现在我们需要:
模块化的标准与模块加载的库。
说到模块化规范:首先我们想到的就是CommonJS
规范,它是node.js
中提到的一个规范。也就是说我们在写Node.js
的时候,必须符合CommonJS
的规范。
该规范要求:
一个文件就是一个模块
每个模块都有单独的作用域
通过module.exports
导出成员
通过require
函数载入模块
如果,我们在浏览端使用CommonJS
规范,就会出现问题。而且CommonJS
是以同步模式加载模块,因为Node
的执行机制是在启动的时候加载模块,执行过程中是不需要加载模块的,所以这种方式在Node
中不会出现问题。如果我们在浏览器端使用CommonJS
规范,必然会导致效率低下,因为每次页面的加载,都会导致大量的同步请求出现,这样严重的降低了浏览器加载的效率。
所以后来浏览器端没有使用CommonJS
规范,而是结合浏览器端的特性,重新设计了一套规范。
这个规范就是AMD(Asynchronous Module Definition)
,翻译过来就是异步的模块定义规范。
同期还出现了一个比较著名的库Require.js
,它实现了AMD
的规范。
它定义模块的方式:
1 2 3 4 5 6 7 8 define ('module1' ,['jquery' ,'./module2' ],function ($,module2 ){ return { start :funciton ( ){ $('body' ).animate ({margin :'200px' }) module2 () } } })
通过define
来定义模块,第一个参数为模块的名字,第二个参数是一个数组,用来声明我们创建的模块所依赖的项。第三个参数是一个函数,函数的参数与前面依赖项一一对应,$
对应jquery
,module2
对应的是./module2
.
这个函数的作用就是为当前模块提供一个私有的空间,如果我们需要模块向外部导出一些成员,可以使用return
.
同时Require.js
中还提供了一个载入模块的方法:require
1 2 3 require (['./module1' ],funciton (module1 ){module1.start })
目前绝大多数第三方库都支持AMD
规范。但是,AMD
使用起来相对复杂。因为除了编写业务代码,还需要写大量的define
函数与require
函数。
另外,项目中模块划分的过于细致的化,那么在同一个文件中JS
文件请求频繁,从而导致页面的效率比较低下。
所以说AMD
不能是前端模块化最终的实现方案,仅仅是一个过渡的方案。
4、模块化标准规范 随着技术的不断发展,模块化的标准越来越规范,现在大家对模块化的最佳实践方式都统一了。
在浏览器端使用ES Modules
,在Node.js
中使用CommonJS
现在最新的浏览器都开始支持ES Module
模块化的方式了。所以,我们可以浏览器端直接使用ES Module
来实现模块化。
5、ES Modules 语法 下面我们来看一下它的基本使用
通过给script
标签添加type=module
的属性,就可以以ES Module
的标准执行其中的js
代码了。
1 2 3 <script type='module' > console .log ('hello world' ) </script>
每个ES Module
都是运行在单独的私有作用域中的。
1 2 3 4 5 6 7 <script type="module" > var foo=100 console .log (foo) </script> <script type ="module" > console .log (foo) </script >
6、模块导出 ES Modules
的核心功能是模块的导出与导入
1 2 3 4 5 6 const foo='es modules' export {foo}import {foo} from './module.js' console .log (foo)
下面,我们演示一下基本的导入与导出。
在目录下面创建app.js
,index.html
,module.js
三个文件。
module.js
文件中的代码如下:
将name
这个变量进行导出,这样在其他的模块中才能访问该变量。
app.js
文件的代码如下:
1 2 import {name} from './module.js' ;console .log (name);
进行了导入,并且输出name
变量的值
index.html
文件中的代码
1 2 3 4 5 6 7 8 9 10 11 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > ES Module 导出与导入</title > </head > <body > <script type ='module' src ="app.js" > </script > </body > </html >
可以通过Live Server
启动,来查看运行的效果
在上面的案例中,我们通过export
导出了变量,当然也可以导出函数
1 2 3 4 export var name='lisi' export function hello ( ){ console .log ('hello world' ) }
如果以上导出的方式,书写起来比较麻烦,可以采用如下的方式
1 2 3 4 5 6 var name='lisi' function hello ( ){ console .log ('hello world' ) } export {name,hello}
最后通过export
统一进行导出。
这种方式是使用最多的导出方式,因为这种方式可以让我们更加直观的感受到当前模块到底导出了哪些成员。
当然,通过以上的方式,还可以对导出的成员进行重命名。
1 2 3 4 5 6 7 var name='lisi' function hello ( ){ console .log ('hello world' ) } export { name as username}
在上面的代码中,我们在导出name
的时候通过as
关键字将其重命名为username
,那么在进行导入的时候就需要使用username
1 2 import {username} from './module.js' ;console .log (username);
如果我们给export
后面添加了default
关键字,表示的是默认的导出。
1 2 3 4 5 6 7 8 var name='lisi' function hello ( ){ console .log ('hello world' ) } export default name
在上面的代码中,默认导出了name
属性。
这时导入的方式如下:
1 2 import username from './module.js' ;console .log (username);
在导入的时候,不需要添加大括号,同时导入的名称是可以随意命名的,例如导出的变量名叫name
,但是导入的时候修改成了username
7、导入与导出的注意事项 1 2 3 var name='zhangsan' var age=18 export {name,age}
在进行导出的时候,注意语法,例如上面我们导出了name
与age
这两个变量,在这里必须将其放到大括号中,这时它的语法要求,不要认为这里导出的是一个对象。
如果,这里确定需要导出一个对象,可以使用export default
的方式
1 export default {name,age}
下面将module.js
文件中的代码修改成如下形式:
1 2 3 var name='zhangsan' var age=18 export default {name,age}
如果app.js
文件中的导入是如下写法,而这种写法是错误的。
1 2 import {name,age} from './module.js' ;console .log (name,age);
这里,不要把导入的写法理解成解构。这其实是固定的写法。
正确的导入方式如下:
1 2 import obj from './module.js' ;console .log (obj.name ,obj.age );
下面我们再来强调要注意的第二点内容:
在es module
中,通过export
对成员进行导出的时候,注意导出的是该成员的引用。
也就是说,在模块内部导出的成员与其它模块中导入的成员是指向了同一个内存的地址。
下面,演示一下
在module.js
文件中的代码如下:
1 2 3 4 5 6 7 var name='zhangsan' var age=18 export {name,age}setTimeout (function ( ){ name='lisi' },1000 )
在导出name
与age
成员后,停顿了1秒钟,修改name
成员的值。
app.js
文件中的代码如下:
1 2 3 4 5 import {name,age} from './module.js' ;console .log (name,age);setTimeout (function ( ) { console .log (name,age); },1500 )
导入name
与age
成员,然后输出打印,停顿1500
毫秒后,重新打印name
与age
的值,
在app.js
文件中,我们没有修改name
属性的值,但是在1500
毫秒以后,name
的值变成了lisi
,而对name
属性值的修改是在module.js
文件中完成的。通过这个例子,我们可以得出的结论就是:通过export
对成员进行导出的时候,导出的是该成员的引用,并不是将值进行了拷贝。
第三点:要注意的时,导出的成员是只读的 。也就是说,如果我们现在在app.js
文件中导入了module.js
模块中的name
和age
成员,然后在app.js
文件中去修改这两个成员的值的时候,会出现错误。也就说,导入的成员是一个只读的成员
1 2 import {name,age} from './module.js' ;name="ssss"
以上就是导入与导出要注意的事项。
8、import用法 import
中的from
后面跟的是一个字符串,表示的是文件的路径,该路径必须是一个完整的路径。
1 import {name} from './module'
以上的写法是错误的,必须添加js
后缀。
1 import {name} from './module.js'
如果某个目录下面有index.js
文件,在进行导入的时候,也需要写完整。例如,如下写法错误
1 import {age} from './utils'
正确的写法如下:
1 import {age} from './utils/index.js'
当我们后面使用一些打包工具,来打包模块的时候,就可以省略扩展名和默认文件的情况。
下面还要注意的就是相对路径中的.
不能省略,如下写法是错误的
1 import {name} from 'module.js'
以上写法是错误的,这种写法默认的是加载第三方模块。
当然也可以使用绝对路径
1 import {name,age} from '/module.js' ;
也可以使用完整的url
的方式,这样就可以引用cdn
上的模块。
以上就是在导入模块的时候,关于路径的问题。
如果,我们只是需要执行某个模块,而不需要提取这个模块中的成员 ,可以采用如下的写法:
1 import {} from './module.js'
以上表明只是执行module
这个模块,并不提取这个模块中的成员。
也可以采用如下简写的方式
下面,我们再来看第三种情况,如果某个模块中导出的成员比较多,而且现在我们都需要这些导出的成员,那么再进行导入的时候,为了书写的简单方便,可以采用如下的方式:
1 2 import * as mod from './module.js' console .log (mod.name )
首先import
后面跟了一个星号,然后在星号后面跟了一个关键字as
,将导入的全部成员放到一个对象中。这样导出的成员都会作为这个对象的属性而存在。
下面,我们看一下第四点情况。
当我们通过import
进行模块导入的时候,必须明确指定导入模块的路径。
但是,在有的情况下,我们所导入的模块的路径是在运行的情况下才确定的,那么你可能会想到使用如下的方式进行导入:
1 2 3 var modulePath='./module.js' import {name} from modulePathconsole .log (name)
以上的写法是错误的,也就是说我们不能通过import
,from
后面跟着一个变量。
还有一种情况,就是我们想在某个判断条件成立后,才去导入某个模块,所以你会想到如下的写法:
1 2 3 if (true ){ import {name} from './module.js' }
以上的写法也是错误的,因为import
这个关键字只能出现在最顶层,并不能嵌套在if
或者是函数中。
如果,现在你遇到了以上两种情况,那么可以通过动态导入模块的方式来进行解决。写法如下:
在以上的写法中,将import
作为一个函数来使用,这样可以在任何的位置去调用该函数来导入相应的模块。而且import
函数返回的是一个promise
,而且我们知道模块加载是一个异步的过程,这时当模块加载完成后,会执行后面的then
函数。
1 2 3 import ('./module.js' ).then (funciton (module ){ console .log (module ) })
下面我们来看第五点: 就是在导出模块的时候,我们导出了一个命名的成员,又导出了一个默认的成员。
如下代码所示:
1 2 3 4 var name='zhangsan' var age=18 export {name,age}export default 'hello world'
在进行导入的时候,可以采用如下的写法:
1 2 import title,{name,age} from './module.js' ;console .log (title,age,name);
import
后面跟的title
是起了一个别名(这个名字可以随便起),用来接收默认的导入的成员,大括号中的内容,用来接收命名的导出的成员。