文章目录
0. 基础回顾
-
JavaScript 数据类型
基本类型:字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。
引用数据类型:对象(Object)、数组(Array)、函数(Function)。
-
对象的理解
对象:对单个物体的简单抽象,对象是一个容器,封装了属性和方法属性:对象的状态
方法:对象的行为 -
基本类型和引用类型的区别
基本类型和引用类型的区别:
- 存储方式不同—基本类型数据存储在栈中,引用类型数据存储在堆中。
栈的操作性能高,速度快,存储量小;堆的操作性能低,存储量大 - 基本类型的变量保存原始值,引用类型的变量保存的存放数据的堆内存空间的地址;
- 操作方式不同—基本类型进行值传递,引用类型进行引用传递
- 存储方式不同—基本类型数据存储在栈中,引用类型数据存储在堆中。
-
什么是堆,什么是栈,有什么区别?
在数据结构中,栈中数据是先进后出。而堆是一个优先队列,是按优先级来进行排序的。在操作系统中,内存被分为栈区和堆区。栈区内存由编译器自动分配释放。其操作方式类似于数据结构中的栈。堆区的内存一般由程序员释放,如果程序员不释放,程序结束时可能由垃圾回收机制回收(引用计数法和标记清除法)
-
null和undefined的区别
null和undefined的区别:
- Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
- null 代表的含义是空对象,主要用于赋值给一些可能会返回对象的变量,作为初始化。 undefined 代表的含义是未定义,即此处应该有值,但还没定义,一般变量声明了但还没有定义的时候会返回 undefined。
- 在转换数字的时候,Number(null) 为 0,而 Number(undefined) 为 NaN
-
如何获取安全的undefined值
undefined 在 JavaScript 中不是一个保留字,这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。 -
代码执行时间
有时候我们需要统计一段代码执行的时间,通俗的做法是使用 Date() 但是这样不够优雅,javaScript 的 console 也可以帮助我们完成这个需求console.time('timer-1') console.log(1+1) console.timeEnd('timer-1')
1. Json对象
- JSON.stringify(object)
- JSON.parse(json)
<script> // JSON.stringify(obj/arr) :js对象(数组)转换为json对象(数组) var obj = { name:'小明', age:'19', }; console.log(obj); // {name: "小明", age: "19"} console.log(typeof obj); // object //JSON.stringify()将对象、数组转换成字符串 json = JSON.stringify(obj); console.log(json); // {"name":"小明","age":"19"} console.log(typeof json); // string // JSON.parse(json)将json对象(数组)转换为js对象(数组) var s = JSON.parse(json); // 将数据(数据是标准的 JSON 格式)转换成对象 console.log(s); // {name: "小小", age: "19"} console.log(typeof s); // object </script>
-
JSON.stringify()除了接收要序列化的object参数外,还可以接收另外两个参数。第一个是过滤器(可以是一个数组,也可以是一个函数[替换函数]),第二个参数是一个选项,表示是否在JSON字符串中保留缩进,如果第三个参数是数值表示用几个空格作为缩进;如果是字符串,则将该字符串用作缩进字符,缩进字符串最多不超过10个,超过十个,只出现前10个。
JSON.stringify(object,[key1,key2...]); //返回object对象中key1,key2...等属性的键值对组成的json字符串 //例: let book={ title:”aaa” authors:[”bbb”,”ccc”] edition:”3” year:2020 } JSON.stringify(book,[“title”,”edition”]); //返回结果: //{“title”:”aaa”,”edition”:3} //JSON.stringify(object,function(key,value){}) //根据函数键名判断如何处理处理要序列化的object中的属性,并返回对应字符串 JSON.stringify(book,function(key,value){ switch(key){ case “authors” return value.join(“,”) default return value } }); //返回结果: //{“title”:”aaa”,”authors”:”bbb,ccc”,”edition”:3,”year”:2020}
-
JSON.parse()也可以接收另一个参数,是一个函数,称为还原函数
2. Array对象
-
检测数组
Array.isArray(需要检测的数组); -
转换方法
join()方法,如果不给该方法传值,或者给他传入undefined,则使逗号作为分隔符。var color = ["red","green","blue"]; alter(colors.join("&"));//red&green&blue
-
栈方法
push()和pop()
push()方法向数组的末尾添加元素,并修改数组的长度
pop()方法移除数组末尾最后一项,减少数组的长度,并返回移除的项。var color = ["red","green","blue"]; color.push("yellow"); console.log(color[3]);//yellow let item = color.pop(); console.log(item);//yellow
-
队列方法
unshift()和shift()
unshift()方法向数组的开头添加元素,并修改数组的长度
shift()方法移除数组第一项,减少数组的长度,并返回移除的项。var color = ["red","green","blue"]; color.unshift("yellow"); console.log(color[0]);//yellow let item = color.shift(); console.log(item);//yellow
-
重排序方法
-
reverse()方法,反转数组
-
sort()方法,默认情况下按升序排列数组,可以添加一个比较函数作为sort()方法的参数,以便指定哪个值位于哪个值的前面。比较函数接受两个参数,如果第一个参数应该位于第二个参数之前则返回一个负数,如果第一个参数应该位于第二个参数之后则返回一个正数。如果两个参数相等则返回0。
var arr = [5,3,6,2]; function compare(value1,value2){ if(value1 < value2){ return -1; }else if(value1 > value2){ return 1; }else{ return 0; } } arr.sort(compare); console.log(arr);//2,3,5,6
-
indexOf(value) :得到value值在数组中出现的第一个下标(下标从0开始) 不存在返回-1
<script> var arr = [1,2,3,3,3,3,4,5,6,7,8]; console.log(arr.indexOf(3)); // 2 console.log(arr.indexOf(10)); //-1表示不存在 </script>
-
lastIndexOf(value) :得到value值在数组中出现的最后一个下标(下标从0开始) 不存在返回-1
<script> var arr = [1,2,3,3,3,3,4,5,6,7,8]; console.log(arr.lastIndexOf(3)); //5 console.log(arr.lastIndexOf(10));//-1 </script>
-
for(变量 in 循环数组){循环体}
<script> var arr = [1,2,3,3,3,3,4,5,6,7,8]; for(i in arr){ console.log(arr[i]); // 1,2,3,3,3,3,4,5,6,7,8 } </script>
-
forEach(function(item,index) {循环体} ) 遍历数组
对数组中的每一项运行给定函数,该方法没有返回值,本质上与for循环一样。也就是说,forEach()会修改原来的数组。<script> /* 数组.forEach(function(数组元素值,数组下标){ 循环体 }) 数组元素值: 自定义命名(value(item),key(index)) forEach 不能用于对象的循环使用 */ var arr = [1,2,3,3,3,3,4,5,6,7,8]; arr.forEach(function (item,index) { console.log(item); // 1,2,3,3,3,3,4,5,6,7,8 }); var obj = { name:"张三", age:19, }; obj.forEach(function(item,index){ console.log(item); }); //obj.forEach is not a function </script>
-
map((function(item,index) {循环体});
对数组中的每一项运行给定函数,返回每次函数调用结果组成的数组。<script> /* 数组.map(function(数组元素值,数组下标){ 循环体 }) */ // 根据arr1数组 新增的数组元素值比原来数组元素值贷1 var arr1 = [38,43,24,52,16,95]; console.log(arr1); // [38,43,24,52,16,95]; // for 循环 var newarr = []; for(var i=0;i<arr1.length;i++){ newarr.push(arr1[i]+1); } console.log(arr1); // [38, 43, 24, 52, 16, 95] console.log(newarr); // [39, 44, 25, 53, 17, 96] // map 遍历 var arr2 = arr1.map(function (item,index) { // item 得到数组的元素的值 return item+1; }); console.log(arr2); // [39, 44, 25, 53, 17, 96] </script>
-
filter((function(item,index) {循环体});
对数组中的每一项运行给定函数,返回调用函数返回值为true的项组成的数组。<script> /* 数组.filter(function(数组的元素值,数组下标){ 循环体 }) */ //对arr3数组中的每个元素进行过滤, //元素的值大于4,返回true //最后所有返回为true的元素过滤出组成一个新数组 //filter遍历过滤 const ar3 = [1, 2, 3, 4, 5, 6, 7, 8]; const newarr1 = arr3.filter(function (item,index) { return item>4; }); console.log(arr3); // [1, 2, 3, 4, 5, 6, 7, 8] console.log(newarr1); // [5, 6, 7, 8] </script>
-
every((function(item,index) {循环体});
对数组中的每一项运行给定函数,如果每一项都返回true,则返回true。<script> const ar1 = [1, 2, 3, 4, 5, 6, 7, 8]; let res = arr1.every(function (item,index) { return item>4; }); console.log(res); // false </script>
-
some((function(item,index) {循环体});
对数组中的每一项运行给定函数,如果有任意一项都返回true,则返回true。<script> const ar1 = [1, 2, 3, 4, 5, 6, 7, 8]; let res = arr1.some(function (item,index) { return item>4; }); console.log(res); // true </script>
-
concat()
concat()方法可以连接两个或多个数组,它不会影响原数组,而是将获得的新数组作为返回值返回<script> var colors = ["red","green","blue"]; var colors2 = colors.concat("yellow",["black","brown"]); alert(colors);//red,green,blue alert(colors2);//red,green,blue,yellow,black,brown </script>
-
slice(参数1[,参数2]);
- 获取在数组中位置为[参数1,参数2)范围内的数组元素,
- 如果没有参数2则获取数组中位置为[参数1,数组长度)范围内的数组。
- 如果参数为负数,则数组长度加上该数来确定相应的位置,
- 如果结束位置小于起始位置,则返回空数组。它不会影响原数组
<script> var colors = ["red","green","blue","yellow","black","brown"]; var colors2 = colors.slice(1,4); var colors3 = colors.slice(1); var colors4 = colors.slice(-4,-1); var colors5 = colors.slice(-1,-4); alert(colors);//red,green,blue,yellow,black,brown alert(colors2);//green,blue,yellow alert(colors3);//green,blue,yellow,black,brown alert(colors4);//blue,yellow,black alert(colors5);//空数组 </script>
-
splice()
- 删除:可以删除任意数量的项,只需指定2个参数:要删除的第一项的位置和要删除的项数。例如,splice(0,2)会删除数组中的前两项。
- 插入:可以向指定位置插人任意数量的项,只需提供3个参数:起始位置、0(要删除的项数) 和要插入的项。如果要插人多个项,可以再传入第四、第五,以至任意多个项。例如,splice(2,0, “red”, " green")会从当前数组的位置2开始插入字符串"red"和" green"
- 替换:可以向指定位置插入任意数量的项,且同时删除任意数量的项,只需指定3个参数:起始位置、要删除的项数和要插入的任意数量的项。插人的项数不必与删除的项数相等。例如, splice (2,1,“red” , " green")会删除当前数组位置2的项,然后再从位置2开始插入字符串"red"和"green"。
**
splice()方法始终都会返回一个数组,该数组中包含从原始数组中删除的项(如果没有删除任何项,则返回一个空数组)。
splice()方法会改变原数组<script> var colors = ["red","green","blue","yellow","black","brown"]; var colors2 = colors.splice(0,1); alert(colors);//green,blue,yellow,black,brown alert(colors2);//red var colors3 = colors.splice(1,0,"purple"); alert(colors);//green,purple,blue,yellow,black,brown alert(colors3);//空数组 var colors4 = colors.splice(3,1,"red"); alert(colors);//green,purple,blue,red,black,brown alert(colors4);//yellow </script>
3.函数Function
0. 创建函数的方式
-
方式一:函数声明
function XXX(){ //函数体 }
-
方式二:函数赋值表达式
var XXX = function(){ //函数体 }
-
方式三:使用构造函数创建函数
//[arg1[, arg2[, ...argN]],] 表示函数参数,可选 //functionBody表示函数体 let XXX= new Function ([arg1[, arg2[, ...argN]],] functionBody) //例: <script> var test1 = new Function('a','b','console.log(a+b)'); test1(1,2);//3 </script>
1. call()、apply()、bind()方法
call()、apply()、bind()方法的作用是改变函数运行时this的指向。这些方法都是Function的原型对象上面的方法。
1)、call()方法
call方法第一个参数是要绑定给 this 的值,后面传入的是一个参数列表,以逗号隔开即可。当第一个参数为 null、undefined的时候,默认this指向window。
- 例:
var obj = { message: 'My name is: '}; function getName(firstName, lastName){ console.log(this.message + firstName + lastName); } getName.call(obj, 'yang', 'wei');
2)、apply()方法
apply接收两个参数,第一个参数是要绑定给 this 的值,第二个参数是一个参数数组。当第一个参数为 null、undefined的时候,默认this指向window。
-
例:
var obj = { message: 'My name is: '}; function getName(firstName, lastName){ console.log(this.message + firstName + lastName); } getName.apply(obj, ['yang', 'wei']);
3)、bind()方法
bind()方法第一个参数是 this 的指向,从第二个参数开始是接收的参数列表。
-
bind()方法返回值是一个函数,bind()方法不会立即执行,而是返回一个改变了上下文 this 后的函数。
var obj = { name: 'yangwei' }; function printName(){ console.log(this.name); } //原函数 printName 中的 this 并没有被改变,依旧指向全局对象 window。 //以下bind()方法返回了一个this指向obj对象的函数 var yw = printName.bind(obj); //调用 yw();
* bind()方法参数使用
```javascript
function fn(a, b, c){
console.log(a, b, c);
}
var fn1 = fn.bind(null, 'yangwei');
fn('A', 'B', 'C'); // A B C
//fn1()方法的实参则是在 bind() 中传入参数的基础上再往后排
fn1('A', 'B', 'C'); // yangwei A B
fn1('B', 'C'); // yangwei B C
```
* 如果对一个函数【普通函数】进行多次 bind,不管我们给函数 bind 几次,函数 中的 this 永远由第一次 bind 决定。
```javascript
let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)() //Window
// 相当于
let fn2 = function fn1() {
return function() {
return fn.apply()
}.apply(a)
}
fn2()
```
4)、call()、apply()、bind()方法区别
- 事实上 apply 和 call 的用法几乎相同,唯一的差别在于:当函数需要传递多个变量时,apply可以接收一个数组作为参数输入,call则是接收一系列的单独变量。
- bind返回对应函数,便于之后调用;apply、call则是立即调用。在ES6的箭头函数下,call()、apply()、bind()方法将失效,因为箭头函数体内的 this 对象, 就是定义时所在的对象, 而不是使用时所在的对象;
5)、手写call、apply、bind函数
2. 函数防抖和节流
函数防抖和函数节流:优化高频率执行js代码的一种手段
(1). 防抖
在事件被触发n秒后再执行,如果在这n秒内又被触发,则重新计时。即在连续触发的事件中,只有最后一次的触发才执行
function debounce(fn,delay){
let timer = null //借助闭包
return function(...args) {
if(timer){
clearTimeout(timer)
timer = null
}
timer = setTimeout(() => {
fn.apply(this, args)
clearTimeout(timer)
timer = null
})
}
}
function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
console.log('滚动条位置:' + scrollTop);
}
window.onscroll = debounce(showTop,1000) // 为了方便观察效果我们取个大点的间断值,实际使用根据需要来配置
(2). 节流
在一段时间间隔内,无论触发多少次事件,只执行一次
function throttle(fn,delay){
//用来标记是否可以被触发。
let valid = true
return function(...args) {
if(!valid){
return false
}
// 工作时间,执行函数并且在间隔期内,标记为不可以被触发
valid = false
setTimeout(() => {
fn.apply(this, args)
valid = true;
}, delay)
}
}
function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
console.log('滚动条位置:' + scrollTop);
}
window.onscroll = throttle(showTop,1000)
节流和防抖分别适合什么场景?
- 节流:resize、scroll
- 防抖:input
4. 原型与原型链
1. 原型
原型,简单的来讲就是一个对象创建的模板,每个函数都有一个prototype属性,它默认指向一个Object空对象,即原型对象。原型对象中有一个属性constructor,它指向函数对象
2. 显示原型属性和隐式原型属性
-
每个函数function都有一个prototype属性,即为显示原型属性,它是在定义函数时自动添加的,默认是指向Object空对象(即原型对象)
-
每个实例对象都有一个__proto__,称为隐式原型属性,它是在创建对象时自动添加的,默认指向其构造函数的prototype属性值
console.log(Object.getPrototypeOf(实例对象) === 实例对象.__proto__) // true
-
实例对象的隐式原型属性的值指向其构造函数的显示原型属性的值
3. 原型链
当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去该对象的原型里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是我们平时所说的原型链的概念。
-
所有函数都是Function()构造函数的实例包括Function()构造函数本身,所以所有函数也有一个__proto__属性,指向Function()构造函数的原型,因此每个函数都有两个属性:prototype属性和__proto__属性
-
读取对象属性时会自动到原型链中查找
-
设置对象属性时不会查找原型链,如果当前对象中没有此属性,直接添加此属性并设置其值。
-
方法一般定义在原型中,属性一般通过构造函数定义在对象本身
-
instanceof是如何判断的?
表达式:
A instanceof B(instanceof用来判断内存中实际对象A是不是B类型)
如果B函数的原型对象在A对象的原型链上,返回true,否则返回false -
测试题:
- instanceOf和typeOf的区别
typeof和instanceof都是用来判断变量类型的,两者的区别在于:
-
typeof可以判断所有变量的类型,返回值字符串类型,值分别有’number’,‘boolean’,‘string’,‘undefined’,‘symbol’,‘function’,‘object’。
-
instanceof用来判断对象,代码形式为 A instanceof B,B必须为对象,否则会报错!
其返回值为布尔值。对A的原型链进行搜索,如果B的原型对象在A对象的原型链上,则返回true。
-
- instanceOf和typeOf的区别
-
自定义instanceOf
//A instanceof B,B的原型对象在A对象的原型链上 function myInstanceof(left, right) { let prototype = right.prototype left = left.__proto__ while (true) { if (left === null || left === undefined) return false if (prototype === left) return true left = left.__proto__ } }
原型链产生的问题
包含引用类型值的原型属性会被所有的实例共享,当一个实例的数据被改变后,其他实例的数据也会被改变。
检测属性位于对象本身还是来自于其原型链
4. new运算符原理
0、调用构造函数
const person = new People()
1、自动创建一个空对象
2、让空对象的__proto__属性【隐式原型属性】(IE没有该属性)指向构造函数的prototype属性【显式原型属性】
3、改变构造函数的this指向,初始化调用该构造器函数,把这个空对象与构造函数的this进行绑定
4、如果构造函数中没有显式返回其它对象,那么返回 this,即new 构造函数时创建的这个的新对象,否则,返回构造函数中显式返回的对象obj
//手写new
//func表示构造函数
//...args构造函数的参数
function _new(func,...args) {
// 第一步 创建新对象
let obj= {};
// 第二步 空对象的_proto_指向了构造函数的prototype成员对象
obj.__proto__ = func.prototype;
// 第三步 改变构造函数的this指向,使用apply初始化调用该构造器函数,把这个空对象与构造函数的this进行绑定
let result = func.apply(obj,args);
if (result && (typeof (result) == "object" || typeof (result) == "function")) {
// 如果构造函数执行的结果返回的是一个对象,那么返回这个对象
return result;
}
// 如果构造函数返回的不是一个对象,返回this指向的新对象
return obj;
}
5. Object对象
get和set都是js对象内置方法,用于设置和读取属性值。对象的属性本身都含有get()和set()方法,所以obj.name = "aaa"可以设置属性值,obj.name 可以获取属性值。
Object对象的属性分类:
//例:
var obj = {
//_name,_age,hobbies为数据属性
//_age 和 _name前面加上一个下划线标识此属性是一个私有属性,不是对外公开的的一个接口,这个是约定促成的惯例。
_name : 'jay',
_age : 24,
hobbies:["喝酒","抽烟","烫头"];
//name、age为访问器属性
//为属性name定义set方法,当执行语句obj.name = "xxx"时,自动调用该set()方法
//obj.name不能使用,因为没用定义get()方法
set name(val) {
console.log("设置_name");
this._name = val;
},
//obj.age = "xxx"不能使用,因为没用定义set()方法
//为属性age定义get方法,当执行语句obj.age时,自动调用该get()方法
get age() {
console.log("获取_age");
return this._age;
}
}
console.log(obj.age);
obj.name = "tom"
console.log(obj._name);
/*
* 执行结果:
* 获取_age
* 24
* 设置_name
* tom
*/
-
数据属性
对象数据属性的描述:- value :指定值。默认为undefined
- writable :标识当前属性值是否是可修改的。直接在对象上定义的属性,默认值为true。使用下面三个方法新增的属性,默认为false
- configurable:标识当前属性是否可以被删除。直接在对象上定义的属性,默认值为true。使用下面三个方法新增的属性,默认为false
- enumerable:标识当前属性是否能用for in枚举。直接在对象上定义的属性,默认值为true。使用下面三个方法新增的属性,默认为false
-
访问器属性
对象访问器属性的描述:- get:在读取属性时调用。默认为undefined
- set:在写入属性时调用。默认为undefined
- configurable:标识当前属性是否可以被删除。直接在对象上定义的属性,默认值为true。使用下面三个方法新增的属性,默认为false
- enumerable:标识当前属性是否能用for in枚举。直接在对象上定义的属性,默认值为true。使用下面三个方法新增的属性,默认为false
- Object.getOwnPeopertyDescriptor(object对象,对象的属性)方法,可以取得object对象的给定属性的描述,返回值是一个由描述作为属性的组成的对象。
1. Object.creat()方法
Object.create(prototype,[descriptors])
- prototype参数为原型对象,[descriptors]表示可新增的属性并为新增的属性进行描述或者对原有属性进行覆盖
- 作用:以指定对象为原型创建新的对象,并为新的对象指定新的属性,并对属性进行描述或者对原有属性进行覆盖
-
//以数据属性为例: //详解Object.create(prototype,[descriptors])方法 <script> var person = { name:"小迟迟", age:18, print:function(){ console.log(this.name); } } //以person为原型创建新的对象 //Object.create(prototype,[descriptors]) //prototype参数为原型对象,[descriptors]即可新增的属性并为新增的属性进行描述 var student = Object.create(person,{ sex:{ value:"男", //当前属性可修改 writable:true, //当前属性可删除 configurable:true, }, hobbies:{ value:["看书","看报纸","看电视"], /* for (var i in student) { console.log(i); }时该属性可被枚举出来 */ enumerable:true } }); console.log(student.sex); //修改属性值 student.sex = "女"; console.log(student.sex); //删除属性值 //delete student.sex; //console.log(student); for (var i in student) { console.log(i); } //**************************** let test = Object.create({x:123,y:345}); console.log(test);//{} console.log(test.x);//123 console.log(test.__proto__.x);//123 console.log(test.__proto__.x === test.x);//true </script>
2.Object.defineProperty() 方法
//Object.defineProperty(object,属性名,descriptors)
//Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改对象的现有属性,并返回此对象。
//object要操作的对象
//descriptors扩展的一个属性的相关描述
//vue2.X响应式原理的实现基础
const object1 = {
property1: "19"
};
Object.defineProperty(object1, 'property1', {
get:function(){
console.log('getter被触发')
return 'aa';
},
set:function(data){
console.log('setter被触发')
}
});
object1.property1 = 77;
console.log(object1.property1);
Object.defineProperty()应用
- es5定义一个常量(值只不能被改变)
Object.defineProperty(window, 'aa', { value: '1', writable: false }) aa = '2' // 值不能被改变,但不会报错,es6的const会报错
- 实现(a == 1 && a == 2 && a == 3)
let val = 0; // Object.defineProperty(window, 'a', { get() { return ++val; } }); console.log(a == 1 && a == 2 && a == 3) // true
3. Object.defineProperties()方法
Object.defineProperties(object,descriptors)
- object要扩展属性的对象,descriptors扩展的多个属性
- 作用:为指定对象定义扩展多个属性
//以访问器属性为例:
//详解Object.defineProperties(object,descriptors)
<script>
var obj = {
firstName:"娜娜",
lastName:"迟"
}
//Object.defineProperties(object,descriptors)
//object要扩展属性的对象
//descriptors扩展的属性
Object.defineProperties(obj,{
fullName:{
//get获取扩展属性的值,获取扩展属性值时get方法自动调用
get:function(){
return this.firstName+this.lastName;
},
//set设置扩展属性的值,监听扩展属性,当扩展属性发生变化时会自动调用
//自动调用会将变化的值作为实参注入到set函数中
set:function(data){
console.log(data);
//获取到改变的值后手动修改属性值
var names = data.split(' ');
this.firstName = names[0];
this.lastName = names[1];
}
}
});
console.log(obj.fullName);
obj.fullName = "猪猪 迟";
console.log(obj.fullName);
</script>
6. 面向对象高级
创建对象的方式
-
方式一:Object构造函数方式
先创建空的Object对象,在动态添加属性/方法
缺点:复用性差,代码冗余高<script> var person = new Object(); person.name = "迟小娜"; person.age = 21; person.setName = function(name){ this.name = name; } console.log(person.name); person.setName("小迟娜"); console.log(person.name); </script>
-
方式二:对象字面量方式
缺点:复用性差,代码冗余高<script> var p = { name:"小迟迟", age:20, setName:function(name){ this.name = name; } } console.log(p.name); p.setName("小娜娜"); console.log(p.name); </script>
-
方式三:简单工厂模式
缺点:无法判断类型<script> function creatObiect(name,age){ var obj = { name:name, age:age, setName: function(name){ this.name = name; } } return obj; } var p1 = creatObiect("小迟同学",21); var p2 = creatObiect("张颜齐",25); console.log(p1); console.log(p2); </script>
-
方式四:自定义构造函数模式
<script> //自定义构造函数 function Person(name,age){ this.name = name; this.age = age; this.setName = function(name){ this.name = name; } } var p1 = new Person("tom",20); console.log(p1.name); p1.setName("迟迟"); console.log(p1.name); Person("tom",20);//不使用new调用Person(),构造函数的属性和方法都添加给windows对象。 console.log(name); </script>
继承
- 继承的意义:
让子类拥有父类的资源,减少代码冗余,方便统一操作 - 继承的弊端:
耦合性比较强
1).原型链继承 - - - 继承属性和方法
子构造函数的原型指向父构造函数的一个实例对象
//例:
<script>
//父构造函数
function Supper(){
this.supProp = "supper p";
}
Supper.prototype.showSupperProp = function(){
window.alert("supper");
}
//子构造函数
function Sub(){
this.subProp = "sub p";
}
//子构造函数的原型指向父构造函数的一个实例对象
Sub.prototype = new Supper();
//让子类型原型的constructor指向子类型
Sub.prototype.constructor = Sub;
Sub.prototype.showSubProp = function(){
window.alter("sub");
}
var sub = new Sub();
sub.showSupperProp();
</script>
2).借用构造函数继承 - - - 继承属性
在子构造函数中通过call() / apply()函数调用父构造函数,继承父构造函数的属性
//例:
<script>
function Person(name,age){
this.name = name;
this.age = age;
}
function Student(name,age,price){
//相当于this.Person(name,age);
Person.call(this,name,age);
this.price = price;
}
var student = new Student("tom",20,20000);
window.alert(student.name);
</script>
3).组合继承(原型链继承+借用父类构造函数继承两种方法组合)
本质上:使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承,这样,既可以通过原型上定义的方法实现方法的复用,又能保证每个实例都有自己的属性。
//例:
<script>
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.setName = function(name){
this.name = name;
}
function Student(name,age,price){
//相当于this.Person(name,age);
//为了得到父类属性
Person.call(this,name,age);
this.price = price;
}
//原型链继承,为了得到父类属性
Student.prototype = new Person();
Student.prototype.constructor = Student;
Student.prototype.setPrice = function(price){
this.price = price;
}
var student = new Student("tom",20,20000);
window.alert(student.name);
student.setName("迟小娜");
window.alert(student.name);
</script>
4).原型式继承
思想:基于已有的对象创建新的对象。
//例:
<script>
//核心代码
function object(o){
function F(){};
F.prototype = o;
return new F();//新对象以o为原型创建并返回
}
var person = {
name:'tom',
friends:['one','two','three']
}
var person1 = object(person);
person1.name = 'krystal';
person1.friends.push('four');
var person2 = object(person);
person2.name = 'yoroll';
person2.friends.push('five');
console.log(person.friends);//["one", "two", "three", "four", "five"]
</script>
- Object.create()方法就是对原型式继承的一种规范
5).寄生式继承
思路:创建一个仅用于封装继承过程的函数。
//例:
<script>
function object(o){
function F(){};
F.prototype = o;
return new F();//新对象以o为原型创建并返回
}
var person = {
name:'tom',
friends:['one','two','three']
}
//核心代码
function aaa(person){
var clone = object(person);//原型式继承
clone.sayHi = function(){//增强新创建的对象
console.log("hi");
}
return clone;//返回先创建的对象
}
var person1 = aaa(person);
person1.sayHi();//hi
</script>
6).寄生组合式继承
寄生组合继承解决组合继承两次调用父类构造函数的问题,核心思想与组合式继承一样。最完美的继承,一般采用该方法。
原型链继承 =》借用构造函数继承 =》组合继承=》寄生组合式继承
//例:
<script>
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.setName = function(name){
this.name = name;
}
function Student(name,age,price){
//相当于this.Person(name,age);
//为了得到父类属性
Person.call(this,name,age);
this.price = price;
}
// function Temp(){};
// Temp.prototype = Person.prototype;
// Student.prototype = new Temp();
// 以上三句可以直接替换成下面这一句
Student.prototype = Object.create(Person.prototype)
Student.prototype.constructor = Student;
</script>
7).es6 — class继承
ES6为了提供更接近传统语言的写法,引入了Class (类)这个概念,作为对象的模板。通过class关键字,可以定义类。基本上,ES6的class可以看作是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象 原型的写法更加清晰、更像面向对象编程的语法而己。
知识点:
- class 声明类
- constructor 定义构造函数初始化,可以没有。
- extends继承父类,父类的静态方法和属性不能被继承
- super 调用父类构造方法
- static定义静态方法和属性,静态方法和静态属性属于类,不属于实例对象,只能通过 类名.XX / 类名.XX() 来调用。
- 父类方法可以重写
<script>
class Phone{
static userYear = 3;
//构造方法,方法名不能改变
constructor(name,price){
this.name = name;
this.price = price;
}
static startPhone(){
console.log("开机");
}
calls(){
console.log(this);//this为该class
console.log("我要使用"+this.name+"打电话");
}
}
const one = new Phone("huawei",4500);
one.calls();
console.log(one.userYear);//undefined
console.log(Phone.userYear);//3
Phone.startPhone();//开机
//one.startPhone();
//error:one.startPhone is not a function
//子类继承
class SmartPhone extends Phone{
constructor(name,price,color,size){
//super调用父类的构造方法
super(name,price);
this.color = color;
this.size = size;
}
play(){
console.log("玩游戏");
}
//父类方的重写
calls(){
console.log("我要使用"+this.name+"打视频电话");
}
}
const xiaomi = new SmartPhone("小米",2000,"红色",25);
xiaomi.calls();//我要使用小米打视频电话
</script>
7. 执行环境
1. 变量声明提升
通过var定义(声明)的变量,在定义语句之前就可以访问到,值为undefined。
- 面试题
2. 函数声明提升
通过function声明的函数,在声明语句之前就可以直接调用,称为函数声明提升
- 先执行函数声明提升再执行变量声明提升
-
提升存在的根本原因就是为了解决函数间互相调用的情况
//例如 function test1() { test2() } function test2() { test1() } test1()
3. 执行环境(执行上下文)
- 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
- 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
- 当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。
每个执行上下文都有一个与之关联的变量环境对象,执行环境中所定义的变量(函数执行环境包括函数的参数变量arguments)和函数都保存在这个对象中。
4. 执行环境栈
JavaScript 引擎是利用栈的这种结构来管理执行上下文的。可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。
- 例
每次进入一个新的执行环境时,都会创建一个用于搜索变量和函数的作用域链。
8. 作用域与作用域链
作用域是指某个变量合法的使用范围。
1).作用域分类
- 全局作用域 ---- < script >标签构成的全局作用域
- 局部作用域 ---- 函数可以创建局部作用域
- 没有块作用域(es6以前没有,es6中let和const声明的变量具有块级作用域)【if、for、while等大括号包裹的就是块作用域】
2).作用域的作用
隔离变量,不同作用域下同名变量不会有冲突
3).作用域与执行上下文的区别
4).作用域链
每次进入一个新的执行环境时,都会创建一个用于搜索变量和函数的作用域链。作用域链的前端,始终都是执行的代码所在环境的变量环境对象,下一个变量环境对象来自包含环境,再下一个变量环境对象来自下一个包含环境,直到全局执行环境,全局执行环境的变量环境对象始终是作用域链的最后一个对象。
- 延长作用域链:
有些语句可以在作用域的前端临时增加一个变量环境对象,该对象会在代码执行完成后被移除。
延长作用域链的情况:
- try-catch语句的catch块
创建一个新的变量环境对象,其中包含被抛出的错误对象的声明,挂到作用域链的前端。 - with语句
会将指定对象【with(obj)的obj】添加到作用域链的前端
var b = 10;
(function b(){
b = 20;
// 函数内部作用域,会先去查找是有已有变量b的声明,有就直接赋值20,确实有了呀。但发现了具名函数function b(){},拿此b做赋值;
// IIFE的函数⽆法进行赋值(内部机制,类似const定义的常量),所以赋值无效。
console.log(b); // fn b
console.log(window.b); // 10
})();
9. this指向
1. 浏览器环境下this指向
全局作用域中,非严格模式下 this 指向 window;在 use strict 指明严格模式的情况下就是 undefined(严格模式不允许 this 指向全局对象)。
所有函数内部的this,它的值本质上都是指向调用该函数的对象。
详细介绍函数内部的this指向:
-
函数直接调用(对于直接调用函数 来说,不管函数被放在了什么地方,this 一定是 window),this指向window【非严格模式下 this 指向全局对象。严格模式下,this 绑定到 undefined ,严格模式不允许this指向全局对象。】,相当于window.函数()【匿名函数的执行环境具有全局性,因此其this通常指向window】
function getSum() { console.log(this) //这个属于函数名调用,this指向window } getSum(); function getSum1() { console.log(this) //this指向window } function getSum2() { console.log(this) //this指向window getSum1(); } getSum2(); var getSum=function() { console.log(this) //实际上也是函数名调用,window } getSum(); (function() { console.log(this) //匿名函数调用,this指向window })()
-
作为对象的方法调用
对象.方法名(),this指向该对象var objList = { name: 'methods', getSum: function() { console.log(this) //objList对象 } } objList.getSum();
-
作为构造函数调用
new 构造函数名()function Person() { console.log(this); } //构造函数调用 //this始终指向new 该构造函数Person时自动创建的一个实例对象 var personOne = new Person();
-
函数的初始化调用
利用call和apply、bind来实现,this就是call和apply、bind对应的第一个参数,如果不传值或者第一个变量为null ,undefined时,this指向window。使用call / apply / bind 如果第一个参数是string,number,布尔值,其调用内部会调用相应的构造器String,Numer,Boolean将其转换为相应的实例对象,然后this指向该实例对象。function foo() { console.log(this); } foo.apply();//window foo.call('我是call改变的this值');//String {"我是call改变的this值"}
-
事件绑定:谁触发事件,函数里面的this指向的就是谁。
<body> <div id="aaa"></div> <div id="bbb"></div> <script> let aaa = document.getElementById("aaa"); let bbb = document.getElementById("bbb"); aaa.onclick = change;//this = <div id="aaa"></div> bbb.onclick = change;//this = <div id="bbb"></div> function change(){ this.style.backgroundColor = "red"; console.log(”this="+this); } </script> </body>
-
普通函数做为参数传递的情况【相当于函数直接调用】, 比如setTimeout, setInterval, 非严格模式下的this指向全局对象
-
箭头函数本身是没有this和arguments的,在箭头函数中的this的值实际上等于声明箭头函数的作用域的this。
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn(); // 10
arguments[0](); // 2, arguments: { 0: fn, 1: 1, length: 2 }
}
};
obj.method(fn, 1);
2. node环境下this指向
在node环境下没有window这个对象,除了以下两种情况外,this的指向与浏览器下的指向相同。
-
第一种情况,全局中的this指向的是module.exports,但是通过this无法直接输出module.exports,而是输出一个空对象。
console.log(this); //{} this.num = 10; console.log(this.num); //10 console.log(module.exports.num); //10
-
第二种情况,当函数直接调用时,在函数中的this指向的是global对象,和全局中的this不是同一个对象,简单来说,你在函数中通过this定义的变量就是相当于给global添加了一个属性,此时与全局中的this已经没有关系了。
//例子一: function fn(){ this.num = 20; } fn(); console.log(this); //{} console.log(this.num); //undefined console.log(global.num); //20 //例子二 function fn(){ function fn2(){ this.age = 18; } fn2(); console.log(this); //global console.log(this.age); //18 console.log(global.age); //18 } fn();
10. 前端内存处理
内存的生命周期
- 内存分配
声明变量和函数等时,js会自动分配内存 - 内存使用
访问变量、调用函数等操作称为内存的使用 - 内存回收
js的垃圾回收机制
js的垃圾回收机制
- 垃圾收集机制原理:垃圾回收器会按照固定的时间间隔(或代码执行中预定的收集时间), 周期性地执行这一操作:找出那些不再继续使用的变量,然后释放其占用的内存。
- js实现垃圾回收功能的方式: 标记清除和引用计数
1. 标记清除
标记清除是js中最常用的垃圾回收方式。
function speakLines(){
let night="天黑";//做个标记 ,进入环境
let speak=`开始狼人杀,${night}请闭眼`;//做个标记 ,进入环境
console.log(speak);
}
speakLines(); //当调用speakLines()这个函数的时候,进入speakLines()函数执行环境(执行上下文),将函数里的引用类型值标记为“进入环境”。
//从逻辑上讲,永远不能释放进入环境的引用类型值所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。
//函数执行完毕,当代码执行进入另一个执行环境(执行上下文)中时,speakLines()函数里的引用类型值标记为“离开环境”,等周期性回收垃圾时,根据标记来决定要清除哪些引用类型值进行释放内存
垃圾收集器在运行的时候会给存储在内存中的所有引用类型值都加上标记。然后清除环境中的引用类型值以及被环境中的变量引用的引用类型值的标记。还有标记的引用类型值将被视为垃圾,原因是环境中的变量已经无法访问到这些引用类型值了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的引用类型值即可回收它们所占用的内存空间。
2. 引用计数
- 另一种不太常见的垃圾收集策略叫做引用计数(reference counting)。引用计数的含义是跟踪记录每个值被引用的次数。
- 当声明了一个变量并将一个引用类型值赋值该变量时,则这个值的引用次数就是1.如果同一个值又被赋给另外一个变量,则该值得引用次数加1。相反,如果引用这个值的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。
- 在采用引用计数策略的实现中,当值出现循环引用时。它们的引用次数永远不会是 0,就永远无法回收。【涉及BOM和DOM的都存在循环引用问题,最好在不使用时手工断开原生js对象与BOM和DOM的链接】
内存泄漏
-
如果那些不再使用的变量所占用的内存没有被释放就会造成 内存泄露。
-
内存泄漏是指程序中己动态分配的堆内存,由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
-
常见的内存泄漏
- 全局变量【全局变量不会被垃圾回收,如果不再使用,需要赋值为null】
- 未被清除的定时器
- 闭包
-
内存泄漏的解决办法:简单的说就是把那些不再需要的,但是垃圾回收又收不走的的那些变量赋值为null,然后让垃圾回收处理掉;
11. 闭包
0).闭包是什么?
内部函数在定义时,引用了外部函数的变量,导致内部函数的作用域链中包含外部函数的活动对象(执行环境中定义的变量和函数都被保存在该对象中,包括函数执行环境的参数对象arguments)【也称变量环境对象】。所以,即使外部函数执行完毕后,其执行环境被销毁,内嵌函数仍能够访问外部函数活动对象中的实参、形参、局部变量和其它内部函数的现象称为闭包现象。该内嵌函数称为闭包函数。本质上闭包是将函数内部与函数外部连接起来的桥梁。
-
简单的闭包实例
function a(){ var i=0; function b(){ i++; alert(i); } return b; } var c = a(); c();//1 c();//2 c();//3
1).产生闭包的条件
- 函数嵌套(在一个函数中定义另一个函数)
- 内部函数引用了外部函数的数据(变量/函数)
- 外部函数被调用
2). 闭包的作用
- 使函数外部可以访问函数内部的变量。
- 使外部函数的变量在外部函数执行完后,仍然能存活在内存中,延长了局部变量的生命周期。
3). 闭包导致的问题
- 外部函数执行完后,外部函数中定义的变量没有释放,占用内存的时间会变长,容易造成内存泄漏
4). 闭包的应用
//立即调用函数
//立即执行函数可以当作是一个私有作用域(块级作用域)
//其作用域内部可以访问立即调用函数外部的变量,而外部环境是不能访问作用域内部的变量的
//立即执行函数是一个封闭的作用域,不会和外部作用域起冲突。
//创建并立即调用一个函数,既可以执行其中的代码,又不会在内存中留下该函数的引用。
//匿名函数自调用
(function(){
//块级作用域或私有作用域
})()
-
使用闭包模仿块级作用域
//示例一 //使用闭包模仿块级作用域 //使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题 <script> var myDivs = document.getElementsByClassName("item"); for(var i = 0;i<myDivs.length;i++){ (function(i){ myDivs[i].onclick = function(){ alert(i+1); } })(i) } </script>
-
for(var i =0;i<3;i++){ console.log(i) } // 0 1 2
-
-
使用闭包创建js模块模式
//示例二 //创建js模块模式 // 自定义js模块.js //使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题 (function(){ var msg = "你好"; function fn1(){ alert(msg+"fn1()"); } function fn2(){ alert(msg+"fn2()"); } //向外暴漏方法 window.module = { fn1, fn2 } })() //****************************************************** // 测试使用.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> <script src="自定义js模块.js"></script> <script> module.fn1(); module.fn2(); </script> </head> <body> </body> </html> //示例三 //jquery
5). 柯里化函数
* JS四种异步编程解决方案
- 异步回调函数
- Generator
- Promise
- async/await
12. 回调函数
什么是回调函数?
- 自定义的函数,但自己没有调用,却最终执行了的函数称为回调函数。
- 回调函数分类
异步回调函数例:
1)dom事件回调函数
2)定时器回调函数。setTimeout、setInterval
3)ajax请求回调函数
13. 事件机制(js执行机制)
-
js为什么有事件循环机制
因为js是单线程的
事件驱动模型(浏览器模型的运转流程)
事件轮询(event loop):从callback queue中循环取出回调函数放入执行栈中处理(一个接一个)
14. Web Workers
- 例:
//主线程
<body>
<input id="text" type="text" spellcheck="请输入要发送的消息"/>
<button id="btn">向分线程发送消息</button>
<script>
var btn = document.getElementById("btn");
btn.onclick = function(){
var mytext = document.getElementById("text").value;
//创建一个Worker对象,参数为分线程的js文件名
var worker = new Worker('worker.js');
//向分线程发送消息
worker.postMessage(mytext);
//绑定接收分线程传来的消息的监听
worker.onmessage = function(event){
window.alert(event.data);
}
}
</script>
</body>
//分线程
//worker.js
//分线程不能调用alter()方法
var onmessage = function(event){//不能用函数声明,必须这样写
//分线程通过event.data获取主线程发来的数据mytext
var mainMessage = event.data;
//将数据发送给主线程
postMessage(mainMessage+"aaaa");
}
15. js的深拷贝和浅拷贝
深拷贝和浅拷贝只针对像 Object, Array 这样的引用类型的复杂数据的。
//基本数据类型
var a = 1;
var b =a;
深浅拷贝的主要区别就是:复制的是引用(地址)还是复制的是实例。
1.浅拷贝
浅复制仅仅是复制的内存地址,换句话说,复制了之后,原来的变量和新的变量指向同一个东西,彼此之间的操作会互相影响,为浅拷贝。
例:
- Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
结果:<script> var obj = { name:'尹净汉', age:20, friends:['崔胜澈','洪知秀'] } var newObj = {} Object.assign(newObj,obj); obj.friends.pop(); console.log(obj); console.log(newObj); </script>
2.深拷贝
如果是在堆中重新分配内存,拥有不同的地址,但是值是一样的,复制后的对象与原来的对象是完全隔离,互不影响,为深拷贝。
-
手写深拷贝
其实实现一个深拷贝是很困难的,需要我们考虑好多种边界情况,比如原型链如何处理、DOM 如何处理等等,所以这里实现的深拷贝只是简易版,建议使用 lodash 的深拷贝函数。<script> //深拷贝 function deepCope(fromObj,toObj){ for(var key in fromObj){ var item = fromObj[key]; //判断是否是引用类型 if(item instanceof Object){ //实例.constructor即实例.__proto__.constructor //可以得到创建该实例的构造函数 var newItem = new item.constructor; //递归进行拷贝 deepCope(item,newItem); toObj[key] = newItem; }else{ toObj[key] = fromObj[key]; } } } var obj = { name:'尹净汉', age:20, friends:['崔胜澈','洪知秀'], pobo:{ name:'李硕珉', age:'18', nickname:[{name1:'DK'},{name2:'大壳'}] }, birth: new Date() } var newObj = {} deepCope(obj,newObj); obj.friends.pop(); console.log(obj); console.log(newObj); </script>
结果:
16. JavaScript从编译到执行分为四个步骤
-
词法分析
-
语法分析
-
预编译
预编译一般发生在全局代码执行前和函数执行前
- 首先全局代码执行前,创建全局作用域
1、创建全局对象(window)
2、函数声明提升
3、var声明的变量提升并赋值undefined【var声明的变量会挂载到全局对象上】 - 当有函数准备执行时:编译函数方法体,创建局部作用域
1、 建立函数的活动对象
2、 函数声明提升
3、 变量提升
3.1将形参变量为活动对象的属性,值为undefined,并分配内存
3.2 var声明的变量提升,赋值undefined,挂载到活动对象上
3.3实参赋值给形参
- 首先全局代码执行前,创建全局作用域
-
解释执行