前端JS面试
JavaScript
1、原始值和引用值类型和区别
原始值类型:Number、String、Boolean、Null、Undefined
引用值类型:Object、Array、Function、Date、RegExp
区别:原始值存储在栈中,引用值把引用变量存储在栈中,而实际的对象存储在堆中,每一个引用变量都有一个指针指向其堆中的实际对象
let a = 1;
let b = a;
a = 2;
console.log(b); // 1
原始变量赋值给另一个原始变量时,只是把栈中的内容复制给另一个原始变量,此时这两个原始变量互不影响
let a = [1,2,3,4];
let b = a;
a.push(5);
console.log(b); // [1,2,3,4,5]
a = [6];
console.log(b); // [1,2,3,4,5]
引用变量赋值给另一个引用变量时,各自的变量名存储在栈中,而实际对象的值指向堆中同一个地址,当变量a通过方法改变值时,实际上只改变堆中的内容,但地址不变,因此b的值也会改变;但是当变量a通过非方法改变值时,系统会为a重新创建一个堆区,a的指针指向新的堆地址,而b的指针仍然指向旧的堆地址
2、判断数据类型
1.typeof
typeof 对于原始数据类型除了 null 以外都能判断出来,但是对于引用数据类型,判断的结果都是 object
console.log(typeof 1); // number
console.log(typeof '1'); // string
console.log(typeof true); // boolean
console.log(typeof undefined); // undefined
console.log(typeof null); // object
console.log(typeof function () {}); // function
console.log(typeof []); // object
console.log(typeof {}); // object
2.instanceof
a instanceof b 判断 b 的原型对象是否在 a 的原型链上
原理如下:
function instanceOf(a, b) {
let bp = b.prototype;
let ap = a.__proto__;
while(true){
if(ap === bp){
return true;
}else if(ap === null){
return false;
}
ap = ap.__proto__;
}
}
instanceof 主要用于判断引用数据类型
console.log([] instanceof Array); // true
console.log([] instanceof Object); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
console.log(function(){} instanceof Object); // true
instanceof 也可以用来判断原始数据类型(null 和 undefined 除外)
let a = 1;
console.log(a instanceof Number); // false
为什么返回 false 呢?因为 instanceof 是 Object instanceof constructor
,如果不是 Object 都返回 false
console.log(new Number(1) instanceof Number); // true
console.log(typeof new Number(1)); // Object
通过 new Number() 生成的实例就是 object
3.Object.prototype.toString.call()
利用 Object 原型对象上的 toString 方法可以精确的判断各种数据类型
let test = Object.prototype.toString;
console.log(test.call(1)); // [object Number]
console.log(test.call('1')); // [object String]
console.log(test.call(true)); // [object Boolean]
console.log(test.call(null)); // [object Null]
console.log(test.call(undefined)); // [object Undefined]
console.log(test.call([])); // [object Array]
console.log(test.call({})); // [object Object]
4.constructor
通过实例对象原型上的 constructor 属性来判断数据类型(null 和 undefined 除外)
console.log(1.constructor === Number); // true
console.log(true.constructor === Boolean); // true
console.log("1".constructor === String); // true
console.log([].constructor === Array); // true
console.log(function(){}.constructor === Function); // true
console.log({}.constructor === Object); // true
3、类数组与数组的区别与转换
类数组:
- 拥有 length 属性,其他属性(索引)为非负整数(对象中的索引会被当做字符串来处理)
- 不具有数组的方法
- 类数组是一个普通对象,而数组是 Array 类型
常见的类数组有:
- 函数的参数
arguments
- DOM 方法返回的结果
- jQuery 对象(比如 $('div'))
类数组转换为数组:
-
Array.prototype.slice.call
const divs = document.querySelectorAll('div'); const newDivs = Array.prototype.slice.call(divs);
-
扩展运算符
const divs = document.querySelectorAll('div'); const newDivs = [...divs];
-
Array.from
const divs = document.querySelectorAll('div'); const newDivs = Array.from(divs);
4、数组的常见API
isArray():判断是否为数组
const arr = [1, 2, 3];
console.log(Array.isArray(arr)); // true
toString():将数组转换为以逗号分隔的字符串
const arr = [1, 2, 3];
console.log(arr.toString()); // 1,2,3
join():返回按照指定字符分隔的字符串
const arr = [1, 2, 3];
console.log(arr.join('-')); //1-2-3
concat():用于连接两个或多个数组,该方法不会改变原数组,而仅仅会返回被连接数组的一个副本
const arr = [1, 2, 3];
const arr1 = [4, 5, 6];
console.log(arr.concat(arr1)); // [1,2,3,4,5,6]
slice(start, end):截取数组中的元素,该方法不会改变原数组,而是返回一个子数组
start 和 end均为空时,截取数组所有元素
const arr = [1, 2, 3];
console.log(arr.slice()); // [1,2,3]
end 为空时,从 start 开始截取到数组结尾
const arr = [1, 2, 3];
console.log(arr.slice(1)); // [2,3]
end 不为空时,从 start 开始截取到 end 的前一位
const arr = [1, 2, 3];
console.log(arr.slice(1, 2)); // [2]
start 和 end 为负数时,从数组末尾开始计算
const arr = [1, 2, 3];
console.log(arr.slice(-3, -1)); // [1,2]
reverse():翻转数组
const arr = [1, 2, 3];
console.log(arr.reverse()); // [3,2,1]
sort():自定义排序
const arr = [1, 2, 3];
console.log(arr.sort(function (a, b) {
//return a - b; 从小到大排序
return b - a; // 从大到小排序
}));
splice():向数组中添加/删除元素,该方法会改变原数组
参数 | 描述 |
---|---|
index | 必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。 |
howmany | 必需。要删除的项目数量。如果设置为 0,则不会删除项目。 |
item1, ..., itemX | 可选。向数组添加的新项目。 |
添加元素
const arr = [1, 2, 3];
arr.splice(3,0,4,5);
console.log(arr); // [1,2,3,4,5]
删除元素
const arr = [1, 2, 3];
arr.splice(2,1);
console.log(arr); // [1,2]
替换元素
const arr = [1, 2, 3];
arr.splice(2,1,4);
console.log(arr); // [1,2,4]
push():向数组末尾添加一个或多个元素,并返回新的长度
const arr = [1, 2, 3];
console.log(arr.push(4)); // 4
console.log(arr); // [1,2,3,4]
unshift():向数组开头添加一个或多个元素,并返回新的长度
const arr = [1, 2, 3];
console.log(arr.unshift(0)); // 4
console.log(arr); // [0,1,2,3]
pop():删除数组末尾的元素
const arr = [1, 2, 3];
arr.pop();
console.log(arr); // [1,2]
shift():删除数组开头的元素
const arr = [1, 2, 3];
arr.shift();
console.log(arr); // [2,3]
entries():返回数组的可迭代对象
const arr = ['张三', '赵四', '王五'];
let iterator = arr.entries();
for(let v of iterator){
console.log(v);
}
/*
[0, "张三"]
[1, "赵四"]
[2, "王五"]
*/
every():检测数组的每个元素是否都符合条件,不会对空数组进行检测,不会改变原数组
const arr = [1, 2, 3];
const result = arr.every(item => item > 0 );
console.log(result); // true
fill():用一个固定值来填充数组
const arr = [1, 2, 3];
arr.fill(6);
console.log(arr); // [6,6,6]
filter():检测数组元素,并返回符合条件的所有元素的数组,不会对空数组进行检测,不会改变原数组
const arr = [1, 2, 3];
console.log(arr.filter(item => item>1)); // [2,3]
find():返回数组中符合条件的第一个元素,空数组不会执行,不会改变原数组
const arr = [1, 2, 3];
console.log(arr.find(item => item>1)); // 2
findIndex():返回数组中符合条件的第一个元素的索引,空数组不会执行,不会改变原数组,如果不存在,则返回 -1
const arr = [1, 2, 3];
console.log(arr.findIndex(item => item > 1)); // 1
console.log(arr.findIndex(item => item > 4)); // -1
forEach():遍历数组
const arr = [1, 2, 3];
arr.forEach(item => {
console.log(item);
})
from():从一个类数组或可迭代对象创建一个新的浅拷贝的数组
console.log(Array.from('foo'));
// ["f", "o", "o"]
console.log(Array.from([1, 2, 3], x => x + x));
// [2,4,6]
flat():按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回
扁平化嵌套数组
const arr1 = [1, 2, [3, 4]];
arr1.flat();
// [1, 2, 3, 4]
const arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();
// [1, 2, 3, 4, [5, 6]]
const arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2);
// [1, 2, 3, 4, 5, 6]
//使用 Infinity,可展开任意深度的嵌套数组
const arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
arr4.flat(Infinity);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
扁平化并移除数组空项
const arr4 = [1, 2, , 4, 5];
arr4.flat();
// [1, 2, 4, 5]
includes():用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false
const arr = [1,2,3];
console.log(arr.includes(1)); // true
indexOf():返回数组中给定元素的第一个索引,如果不存在,则返回-1
const arr = [1,2,3,1];
console.log(arr.indexOf(1)); // 0
console.log(arr.indexOf(1,2)); // 3
console.log(arr.indexOf(4)); // -1
reduce():对数组中的每个元素执行一个提供的函数(升序执行),并将结果汇总为单个返回值
const arr = [1,2,3,4,5,6,7,8,9,10];
// 累加必须提供初始值
console.log(arr.reduce((acc, cur) => acc + cur, 0)); // 55
map():创建一个新数组,其结果是该数组中的每个元素调用提供的函数后的返回值
const arr = [1,2,3];
console.log(arr.map(item => item*2));
// [2,4,6]
考点:
通常情况下,map
方法中的 callback
函数只需要接受一个参数,就是正在被遍历的数组元素本身。但这并不意味着 map
只给 callback
传了一个参数。例如:
const arr = ['1','2','3'];
console.log(arr.map(parseInt));
期望输出 [1,2,3]
,然而实际结果是 [1,NaN,NaN]
parseInt 经常被带着一个参数使用, 但是这里接受两个。第一个参数是一个表达式,第二个是callback function的基,Array.prototype.map
传递3个参数:
- the currentValue
- the index
- the array
第三个参数被parseInt忽视了, 但第二个参数会被使用
参数 | 描述 |
---|---|
string | 必需。要被解析的字符串。 |
radix | 可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。 |
上述的迭代步骤为:
// parseInt(string, radix) -> map(parseInt(value, index))
/* 1 */ parseInt('1',0); // 1
/* 2 */ parseInt('2',1); // NaN
/* 3 */ parseInt('3',2); // NaN 二进制只有0和1
解决方案:
function returnInt(element) {
return parseInt(element, 10);
}
['1', '2', '3'].map(returnInt); // [1, 2, 3]
// 指定基数即进制为10
// 只给parseInt传入当前元素值
['1', '2', '3'].map( item => parseInt(item) );
['1', '2', '3'].map(Number); // [1, 2, 3]
5、bind、call、apply的区别
bind():该方法会创建一个函数的实例,其 this 值会被绑定到传给 bind() 函数的值
语法:
var fn = Function.bind(obj, [param1[,param2][,...paramN]])
使用场景为函数不需要立即调用,但又想改变函数内部的 this 指向(比如定时器内部的 this)
const btn = document.querySelector('button');
btn.onclick = function () {
this.disabled = true;
setTimeout(function () {
this.disabled =false;
}.bind(this), 2000);
}
bind() 主要是为了改变函数内部的 this 指向
apply():apply() 方法接收两个参数,一个是在其中运行函数的作用域,另一个是参数数组(参数数组可以是数组实例,也可以是 arguments 对象)
语法:
Function.apply(obj, args)
// args将作为参数传递给Function
使用场景主要与数组有关
1.Math.max 实现得到数组的最大项
const arr = [1,2,3];
console.log(Math.max.apply(Math, arr)); // 也可以使用null,但严格模式下还是要使用Math
// 3
2.Array.prototype.push 实现合并两个数组
const arr1 = [1,2,3];
const arr2 = [4,5,6];
Array.prototype.push.apply(arr1, arr2);
console.log(arr1);
// [1,2,3,4,5,6]
call():call() 方法与 apply() 方法类似,接收参数的方式有些不同,第一个参数为在其中运行函数的作用域,其余参数都直接传递给函数,即传递给函数的参数必须逐个列举出来
语法:
Function.call(obj, [param1[,param2][,...paramN]])
// param参数列表会直接传递给Function
使用场景是可以实现继承
function Person(name, age) {
this.name = name;
this.age = age;
}
function Student(name, age, id) {
Person.call(this, name, age);
this.id = id;
}
let s = new Student('张三', 20, '007');
console.log(s);
// Student {name: "张三", age: 20, id: "007"}
6、new 的原理
- 在内存中创建一个空对象
- 给这个空对象添加属性和方法
- 将构造函数的 this 指定为创建的新对象,并将参数传入
- 如果构造函数没有手动返回对象,则返回第一步创建的对象,如果构造函数有返回对象,则 this 指向构造函数返回的对象
实现原理如下:
function Person(name) {
this.name = name;
}
function newObject(parent, ...args) {
let child = {};
child.__proto__ = parent.prototype;
parent.apply(child, args);
return child;
}
const p = newObject(Person, '张三');
console.log(p);// 张三
Person.prototype.sayName = function () {
console.log('我叫'+this.name);
};
p.sayName(); // 我叫张三
7、如何正确判断 this 的指向
this:谁调用它,this 就指向谁
1.普通函数:this 指向 window,严格模式下('use strict')会抛出错误 undefined
// 'use strict';
var name = '张三';
function fn() {
console.log(this.name);
}
fn();
2.对象函数:this 指向该函数所属对象
var obj = {
sayHello(){
console.log(this);
}
}
obj.sayHello();
// {sayHello: f}
3.构造函数:如果构造函数没有返回对象,则 this 指向创建的对象实例;如果构造函数有返回对象,则 this 指向返回的对象
function Person(name, age) {
this.name = name;
this.age = age;
}
var p = new Person('张三', 20);
console.log(p);
// Person {name: "张三", age: 20}
function Person(name, age) {
this.name = name;
this.age = age;
let obj = {
name: '赵四',
age: 18
};
return obj;
}
var p = new Person('张三', 20);
console.log(p);
// {name: '赵四', age: 18}
4.绑定事件函数:this 指向事件的调用者
var btn = document.querySelector('button');
btn.onclick = function () {
console.log(this);
}
// <button>点击</button>
5.定时器函数:this 指向 window
setTimeout(function () {
console.log(this);
},1000);
6.立即执行函数:this 指向 window
(function() {
console.log(this);
})();
7.箭头函数:不绑定 this,this 指向函数定义位置的上下文
btn.onclick = function () {
setTimeout(() => {
console.log(this);
// <button>点击</button>
},1000)
}
// 通过箭头函数可以改变定时器函数的this指向
8.显式绑定:函数通过 call()、apply()、bind()方法绑定,this 指向方法中传入的对象
function fn() {
console.log(this);
}
var person = {
name: '张三'
};
fn.call(person);
fn.apply(person);
fn.bind(person)();
// {name: '张三'}
如果这些方法中传入的第一个参数是 undefined 或 null,严格模式下 this 指向传入的值 undefined 或 null;非严格模式下 this 指向 window
function fn() {
console.log(this);
}
fn.call(null);// window
'use strict';
function fn() {
console.log(this);
}
fn.call(null); // null
9.隐式绑定:函数的调用时在某个对象上触发的,即调用位置存在上下文对象(相当于对象函数中的 this 指向)典型的隐式绑定为 xxx.fn()
function fn() {
console.log(this.name);
}
var person = {
name: '张三',
fn
};
person.fn(); // 张三
8、变量提升与函数提升
变量提升:将变量的声明提升到它所在作用域的顶端去执行,将赋值放在代码所在的位置(注意只有 var
才存在变量提升)
console.log(a);
var a = 1; // undefined
上述代码的实际执行顺序如下:
var a;
console.log(a);
a = 1;
而如果先进行赋值:
a = 1;
var a;
console.log(a); // 1
声明提升到顶端,所以输出1
console.log('1-'+v1);
var v1 = 100;
function foo() {
console.log('2-'+v1);
var v1 = 200;
console.log('3-'+v1);
}
foo();
console.log('4-'+v1);
// 1-undefined
// 2-undefined
// 200
// 100
函数提升:函数提升是整个代码块提升到它所在作用域的顶端执行
console.log(fn);
function fn () {
console.log(1);
}
/*
ƒ fn () {
console.log(1);
}
*/
执行顺序相当于:
function fn () {
console.log(1);
}
console.log(fn);
函数提升存在函数优先原则:
foo(); //1
var foo;
function foo () {
console.log(1);
}
foo = function () {
console.log(2);
}
9、作用域与作用域链、执行上下文
作用域:变量在某个范围内生效,目的是为了提高程序的安全性,减少命名冲突,分为全局作用域和局部作用域
- 全局作用域:script 标签,或者整个 js 文件
- 全局变量:
- 在全局作用域下的变量
- 在函数内部没用声明,直接赋值
- 只有在浏览器关闭时才会销毁,消耗内存
- 全局变量:
- 局部作用域:函数内部,变量只在函数内部生效
- 局部变量:
- 在局部作用域下的变量
- 函数的形参可以看做局部变量
- 当程序执行完毕就会销毁,节约内存资源
- 局部变量:
作用域链:一般情况下,变量的取值是到创建该变量的函数作用域下查找,但是如果在当前作用域下没有查找到,就会向上一级作用域查找,直到全局作用域,这样一个查找过程形成的链称为作用域链。作用域链相当于内部函数访问外部函数的变量,采取的是链式查找的方式来决定取哪个值。
//第一种情况,当函数作为参数
var x = 10;
function show(callback) {
var x = 5;
callback && callback();
}
function fun() {
console.log(x);// 10 函数fun的上级作用域是全局作用域
}
show(fun);
//第二种情况,当函数作为返回值输出
var x = 10;
function show() {
var x = 5;
return function() {
console.log(x);// 5 函数的上级作用域是show函数
}
}
var res = show();
res();
执行上下文:当代码运行时,会产生一个对应的执行环境,在这个环境中,所有变量会被提升,有的直接赋值,有的为默认值 undefined,代码从上往下开始执行,就叫做执行上下文,JavaScript 运行任何代码都是在执行上下文中运行
执行上下文分类:
- 全局执行上下文:不在任何函数中的代码都位于全局执行上下文中,代码首先进入的环境
- 函数执行上下文:每次调用函数时,都会为该函数创建一个新的执行上下文,相当于该函数被调用时执行的环境
- eval 函数执行上下文:运行在 eval 函数中的代码也有自己的函数执行上下文(不常用)
执行上下文栈:也叫调用栈,执行上下文栈用于存储代码运行期间创建的所有执行上下文,JS 代码首次运行,先创建一个全局执行上下文并压入栈中,之后每次函数调用,都会创建一个函数执行上下文并压入栈中,当函数调用完成后,这个函数执行上下文以及其中的数据都会被销毁,然后重新进入全局执行上下文
var a = 10 ; // 1.进入全局上下文环境
var fun;
var bar = function (x) {
var b = 10;
fun(x + b); // 3.进入fun上下文环境
}
fun = function (y) {
var c = 20;
console.log(y + c);
}
bar(5); // 2.进入bar上下文环境
执行上下文的生命周期:
- 创建阶段
- 创建变量:首先初始化函数的参数 arguments,提升函数声明和变量声明(预解析)
- 创建作用域链:作用域链本身包含变量,作用域链用于解析变量,当被要求解析变量时, JS 始终从代码嵌套的最内层开始查找,如果最内层没有找到,则会向上一级作用域查找,直到找到该变量
- 确定 this 的指向
- 执行阶段:变量赋值,代码执行
- 回收阶段:执行上下文出栈等待虚拟机回收执行上下文
执行上下文特点:
- 单线程,在主进程上运行
- 同步执行,从上往下按顺序执行
- 全局执行上下文只有一个,浏览器关闭时会被弹出栈(销毁)
- 函数执行上下文没有数目限制
- 函数每被调用一次,都会创建一个新的执行上下文环境
- 函数调用完毕时,函数执行上下文以及其中的数据都会被销毁
10、闭包及其作用
高阶函数
高阶函数时对其他函数进行操作的函数,它接收函数作为参数或将函数作为返回值;JavaScript 的回调函数是以实参形式传入其他函数中,也属于高阶函数
// 1.将函数作为参数(回调函数)
function fn(callback) {
callback && callback();
}
fn(function () {
console.log('hello');
})
// 2.将函数作为返回值
function fun() {
return function () {
console.log('world');
}
}
fun()();
变量作用域
- 函数内部不可以访问全局变量
- 函数外部不可以访问局部变量
- 当函数执行完毕,本作用域内的局部变量会被销毁
闭包
闭包指有权访问另一个函数作用域中的变量的函数,闭包允许函数访问局部作用域之外的数据,即使外部函数已经退出,外部函数中的变量仍然可以被内部函数访问到,闭包的主要作用:延伸了变量的作用范围
闭包实现的三个条件:
- 内部函数访问外部函数的变量
- 外部函数已经退出
- 内部函数仍然可以访问
function fn() {
var a = 1;
return function (b) {
a = a + b;
console.log(a);
}
}
var f = fn();
f(1); // 2
f(2); // 4
上述函数执行的时候,f 得到的是闭包对象的引用,fn 函数执行完毕退出,但是 fn 函数中的活动对象由于闭包的存在并没有被销毁,执行 f 函数仍然可以访问到 a 变量,而执行 f(2)后 a 变量的值为4,因为闭包的引用,f 并没有消除
闭包的核心内容:有些情况下(函数调用返回一个函数),函数调用完成之后,其执行上下文环境不会被销毁,所以使用闭包会增加内存开销,在 IE 中可能导致内存泄露,解决方法:在退出函数之前,将不使用的局部变量全部清除(变量赋值为null)
11、原型和原型链
原型:在 JavaScript 中,每一个函数都有一个 prototype 对象属性,指向另一个对象(原型对象),prototype 的所有属性和方法都会被构造函数的实例所继承。所以,我们可以把那些公共不变的方法,直接定义在 prototype 对象属性上 (一般情况下,公共属性定义在构造函数里,公共方法定义在原型对象上)
原型链:JavaScript 成员查找机制是按照原型链来查找的(就近原则)
- 当访问一个对象的属性(或方法)时,首先查找对象是否拥有该属性(或方法)
- 如果没有,就找它的原型(
__proto__
)指向的构造函数的原型对象(prototype) - 如果还没有,就找原型对象的原型指向的 Object 的原型对象
- 以此类推,递归访问
__proto__
,直到找到,找不到则为 null
12、prototype 与 __proto__
的关系与区别
prototype(显式原型属性):只有函数对象才具有 prototype 属性,这个属性指向一个对象,这个对象包含所有实例共享的属性和方法,这个对象也有一个属性 constructor,指回原构造函数
__proto__
(隐式原型属性):所有对象都具有该属性,指向构造该对象的构造函数的原型
13、继承的实现方式及比较
父类:
function Parent(name) {
this.name = name;
this.arr = [1,2,3];
}
Parent.prototype.showName = function () {
console.log(this.name);
}
1.原型继承:子类构造函数的原型等于父类构造函数的实例
function Child() {}
Child.prototype = new Parent();
var c = new Child();
console.log(c);
优点:实例可以继承构造函数的属性,父类构造函数的属性,父类构造函数原型的属性
缺点:
-
实例无法向父类构造函数传参
-
所有实例都会共享父类的引用类型属性
var c = new Child(); var c1 = new Child(); c.age = 20; c.arr.push(4); console.log(c1.arr); // [1,2,3,4]
2.借用构造函数:利用 call() 方法将父类构造函数引入子类构造函数
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
var c = new Child('张三', 20);
console.log(c);
优点:
- 实例可以向父类构造函数传参
- 父类原型属性不会共享
缺点:
- 只能继承父类构造函数的属性,不能继承原型属性
- 无法实现构造函数的复用(每次实例化都会重新调用)
3.组合继承:将原型继承和借用构造函数继承组合(常用)
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
var c = new Child('张三', 20);
console.log(c);
c.showName();
优点:
- 可以继承父类原型上的属性,实例可以向父类构造函数传参,构造函数可以复用
- 每个实例引入的构造函数属性是私有的
缺点:
- 调用了两次父类构造函数(消耗内存)
- 子类构造函数会代替原型上的父类构造函数
4.原型式继承:将一个函数的原型指向父类实例,然后返回这个函数的对象
function Child(obj) {
function F() {}
F.prototype = obj;
return new F();
}
var p = new Parent('张三');
var c = new Child(p);
console.log(c);
缺点:
- 所有实例都会共享父类的引用类型属性
- 子类实例化对象时无法传参
5.寄生式继承:给原型式继承再嵌套一层,实现传参
function Child(obj) {
function F() {}
F.prototype = obj;
return new F();
}
var p = new Parent('张三');
// 以上是原型式继承
function ChildObject(obj, age) {
var c = Child(obj);
c.age = age;
return c;
}
var c2 = new ChildObject(p, 20);
console.log(c2);
缺点:所有实例都会共享父类的引用类型属性
6.寄生组合式继承:寄生+组合实现继承
function Child(obj) {
function F() {}
F.prototype = obj;
return new F();
}
var c = new Child(Parent.prototype);
function ChildObject(name, age) {
Parent.call(this, name);
this.age = age;
}
ChildObject.prototype = c;
c.constructor = ChildObject;
var co = new ChildObject('赵四', 20);
console.log(co);
优点:
- 可以多重继承
- 解决调用两次父类构造函数的问题
- 解决实例共享父类引用类型属性的问题
深拷贝与浅拷贝
区分浅拷贝与深拷贝:假设 B 复制了 A,如果修改 B,A 也发生变化,就是浅拷贝;如果 A 没有发生变化,就是深拷贝
实现浅拷贝
1.for...in 循环赋值(只能拷贝第一层)
function simpleCopy(obj1) {
let obj2 = Array.isArray(obj1) ? [] : {};
for(let k in obj1){
obj2[k] = obj1[k];
}
return obj2;
}
let obj1 = {
a: 1,
b: 2,
c: {
d: 3
}
};
let obj2 = simpleCopy(obj1);
obj2.a = 3;
obj2.c.d = 4;
console.log(obj1.a); // 1
console.log(obj1.c.d); // 4
2.Object.assign
let obj2 = Object.assign(obj1);
obj2.a = 3;
obj2.c.d = 4;
console.log(obj1.a); // 3
console.log(obj1.c.d); // 4
3.直接用 =赋值
let obj2 = obj1;
obj2.a = 3;
obj2.c.d = 4;
console.log(obj1.a); // 3
console.log(obj1.c.d); // 4
实现深拷贝
1.递归拷贝所有层级属性
let obj1 = {
a: 1,
b: 2,
c: {
d: [1,2,3]
},
f: function () {
console.log('f');
}
};
function deepCopy(obj1) {
let obj2 = Array.isArray(obj1) ? [] : {};
for(let k in obj1){
if(typeof obj1[k] === "object"){
obj2[k] = deepCopy(obj1[k])
}else{
obj2[k] = obj1[k];
}
}
return obj2;
}
let obj2 = deepCopy(obj1);
console.log(obj2);
obj2.c.d.push(4);
console.log(obj1.c.d); // [1,2,3]
2.通过 JSON 对象来实现深拷贝
function deepCopy(obj1) {
let obj = JSON.stringify(obj1);
return JSON.parse(obj);
}
let obj2 = deepCopy(obj1);
缺点:
- 不能复制 function、正则、Symbol
- 循环引用报错
- 相同的引用会被重复复制
3.通过jQuery的extend方法实现深拷贝
var array = [1,2,3,4];
var newArray = $.extend(true,[],array); // true为深拷贝,false为浅拷贝
4.lodash函数库实现深拷贝
let result = _.cloneDeep(test)
14、函数防抖和节流
函数防抖和节流是为了解决用户在某一时间内频繁提交请求,给服务器造成压力的情况
函数防抖:在一定时间内,连续触发同一事件,只执行一次(只在最后一次执行或第一次执行)
定时器实现 :
// 只在最后一次执行
function debounce(fn, delay) {
let timer = null;
return () => {
if(timer){
//第一次触发时不会执行,后续触发时会清除定时器
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, arguments);
}, delay)
}
}
// 只在第一次执行
function debounce(fn, delay) {
let timer = null;
return () => {
if(timer){
clearTimeout(timer);
}
let callNow = !timer; // true
// 后续如果没有触发,则将timer初始化为null
timer = setTimeout(() => {
timer = null;
}, delay);
if(callNow){
fn.apply(this, arguments);
}
}
}
时间戳实现:
function debounce(fn, delay) {
let pre = 0;
return () => {
let now = new Date();
if(now - pre > delay){
fn.apply(this, arguments);
}
pre = now;
}
}
函数节流:在单位时间内,连续触发同一事件,只执行一次
定时器实现:
function throttle(fn, delay) {
let flag = true;
return () => {
if(!flag) return; // flag为false时,直接返回
flag = false;
setTimeout(() => {
fn.apply(this, arguments);
// 执行完fn函数再将flag重置为true
flag = true;
}, delay)
}
}
// 或者
function throttle(fn, delay) {
let timer = null;
return () => {
if(!timer){ // timer为null时才设置定时器
timer = setTimeout(() => {
//执行完fn函数后将timer重置为null
fn.apply(this, arguments);
timer = null;
},delay)
}
}
}
时间戳实现:
function throttle(fn, delay) {
let pre = 0;
return () => {
let now = new Date();
if(now - pre > delay){
fn.apply(this, arguments);
pre = now;
}
}
}
15、DOM常见的操作方式
1.查找节点
document.querySelector(selectors)
//接受一个CSS选择器为参数,返回第一个匹配该选择器的元素节点
document.querySelectorAll(selectors)
//接受一个CSS选择器为参数,返回所有匹配该选择器的元素节点
document.getElementById(id)
//返回匹配指定id属性的元素节点
2.生成节点
document.createElement(tagName)
// 用来生成HTML元素节点
document.createTextNode(text)
// 用来生成文本节点
document.createAttribute(name)
// 生成一个新的属性对象节点,并返回它
3.事件操作
document.addEventListener(type,listener,capture) // 注册事件
document.removeEventListener(type,listener,capture) // 注销事件
4.节点操作
Node.appendChild(node)
// 向节点添加最后一个子节点
Node.hasChildNodes()
// 返回布尔值,表示当前节点是否有子节点
Node.cloneNode(true);
// 默认为false(克隆节点), true(克隆节点及其属性,以及后代)
Node.insertBefore(newNode,oldNode)
// 在指定子节点之前插入新的子节点
Node.removeChild(node)
// 删除节点,在要删除节点的父节点上操作
Node.replaceChild(newChild,oldChild)
// 替换节点