目录
1、闭包
1.1 变量作用域
理解闭包,我们需要首先理解变量的作用域。变量的作用域可以理解为变量的使用范围。
function func(){ console.log(num1); // 报错使用未定义的变量,程序终止 num1 =3; console.log(num1); } func();
function func(){ num1 =3; // 不加 var 变量提升为全局 console.log(num1); } console.log(num1); // 报错 func();
function func(){ num1 =3; // 不加 var 变量提升为全局 console.log(num1); } func(); console.log(num1); // 3
function func(){ console.log(num1); // undefined var num1 =3; console.log(num1); // 3 } func();
var num1 =1; function func(){ console.log(num1); // undefined var num1 =3; // 此变量的声明会提升 console.log(num1); // 3 } console.log(num1); // 1
function func(){ num1 =3; // 不加 var 变量提升为全局 console.log(num1); for(var i=0; i<5; i++){ } console.log(i); // 5 } func();
js中变量的作用域分为两种:(ES6之后新增了块级作用域)
-
全局作用域:在script标签中直接定义的属性和方法。在全局作用域中声明的变量和函数,都属于window对象。
-
局部作用域:局部变量属于某个函数。在函数中定义的变量成为局部变量。
-
调用函数时创建函数作用域,函数执行完毕之后,函数作用域销毁
-
每调用一次函数就会创建一个新的函数作用域,它们之间是相互独立的
-
var a = 0; // 全局变量 function func(){ var b =1; // 局部变量 d = 3; // 全局变量: 不声明直接使用,会出现变量的提升 { var c = 2; // 块级变量,使用var和let会有不同的结果。let会阻止变量的提升。 } console.log(c); } func(); // 放在输出d的代码后,会有不同的效果。 console.log(b); console.log(d);
1.2 什么是闭包
闭包就是能够读取其他函数内部变量的函数。
正常情况下,我们在函数的外部是无法读取到函数内部的变量。
那我们把这个变量变成全局变量不就解决了吗?但是全局变量就意味着此变量可以被所有函数访问,安全性低。
这个时候就需要用到闭包。
-
外层方法中定义一个局部变量
-
外层方法中定义一个内层方法,此方法修改局部变量,并返回局部变量
-
外层方法将内层方法作为返回值返回
-
在外部调用外层方法,并接受内层方法对象。
-
在外部调用内层方法,可以获取或者修改外层方法中的局部变量
function func(){ var count =1; // 局部变量 function addCount(){ count++; return count; } function reduceCount(){ count--; return count; } return [addCount,reduceCount]; } var funArray = func(); funArray[0](); funArray[1]();
有了闭包,函数内部的变量,外部无法直接访问,但是可以通过暴露出来的方法进行操作。
1.3 闭包的缺陷
为了实现闭包,我们需要把函数内部的作用域强行保留在内存中。这就是导致了这部分的内存无法回收。大量的闭包会导致内存消耗太多。
所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。
1.4 作用域链
作用域有上下级关系,上下级关系的确定就看函数是在哪个作用域下创建的。如上,fn作用域下创建了bar函数,那么“fn作用域”就是“bar作用域”的上级。
作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
变量取值:到创建 这个变量 的函数的作用域中取值
在作用域链中,只能子调用父,不能父调用子。
一般情况下,变量取值到 创建 这个变量 的函数的作用域中取值。
但是如果在当前作用域中没有查到值,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。
name="lwy"; function t(){ var name="tlwy"; function s(){ var name="slwy"; console.log(name); } function ss(){ console.log(name); } s(); ss(); } t();
2、js中的真假判断
不是只有boolean类型才能进行逻辑运算。在js中变量可以转换为布尔值。
通常用来判断变量是否为空
var a = 0; var b = 6; console.log(a&&b); console.log(b&&a);
在js中以下都是假变量:
-
false
-
0 数字类型 注意
-
空字符串
-
null
-
undefined
-
NaN (not a number)
3、js函数中的隐藏参数
arguments是函数体中的一个隐藏的对象。
-
用来取参数。
-
传入的实参都能在函数体里通过arguments类数组获取到
-
当方法的参数个数不确定的时候,可以用它来获取参数
function test01(num1,num2){ // arguments可以拿到所有的实参 console.log(arguments); } test01(1,2,3,4,5);
4、立即执行函数
什么是立即执行函数? IIFE : Immediate-invoke function expressioon
4.1 特点
立即执行函数,会在函数声明后立即执行,执行后会销毁。
4.2 写法
// 常用方式1 (function(str){ console.log('a'+str) }('abc')); // 常用方式2 (function(str){ console.log('a'+str) })('abc'); // 常用方式3 var foo = function(){ console.log('方式3') }();
4.3 错误写法
function () { console.log('错误写法1') }(); function foo() { console.log('错误写法2') }();
4.4 与闭包的常用结合
let single = (function () { let name = "小明"; let age = 20; return { getName: function () { return name; }, getAge: function () { return age; } } })(); console.log(single.getName()); //小明 console.log(single.getAge()); //20
5、变量的提升
通常JS引擎会在正式执行之前先进行一次预编译,在这个过程中,首先将变量声明及函数声明提升至当前作用域的顶端,然后进行接下来的处理。
function foo() { a=10; // a变量没有声明直接赋值,会产生变量的提升,提升到全局作用域 console.log(a); // 输出10 a=20; } foo(); console.log(a) // 输出20
function foo() { a=10; // a变量没有声明直接赋值,会产生变量的提升,但是由于本作用域中出现了声明代码,所以变量提升到本作用域顶端 console.log(a); // 输出10 var a=20; } foo(); console.log(a) // 报错
let 会阻止变量的提升
function foo() { a=10; // 报错。 没有变量的提升 console.log(a); let a=20; // 使用let声明变量,最阻止变量的提升 } foo(); console.log(a)
下面是一道经典面试题,大家猜猜运行结果
console.log(v1); // undefined var v1 = 100; function foo() { console.log(v1); // undefined var v1 = 200; console.log(v1);// 200 } foo(); console.log(v1);// 100
当在不同的作用域中都声明了同名变量。那js引擎会优先调用本作用域的变量。
6、函数的提升
js在预编译阶段,会把函数的声明,提前到代码的前端。
函数的声明和函数的调用是两回事。函数的声明会提升,但是函数的调用是顺序执行的。
函数的作用域,只有在函数执行时才会产生,运行完毕后销毁。
函数的声明有两种:
-
函数式声明。会有函数的提升
-
字面量声明。不会有函数的提升
console.log(foo); function foo() { // 函数的声明提升 console.log(1); } foo();
console.log(foo); var foo = function () { // 函数声明不提升 console.log(1); } foo();
7、递归
递归就是在方法内部自己调用自己。
递归中一定要有退出判断,否则就是死循环。
function foo(i) { if (i == 4) { return; } console.log("fb:" + i); foo(i + 1); console.log("fe:" + i); } foo(1);
8、原型
prototype。每个方法都有一个对象prototype,这个对象就是原型。
一般工作的时候不常用,开发框架和插件的时候使用。但是面试问的多。
原型的作用:
-
给构造出来的对象设置公共的属性和方法
-
建立了构造函数和实例化出来对象的联系
多个原型之间是有类似继承关系的,构成了原型链。原型链的尽头是null,属于object对象。
9、数组
数据就是放置多个数据的集合对象。js中数组的长度和存储的类型是不固定的。
9.1 创建数组
var list1 = new Array(4); // 创建一个长度为4的数组,里面有4个空值 var list2 = ['1', 2, 3, true]; // 创建数组并赋值。
9.2 基本方法
var list = [1, 2, 3]; // 创建数组并赋值。 console.log(list.indexOf(1));// 返回1这个元素在数组中第一次出现的下标,如果没有返回-1 list[1] = 22; // 修改指定下标的值 list.push(4,5);// 向数组末尾追加元素,元素数量不定。返回新的长度 list.unshift(-1,0) // 向数组头部添加元素,元素数量不定。返回新的长度 list.pop(); // 删除最后一位元素,返回被删除的元素 list.shift(); // 删除第一位元素,返回被删除的元素 list.splice(1); // 从指定下标开始删除,删到最后。返回删除的元素数组 list.splice(1,2); // 删除从下标1开始,长度为2的元素。返回删除的元素数组 list.splice(1,1,33,44); // 删除从下标1开始,长度为1的元素,并在删除的位置上添加两个元素,添加的元素个数不定 var str = list.join(''); // 把数组按照指定的间隔符,拼成一个字符串 var list3 = [11,22,33]; var newlist = list.concat(list3); // 合并数组,注意合并数组返回的是新的数组对象,对原对象不做修改 // console.log(list2) // console.log(str); console.log(list);
10、判断数据类型
-
typeof:此方法直接返回原型链上的最后一个对象。
-
instanceof: 此操作符会查找原型链上是否存在这个构造函数。存在返回true。
11、事件
11.1 绑定事件
绑定事件有三种方式:
-
在html中添加事件属性。html和js代码耦合在一起,不利于维护。不推荐。
<body> <ul id="list" οnclick="func()"> <li>1</li> <li>2</li> <li>3</li> </ul> </body> <script> function func(){ console.log('触发了') } </script>
-
在js中调用事件属性。会产生覆盖,只能写一次。本质上和上面一样。
<body> <ul id="list"> <li>1</li> <li>2</li> <li>3</li> </ul> </body> <script> var listDom = document.getElementById('list'); listDom.onclick = function(){ console.log('触发了1') }; listDom.onclick = function(){ console.log('触发了2') }; </script>
-
在js中添加事件监听器。不会覆盖,可以绑定多个。
<body> <ul id="list"> <li>1</li> <li>2</li> <li>3</li> </ul> </body> <script> var listDom = document.getElementById('list'); listDom.onclick = function(){ console.log('触发了1') }; listDom.onclick = function(){ console.log('触发了2') }; listDom.addEventListener('click',function(){ console.log('触发了3'); }); listDom.addEventListener('click',function(){ console.log('触发了4'); }); var func5 = function(){ console.log('触发了5'); } listDom.addEventListener('click',func5); // 添加事件 listDom.removeEventListener('click',func5); // 移除指定方法 </script>
11.2 事件传播机制
事件有两种传播机制:
-
冒泡 。先触发子元素事件,再触发父元素事件
-
捕获。 先触发父元素事件,再触发子元素事件
<body> <ul id="parent"> <li id="chlid1">1</li> <li>2</li> <li>3</li> </ul> </body> <script> var parentDom = document.getElementById('parent'); parentDom.addEventListener('click',function(){ console.log('parent'); },true) // 第三个参数控制事件传播机制,默认false冒泡,true捕获 var child1Dom = document.getElementById('chlid1'); child1Dom.addEventListener('click',function(){ console.log('chlid1'); }) </script>
11.3 阻止冒泡
var parentDom = document.getElementById('parent'); parentDom.addEventListener('click',function(){ console.log('parent'); }) var child1Dom = document.getElementById('chlid1'); child1Dom.addEventListener('click',function(event){ console.log('chlid1'); event.stopPropagation(); // event为事件对象,调用stopPropagation()阻止冒泡 })
12、cookie
原生的js操作cookie比较麻烦,我们自己可以封装一些方法来使用。
function setCookie(cname, cvalue) { var d = new Date(); d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); var expires = "expires=" + d.toGMTString(); document.cookie = cname + "=" + cvalue + "; "+expires; } function getCookie(cname) { var name = cname + "="; var ca = document.cookie.split(';'); // 将字符串串以;分割数组 for (var i = 0; i < ca.length; i++) { var c = ca[i].trim(); // 把多余空格和回⻋车删掉 if (c.indexOf(name) == 0) { return c.substring(name.length, c.length); } } return ""; }
13、es6关于对象的新特性
13.1 扩展运算符
{ let person = {id:'1',name:'张三',sex:1}; let personitem = {name:'王五',pwd:'abc'}; // 复制对象,和复制数组一样 let newPerson = {...person}; console.log(newPerson); // 复制对象,并修改其中的某几个属性 let newPerson1 = {...person, name:'李四',sex:0,love:'吃'} console.log(newPerson1); // 合并对象,谁在后谁优先 let newPerson2 = {...person,...personitem}; console.log(newPerson2) }
13.2 对象初始化
{ // 属性初始化简写 let objes5 = { name:name, sayHello:function(){ console.log(this.name) } } objes5.sayHello(); let objes6 = { name, sayHello(){ console.log(this.name) } } } { // 可计算的属性名 let key = 'name'; let objes5 = {}; objes5[key] = '张三'; console.log(objes5); let objes6 = { [key]:'李四' } console.log(objes6); }
13.3 新增方法
{ // Object.is() 判断两个对象是否相等,类似于===。但也有不同 let result = Object.is(NaN,NaN); console.log(result); console.log(NaN===NaN); let person1 = {id:'1',name:'张三',sex:1}; let person2 = {id:'1',name:'张三',sex:1}; let person3 = person1; console.log(Object.is(person1,person2)); console.log(Object.is(person1,person3)); } { // Object.assign() 复制对象,注意复制简单属性,类属性还是引用 let sourcePerson = {id:'1',name:'张三',sex:1,info:{height:180}}; let targetPerson = {} Object.assign(targetPerson,sourcePerson); targetPerson.info.height=160; targetPerson.name='李四'; console.log(sourcePerson,targetPerson) } { // Object.keys(); Object.values(); Object.entries(); let json = {id:'1',name:'张三',sex:1}; let obj = {}; for (const key of Object.keys(json)) { obj[key] = json[key] } console.log(obj); }
14、Map
js中的对象本身就是一个Hash结构的,但是他的key只能是string类型。
Map数据结构他的key就可以是任意类型。
{ let map1 = new Map(); // 添加方法 map1.set([1,2,3],'abc'); console.log(map1); // 声明并赋值 let map2 = new Map([['name','张三'],['sex','男']]); console.log(map2); console.log(map2.size); // size属性 // 遍历方法 map2.keys(); map2.values(); map2.entries(); for (const key of map2.keys()) { console.log(key); } // 是否包涵key console.log(map2.has('name')); // 根据key删除 console.log(map2.delete('name')); map2.clear(); // 清空 }
15、for in 和for of的区别
1、for...in 循环:只能获得对象的键名,不能获得键值
for...of 循环:允许遍历获得键值
var arr = ['red', 'green', 'blue'] for(let item in arr) { console.log('for in item', item) } /* for in item 0 for in item 1 for in item 2 */ for(let item of arr) { console.log('for of item', item) } /* for of item red for of item green for of item blue */
2、对于普通对象,没有部署原生的 iterator(迭代器) 接口,直接使用 for...of 会报错
var obj = { 'name': 'Jim Green', 'age': 12 } for(let key of obj) { console.log('for of obj', key) } // Uncaught TypeError: obj is not iterable
可以使用 for...in 循环遍历键名
for(let key in obj) { console.log('for in key', key) } /* for in key name for in key age */
也可以使用 Object.keys(obj) 方法将对象的键名生成一个数组,然后遍历这个数组
for(let key of Object.keys(obj)) { console.log('key', key) } /* key name key age */
3、for...in 循环不仅遍历数字键名,还会遍历手动添加的其它键,甚至包括原型链上的键。for...of 则不会这样
let arr = [1, 2, 3] arr.set = 'world' // 手动添加的键 Array.prototype.name = 'hello' // 原型链上的键 for(let item in arr) { console.log('item', item) } /* item 0 item 1 item 2 item set item name */ for(let value of arr) { console.log('value', value) } /* value 1 value 2 value 3 */
4、forEach 循环无法中途跳出,break 命令或 return 命令都不能奏效
let arr = [1, 2, 3, 5, 9] arr.forEach(item => { if(item % 2 === 0) { return } console.log('item', item) }) /* item 1 item 3 item 5 item 9 */
for...of 循环可以与break、continue 和 return 配合使用,跳出循环
for(let item of arr) { if(item % 2 === 0) { break } console.log('item', item) } // item 1
总之,for...in 循环主要是为了遍历对象而生,不适用于遍历数组
for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象
16、什么是event loop
-
主线程运行的时候会生成堆(heap)和栈(stack);
-
js 从上到下解析方法,将其中的同步任务按照执行顺序排列到执行栈中;
-
当程序调用外部的 API 时(比如 ajax、setTimeout 等),会将此类异步任务挂起,继续执行执行栈中的任务。等异步任务返回结果后,再按照顺序排列到事件队列中;
-
主线程先将执行栈中的同步任务清空,然后检查事件队列中是否有任务,如果有,就将第一个事件对应的回调推到执行栈中执行,若在执行过程中遇到异步任务,则继续将这个异步任务排列到事件队列中。
-
主线程每次将执行栈清空后,就去事件队列中检查是否有任务,如果有,就每次取出一个推到执行栈中执行,这个循环往复的过程被称为“Event Loop 事件循环”
结束语:
首先,恭喜大家已经阅读完整个JS(重点难点),一般而言,不管书籍也好,能够完整跟下来的就已经很不容易了。所以尽量帮助初学者减少初级的困难,其实一旦掌握了之后,会发现它其实是非常容易。但大道至简,知易行难,需要大家之后不断练习,在此基础上加强知识的认知深度。虽然我尽量以通俗易通的形式,将内容体现出来,但水平毕竟有限,望大家海涵。