1、说一说JavaScript的数据类型以及存储方式
JavaScript一共有8种数据类型
其中有7种基本数据类型:
ES5的5种:Null,undefined,Boolean,Number,String,
ES6新增:Symbol 表示独一无二的值
ES10新增:BigInt 表示任意大的整数
一种引用数据类型:
Object(本质上是由一组无序的键值对组成)
包含function,Array,Date等。JavaScript不支持创建任何自定义类型的数据,也就是说JavaScript中所有值的类型都是上面8中之一。
存储方式
基本数据类型:直接存储在栈内存中,占据空间小,大小固定,属于被频繁使用的数据。
引用数据类型:同时存储在栈内存与堆内存中,占据空间大,大小不固定。引用数据类型将指针存在栈中,将值存在堆中。
当我们把对象值赋值给另外一个变量时,复制的是对象的指针,指向同一块内存地址。
null 与 undefined的异同
相同点:
Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null
不同点:
undefined 代表的含义是已定义、未赋值, null 代表的含义是空对象。
typeof null 返回'object',typeof undefined 返回'undefined'
null == undefined // true
null === undefined // false
其实 null 不是对象,虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,
为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,
但是对于这个 Bug 却是一直流传下来。
2、JS判断数据类型的方法
① typeof:基本数据类型没有问题,引用数据类型有问题。
当变量是:number, string, boolean, function, undefined, object类型时,可以使用typeof进行判断。
当变量是arr, json, null, date, reg, error 类型时全部被错误的检测为object类型。
② instanceof:基本数据类型会有问题,而引用数据类型没有问题。优点:
```
//基本数据类型
console.log("1" instanceof String); //false
console.log(1 instanceof Number); //false
console.log(true instanceof Boolean); //false
//引用数据类型
console.log([] instanceof Array); //true
console.log(function(){} instanceof Function); //true
console.log({} instanceof Object); //true
```
③ constructor:除了undefined和null,其它变量都能使用constructor判断类型。
```
console.log(("1").constructor === String); //true
console.log((1).constructor === Number); //true
console.log((true).constructor === Boolean); //true
console.log(([]).constructor === Array); //true
console.log((function(){}).constructor === Function); //true
console.log(({}).constructor === Object); //true
console.log((null).constructor === Null); //报错
console.log((undefined).constructor === Undefined); //报错
```
```
声明了一个构造函数,并且把他的原型指向了Array的原型。
function Fn(){};
Fn.prototype=new Array();
var f=new Fn();
console.log(f.constructor===Fn); //false
console.log(f.constructor===Array); //true
```
*** \*原因:\ ****
1、array属于引用型数据,在传递过程中,仅仅是引用地址的传递。
2、每个页面的Array原生对象所引用的地址是不一样的,在子页面声明的array,所对应的构造函数,是子页面的Array对象;父页面来进行判断,使用的Array并不等于子页面的Array;切记,不然很难跟踪问题!
④ Object.prototype.toString.call();
```
console.log(Object.prototype.toString.call(1)); //[object Number]
console.log(Object.prototype.toString.call("1")); //[object String]
console.log(Object.prototype.toString.call(true)); //[object Boolean]
console.log(Object.prototype.toString.call([])); //[object Array]
console.log(Object.prototype.toString.call(function(){})); //[object Function]
console.log(Object.prototype.toString.call({})); //[object Object]
console.log(Object.prototype.toString.call(null)); //[object Null]
console.log(Object.prototype.toString.call(undefined)); //[object Undefined]
```
⑤ isArray() 判断是否是数组 isObject() 判断是否是对象 jquery中的$.type()
3、js数据类型转换
在JavaScript中类型转换有三种情况:
转换为数字(调用Number(),parseInt(),parseFloat()方法)
转换为字符串(调用.toString()或String()方法)
转换为布尔值(调用Boolean()方法)
null、undefined没有.toString方法
转换为数字
Number():可以把任意值转换成数字,如果要转换的字符串中有不是数字的值,则会返回NaN
```
Number('1') // 1
Number(true) // 1
Number('123s') // NaN
Number({}) //NaN
```
parseInt(string,radix):解析一个字符串并返回指定基数的十进制整数,radix是2-36之间的整数,表示被解析字符串的基数。
```
parseInt('2') //2
parseInt('2',10) // 2
parseInt('2',2) // NaN
parseInt('a123') // NaN 如果第一个字符不是数字或者符号就返回NaN
parseInt('123a') // 123
```
parseFloat(string):解析一个参数并返回一个浮点数
```
parseFloat('123a')
//123
parseFloat('123a.01')
//123
parseFloat('123.01')
//123.01
parseFloat('123.01.1')
//123.01
```
隐式转换
```
let str = '123'
let res = str - 1 //122
str+1 // '1231'
+str+1 // 124
```
转换为字符串
.toString() ⚠️注意:null,undefined不能调用
```
Number(123).toString()
//'123'
[].toString()
//''
true.toString()
//'true'
```
String() 都能转
```
String(123)
//'123'
String(true)
//'true'
String([])
//''
String(null)
//'null'
String(undefined)
//'undefined'
String({})
//'[object Object]'
```
隐式转换:当+两边有一个是字符串,另一个是其它类型时,会先把其它类型转换为字符串再进行字符串拼接,返回字符串
```
let a = 1
a+'' // '1'
```
转换为布尔值
0, ''(空字符串), null, undefined, NaN会转成false,其它都是true
Boolean()
```
Boolean('') //false
Boolean(0) //false
Boolean(1) //true
Boolean(null) //false
Boolean(undefined) //false
Boolean(NaN) //false
Boolean({}) //true
Boolean([]) //true
```
条件语句
```
let a
if(a) {
//... //这里a为undefined,会转为false,所以该条件语句内部不会执行
}
```
隐式转换 !!
```
let str = '111'
console.log(!!str) // true
```
4、{}和[]的valueOf和toString的返回结果?
valueOf:返回指定对象的原始值
对象 返回值
Array 返回数组对象本身。
Boolean 布尔值。
Date 存储的时间是从 1970 年 1 月 1 日午夜开始计的毫秒数 UTC。
Function 函数本身。
Number 数字值。
Object 对象本身。这是默认情况。
String 字符串值。
Math 和 Error 对象没有 valueOf 方法。
toString:返回一个表示对象的字符串。默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,
toString() 返回 "[object type]",其中 type 是对象的类型。
```
({}).valueOf() //{}
({}).toString() //'[object Object]'
[].valueOf() //[]
[].toString() //''
```
5、定义函数的方式
```
//第一种
function myFunction(){
}
//定义函数的字符串,函数名本质是变量名,指向某个function的引用。
console.log(myFunction);
//function
console.log(typeof myFunction)
//第二种
var myFunction = new Function(
"num1"
"num2"
"var sum = num1 + num2;return sum"
)
console.log(myFunction(10,20))
```
6.Object.is()与比较操作符==、===的区别?
==会先进行类型转换再比较
===比较时不会进行类型转换,类型不同则直接返回false
Object.is()在===基础上特别处理了NaN,-0,+0,保证-0与+0不相等,但NaN与NaN相等
==操作符的强制类型转换规则
字符串和数字之间的相等比较,将字符串转换为数字之后再进行比较。
其他类型和布尔类型之间的相等比较,先将布尔值转换为数字后,再应用其他规则进行比较。
null 和 undefined 之间的相等比较,结果为真。其他值和它们进行比较都返回假值。
对象和非对象之间的相等比较,对象先调用 ToPrimitive 抽象操作后,再进行比较。
如果一个操作值为 NaN ,则相等比较返回 false( NaN 本身也不等于 NaN )。
如果两个操作值都是对象,则比较它们是不是指向同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回true,否则,返回 false。
```
'1' == 1 // true
'1' === 1 // false
NaN == NaN //false
+0 == -0 //true
+0 === -0 // true
Object.is(+0,-0) //false
Object.is(NaN,NaN) //true
```
7.数组排序
```
var arr = [1,36,52,23,48,96,5];
//第一种:
function arrSort(a,b){
return a - b;
}
console.log(arr.sort(arrSort));
//第二种:冒泡排序
//思想:让数组当中相邻的两个数进行比较,数组当中比较小的数值向下沉,数值比较大的向上浮!
// 外层for循环控制循环次数,内层for循环控制相邻的两个元素进行比较。
function bubbleSort(arr){
for(var i = 0;i < arr.length-1;i++){
for(var j = 0;j < arr.length-1-i;j++){
if(arr[j] > arr[j+1]){
swap(arr,j,j+1)
}
}
}
return arr;
}
function swap(arr,i,j){
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
console.log(bubbleSort(arr));
//第三种:选择排序
//思想:以起始元素为基准,再从剩余的未排序的元素中挨个与起始元素进行比较,找到剩余未排序元素中
// 最小的一个与之交换位置。重复此步骤。
function selectSort(arr){
for(var i = 0;i < arr.length-1;i++){
for(var j = i+1;j < arr.length;j++){
if(arr[i] > arr[j]){
awap(arr,i,j)
}
}
}
return arr;
}
function swap(arr,i,j){
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
console.log(selectSort(arr))
//第四种:选择排序2
function quickSort(arr){
for(var i = 0;i < arr.length-1;i++){
var num = arr[i];
var index = i;
for(var j = i+1;j < arr.length;j++){
if(num > arr[j]){
num = arr[j];
index = j;
}
}
if(index != i){
swap(arr,i,index);
}
}
return arr;
}
function swap(arr,i,j){
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
var arr = [23,56,4,89,556,114,1];
console.log(quickSort(arr));
//第五种:快速排序
//1、从数组中取出一个数作为基准。在原数组中进行移动,将大于基准的数放到基准的右边,小于基准的数放到
// 基准左边,在基准左右形成两个子数组。在左右子数组中反复执行步骤1、2。直到所有子数组只剩下一个数。
function quickSort(arr,i,j){
if(i < j){
let left = i;
let right = j;
let pivot = arr[left];
while(i < j){
while(arr[j] >= pivot && i < j){
j--;
}
if(i < j){
arr[i++] = arr[j];
}
while(arr[i] <= pivot && i < j){
i++;
}
if(i < j){
arr[j--] = arr[i]
}
}
arr[i] = pivot;
quickSort(arr,left,i-1);
quickSort(arr,i+1,right);
return arr;
}
}
let arr = [23,56,4,89,556,114,1];
console.log(quickSort(arr,0,arr.length-1));
//快速排序(for循环)
function quickSort(arr){
//如果数组的长度小于等于1,则直接返回这个数组
if(arr.length <= 1){
return arr;
}
//选择基准数(四舍五入)
var pivotIndex = Math.floor(arr.length/2);
//将基准数与原数组分离
var pivot = arr.splice(pivotIndex,1)[0];
var left = [];
var right = [];
for(var i = 0;i < arr.length;i++){
if(arr[i] <= pivot){
left.push(arr[i]);
}else{
right.push(arr[i]);
}
}
return quickSort(left).concat(pivot,quickSort(right));
}
let arr = [23,56,4,89,556,114,1];
console.log(quickSort(arr));
```
8.深拷贝和浅拷贝?
浅拷贝:浅拷贝是会将对象的每个属性进行依次复制,但是当对象的属性值是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。
深拷贝:深拷贝复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制。 深拷贝后的对象与原来的对象是完全隔离的,互不影响,对一个对象的修改并不会影响另一个对象。
深拷贝和浅拷贝是针对复杂数据类型来说的,浅拷贝只拷贝一层,而深拷贝是层层拷贝;
浅拷贝(只能拷贝一层):Object.assign和for in进行{ }和[ ]的拷贝。
```
//拷贝1层(测试)------------------------------------------------------------
//Object.assign()拷贝方法
//拷贝{}
a = {name:"张三"};
b = Object.assign({},a);
b.name = "李四"; //拷贝完之后进行改变属性,测试拷贝的属性值改变原数据是否改变。
console.log(a,b);
//输出:{name:"张三"}
{name:"李四"} (拷贝成功)
//拷贝[]
a = ["1","2","3"];
b = Object.assign([],a);
b[1]="hello";
console.log(a,b)
//输出:["1","2","3"]
["1","hello","3"] (拷贝成功)
//for in拷贝方法
var copy = function(a){
var res = a.constructor();
for(var key in a){
if(a.hasOwnProperty(key)){
res[key] = a[key]
}
}
return res;
}
a = {name:"123",people:{name:"456"}};
b = copy(a);
b.people.name="hello";
a = ["a","b","c"];b = copy(a);
b[1] = 0;//拷贝完之后进行改变属性,测试拷贝的属性值改变原数据是否改变。
console.log(a,b)
//输出:["a","b","c"]
["a","0","c"] (拷贝成功)
//拷贝2层(测试)-------------------------------------------------------------
//Object.assign()拷贝方法
a = {name:"abc",people:{name:"张三"}};
b = Object.assign({},a);
b.people.name="李四";
console.log(a,b)
//输出:{name:"abc",people:{name:"李四"}}
{name:"abc",people:{name:"李四"}} (拷贝失败) //for in拷贝方法
var copy = function(a){
var res = a.constructor();
console.log(res);
for(var key in a){
if(a.hasOwnProperty(key)){
res[key] = a[key]
}
}
return res;
}
a = ["a","b",{name:"张三"}];b = copy(a);b[2].name="李四";
console.log(a,b)
//输出:{name:"abc",people:{name:"李四"}}
{name:"abc",people:{name:"李四"}} (拷贝失败)
```
***\*constructor( )\**** 是一种用于创建和初始化class创建的对象的特殊方法。
***\*hasOwnProperty( )\**** 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键);
***\*深拷贝最简单的实现是:JSON.parse ( JSON.stringify ( obj ) ),同时有一定的缺陷:\****
① 对象的属性值是函数时,无法拷贝
② 原型链上的属性无法拷贝
③ 不能正确的处理Data类型的数据
④ 不能处理RegExp
⑤ 会忽略symbol、undefined
```
//JSON.parse(JSON.stringify(obj))方法拷贝2层
var deepCopy = function(a){
return JSON.parse(JSON.stringify(a));
}
var a = {name:"aaa",people:{name:"abc"}};
var b = deepCopy(a);
b.people.name = "def";
console.log(a,b)
//输出:{name:"aaa",people:{name:"abc"}}
{name:"aaa",people:{name:"def"}} (拷贝成功)//JSON.parse(JSON.stringify(obj))方法拷贝3层
var deepCopy = function(a){
return JSON.parse(JSON.stringify(a))
}
var a = [1,2,{name:"aaa"}];
var b = deepCopy(a);
b[2].name = "bbb";
console.log(a,b);
//输出:["1","2",{name:"aaa"}]
["1","2",{name:"bbb"}] (拷贝成功)
//JSON.parse(JSON.stringify(obj))拷贝函数的时候
var deepCopy = function(a){
return JSON.parse(JSON.stringify(a));
}
var a = {name:"aaa",fun:function(){console.log(1)},age:undefined};
var b = deep(a);
b.name = "bbb"
console.log(a,b);
//输出:{name:"aaa",fun:function(){console.log(1)},age:undefined};
{name:"bbb"} (拷贝失败,只拷贝到了name属性)
//JSON.parse(JSON.stringify(obj))拷贝原型链上的属性
function Person(name){
this.name=name;
}
var a = new Person("Bob");
var b = deep(a);
console.log(a.constructor == Person); //true
console.log(b.constructor == Object); //true
//先不说拷贝出的值,光是数据类型已经不同了 (拷贝失败)
console.log(a,b)
//输出:
// Person{name:"Bob"} {name:"Bob"}
```
***\*注意:\* ***
上述方法会忽略值为function以及undefined的字段,而且对data类型的支持也不太友好。
上述方法只能克隆原始对象自身的值,不能克隆他继承的值。
***\*深拷贝(完美拷贝):\****
① 如果是基本数据类型,直接返回;
② 如果是RegExp或者Data类型,返回对应类型;
③ 如果是复杂数据类型,递归;
④ 考虑循环引用的问题。
```
function deepClone(obj,hash = new WeakMap()){ //递归拷贝
if(obj instanceof RegExp) return new RegExp(obj);
if(obj instanceof Date) return new Date(obj);
if(obj === null || typeof obj !== 'object'){
//如果不是复杂数据类型,直接返回
return obj;
}
if(hash.has(obj)){
return hash.get(obj);
}
//如果obj是数组,那么 obj.constructor 是 [Function: Array]
//如果obj是对象,那么 obj.constructor 是 [Function: Object]
let t = new obj.constructor();
hash.set(obj,t);
for(let key in obj){
//递归
if(obj.hasOwnProperty(key)){ //是否是自身的属性
t[key] = deepClone(obj[key],hash);
}
}
return t;
}
var show = {
name:"Bob",
fun:function(){console.log(1)},
age:null,
pic:undefined,
}
var show2 = deepClone(show);
show2.name="Mary"
console.log(show,show2) //拷贝成功,完美拷贝
```
***\*浅拷贝和深拷贝的区别:\****
浅拷贝只能复制对象或数组的第一层属性,而深拷贝是拷贝多层,每一级的数据都会被拷贝出来。
9.跨域(怎么解决跨域问题)(跨域是什么)(为什么会有跨域):
造成跨域的原因就是浏览器的同源策略:只要满足***\*协议\****、***\*主机\****、***\*端口\****一致,则两个页面具有相同的源。
同源策略限制了从同一个源加载的文档或脚本如何来自另一个源的资源进行交互,这是一个用于隔离潜在恶意文件的重要安全机制。
(直白:我在一个域名地址下的网页,如果请求一个受到同源策略限制的地址接口的时候,就会报错,
这是为了在我的网页下请求别的地 址的文件可能是恶意代码,造成不必要的问题。)
** 解决方法:**
① jsonp,允许script加载第三方资源。jsonp是一种非正式的传输协议,该协议的一个要点就是允许用户传递一个callback函数给服务. 端,
然后服务端返回数据的时候会将json数据包裹在这个callback函数中返回。jsonp的本质是利用script标签的src熟悉进行跨域请求,但只能. 用于get请求。
② 反向代理 (nginx 服务内部配置 Access-Control-Allow-Origin *);
③ cors 前后端协作设置请求头部,Access-Control-Allow-Origin 等头部信息
④ iframe 嵌套通讯 (可以与下面的postmessage一起使用)
⑤ window.postmessage ( ),该方法的使用须与iframe嵌套使用。
10.为什么会有同源策略?
JavaScript访问资源,处于安全方面考虑,要限制JS的访问能力,不允许跨域访问资源。如果没有同源限制存在浏览器中的cookie等其他数据可以任意读取,不同域下DOM任意操作,
ajax任意请求的话如果浏览了恶意网站那么就会泄漏这些隐私数据。
***\*(再次强调)同源策略:\****同协议、同域名、同端口。
11.原型、原型链、构造函数、实例、继承?
原型(__proto__):每个对象都有__proto__属性,__proto__指向创建他的构造函数的原型对象(prototype)。
原型链:凡是对象都有一个原型,通过__proto__可以访问原型,访问的原型又是对象,这样依次下去,就会构成一个对象的序列,该结构成为原型链。
(简单明了说法:一个原型对象是另一个原型对象的实例,相关的原型对象层层递进,就构成了实例与原型的链条,就是原型链。当访问一个对象的某个属性时,
会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,
如果还没有找到就会再在构造函数的prototype的__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链,当然如果最终找不到返回null。)
构造函数:
① 构造函数的首字母必须大写,用来区分于普通函数;
② 内部使用的this对象,来指向即将要生成的实例对象;
③ 使用new来生成实例对象。
12.arguments的解释?
arguments是一个类似于数组的对象,对应于传递给函数的参数 ,他有length属性,可以arguments[ i ]来访问对象中的元素,
但是它不能用数组的一些方法。
例如push、pop、slice等。arguments虽然不是一个数组,但是它可以转成一个真正的数组。
```
function argText(a,b,c){
var actual = arguments.length; //实际传参个数
var hope = argText.length //期望传参个数
console.log(actual,hope);
//转换数组:
var args = [].slice.call(arguments); //第一种
var args = Array.prototype.slice.call(arguments); //第二种
let args = Array.from(arguments); //第三种
let args = [...arguments]; //第四种
console.log(args)
}
argText(1,2)
//输出: 2 3
```
每一个函数都会有一个Arguments对象实例arguments,它引用着函数的实参,可以用数组下标的方式" [ ] "引用arguments的元素。
arguments.length为函数实参个数,
arguments.callee引用函数自身。
arguments对象是所有函数中可用的局部变量,你可以使用arguments对象在函数中引用函数的参数,此参数包含传递给函数的每个参数条目。
arguments.callee:Arguments的callee属性可以调用函数本身,当函数正在执行时才可调用,可以实现方法的递归调用。
```
function argText(){
var e = arguments.callee.toString();
console.log(e);
}
argText();
```
***\*arguments.caller:指向调用当前函数的函数\****
```
function argText(){
if(argText.caller){
var caller = argText.caller.toString();
console.log(caller);
}else{
console.log("no caller");
}
}
function handler(){
argText();
}
function copyHandler(){
handler();
}
argText()
//输出: no caller
handler()
//输出: function handler(){argText();}
copyHandler();
//输出: function handler(){argText();}
```
13.作用域、作用域链、闭包;
⑴ 作用域:简单来说,作用域是指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限
全局作用域:window。
局部作用域:函数内部定义的。
⑵作用域链
定义:一个函数在访问变量的时候,优先使用自己的局部变量,如果没有这个变量的申明,则向上级访问,一直访问到全局。全局都没有的话,语法错误:is not defined。
⑶闭包closure
定义:当一个函数的返回值是另外一个函数,而返回的那个函数如果调用了其父函数的内部变量,且返回的那个函数在外部被执行,就产生了闭包.
闭包是一个环境,具体指的就是外部函数–高阶函数
一个函数和对其周围(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样一个组合就是闭包(「closure」)
闭包的特性:
①函数嵌套函数;
②内部函数可以直接访问外部函数的内部变量或参数;
③变量或参数不会被垃圾回收机制回收。
闭包原理
函数执行分成两个阶段(预编译阶段和执行阶段)。
1.在预编译阶段,如果发现内部函数使用了外部函数的变量,则会在内存中创建一个“闭包”对象并保存对应变量值,
如果已存在“闭包”,则只需要增加对应属性值即可。
2.执行完后,函数执行上下文会被销毁,函数对“闭包”对象的引用也会被销毁,但其内部函数还持用该“闭包”的引用,
所以内部函数可以继续使用“外部函数”中的变量
利用了函数作用域链的特性,一个函数内部定义的函数会将包含外部函数的活动对象添加到它的作用域链中,函数执行完毕,其执行作用域链销毁,
但因内部函数的作用域链仍然在引用这个活动对象,所以其活动对象不会被销毁,直到内部函数被烧毁后才被销毁。
闭包的优点:
①可以从内部函数访问外部函数的作用域中的变量,且访问到的变量长期驻扎在内存中,可供之后使用;
②避免全局变量的污染;
③把变量存到独立的作用域,作为私有成员存在。
闭包的缺点:
①对内存消耗有负面影响。因内部函数保存了对外部变量的引用,导致无法被垃圾回收,增大内存使用量,所以使用不当会导致内存泄漏;
②对处理速度具有负面影响。闭包的层级决定了引用的外部变量在查找时经过的作用域链长度;
③可能获取到意外的值(captured value)。
应用场景
使用闭包找到dom元素的下标
```
var oLi = document.getElementsByTagName("li");
// 使用闭包后点击对应的li标签,会返回对应的下标
for(var i = 0;i < oLi.length;i++){
//闭包方式一
(function(j){
oLi[j].onclick = function(){
console.log(j)
}
})(i);
//闭包方式二
oLi[i].onclick = function(index){
return function(){
console.log(index);
}
}(i);
//不使用闭包的情况下,输出值全部为(length+1)
oLi[i].onclick = function(){
console.log(i);
}
}
```
模块封装,防止变量污染全局
```
var Person = (function(){
var name = 'tom'
function Person() {
console.log('work for qtt')
}
Person.prototype.work = function() {}
return Person
})()
```
循环体中创建闭包,保存变量
```
for(var i=0;i<5;i++){
(function(j){
setTimeOut(() => {
console.log(j)
},1000)
})(i)
}
```
14.说说你对prototype、constructor、proto、原型链的理解?
每个函数(类)天生自带一个属性prototype,属性值是一个对象,里面存储了当前类供实例使用的属性和方法 「(显示原型)」
在浏览器默认给原型开辟的堆内存中有一个constructor属性:存储的是当前类本身
(⚠️注意:自己开辟的堆内存中默认没有constructor属性,需要自己手动添加)「(构造函数)」
每个对象都有一个__proto__属性,这个属性指向当前实例所属类的原型
(不确定所属类,都指向Object.prototype)「(隐式原型)」
当你试图获取一个对象的某个属性时,如果这个对象本身没有这个属性,那么它会去它的隐式原型__proto__
(也就是它的构造函数的显示原型prototype)中查找。「(原型链)」
15.ES3~5:
***\*ES3~5数组常见的方法:\****
1、concat( ):数组合并。
2、join( ):数组转字符串。
3、pop( ):从尾部删除一个元素 返回被删除掉的元素,改变原有数组。
4、push( ):向数组的末尾追加 返回值是添加数据后数组的新长度,改变原有数组。
5、unshift( ):向数组的开头添加 返回值是添加数据后数组的新长度,改变原有数组。
6、reverse( ):反转,倒置 改变原有数组。
7、shift( ):从头部删除一个元素 返回被删除掉的元素,改变原有数组。
8、slice( ):数组元素的截取,返回一个新数组,新数组是截取的元素,可以为负值。
9、sort( ):按指定规则排序 改变原有数组;
10、splice( ):向数组的指定index处插入 返回的是被删除掉的元素的集合,会改变原有数组;
11、toString( ):数组转字符串;
12、toLocaleString( ):将数组转换为本地数组。
13、forEach( ):数组进行遍历;
14、map( ):没有return时,对数组的遍历。有return时,返回一个新数组,该新数组的元素是经过过滤(逻辑处理)过的函数。
15、filter( ):对数组中的每一运行给定的函数,会返回满足该函数的项组成的数组。
16、every( ):当数组中每一个元素在callback上被返回true时就返回true。(注:every其实类似filter,只不过它的功能是判断是不是数组中的所有元素都符合条件,并且返回的是布尔值)。
17、some( ):当数组中有一个元素在callback上被返回true时就返回true。(注:every其实类似filter,只不过它的功能是判断是不是数组中的所有元素都符合条件,并且返回的是布尔值)。
18、reduce( ):回调函数中有4个参数。prev(之前计算过的值),next(之前计算过的下一个的值),index,arr。把数组列表计算成一个单一值 。
```
//1、every()
var arr = [1,56,80,5];
var main = arr.every(n => n > 0);
console.log(main) //输出:true
//2、some()
var arr = [1,-56,80,-5];
var main = arr.some(n => n > 0);
console.log(main) //输出:true
//3、reducer()
var arr = [10,20,30,40]
let result = arr.reduce(function(prev,next,index,arr){
return prev + next;
})
console.log(result); //输出:100
// 4、filter 返回满足要求的数组项组成的新数组
var arr3 = [3,6,7,12,20,64,35]
var result3 = arr3.filter((item,index,arr)=>{
return item > 3
})
console.log(result3) //[6,7,12,20,64,35]
// 5、map 返回每次函数调用的结果组成的数组
var arr4 = [1,2,3,4,5,6]
var result4 = arr4.map((item,index,arr)=>{
return `<span>${item}</span>`
})
console.log(result4)
/*[ '<span>1</span>',
'<span>2</span>',
'<span>3</span>',
'<span>4</span>',
'<span>5</span>',
'<span>6</span>' ]*/
``
***\*ES3~5字符串常见的方法:\****
1、chartAt( ):返回在指定位置的字符;
2、concat( ):字符串连接;
3、indexOf( ):检索字符串,找不到返回-1;
4、slice( ):提取字符串片段,并在新的字符串中返回被提取的部分;
5、split( ):字符串转数组;
6、substr( ):从起始索引号提取字符串中指定数目的字符;
7、substring( ):提取字符串中两个指定的索引号之间的字符;
8、toLowerCase( ):字符串转小写;
9、toUpperCase( ):字符串转大写;
10、valueOf( ):返回某个字符串对象的原始值;
11、trim( ):删除字符串两边的空格;
16.ES6
***\*ES6数组的常用方法:\****
1、Array.from( ):将对象或字符串转成数组,注意得有length。
2、Array.of( ): 将一组值转换为数组。
3、copyWithin(target,start(可选),end(可选)):数组内数据的复制替换
target:从该位置开始替换数据;
start:从该位置开始读取数据,默认为0;
end:到该位置停止数据的读取,默认为数组的长度
4、find( ):用于找出第一个符合条件的数组成员。
5、findIndex( ):返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
6、fill(value,start,end):使用给定值,填充一个数组。
value:填充的值;
start:开始填充的位置;
end:填充结束的位置。
7、keys( ):对键名的遍历。
8、values( ):对键值的遍历。
9、entries( ):对键值对的遍历。
10、includes( ):数组原型的方法,查找一个数值是否在数组中,只能判断一些简单类型的数据,对于复杂类型的数据无法判断。该方法接受两个参数,分别是查询的数据和初始的查询索引值。
11、flat( ):用于数组扁平,数组去除未定义。
12、flatMap( ):对原数组的每个成员执行一个函数。
13、Map( ):是一组键值对的结构,具有极快的查找速度。
14、Set( ):Set和Map类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在Set中,没有重复的key。
```
//1、Array.from() -- Array.of()
var arrayLink = {
"0":"a",
"1":"b",
"2":"c",
length:3
}
var arr = Array.from(arrayLink)
console.log(arr) // 输出: [a,b,c]
console.log(Array.from("abcdefg")) //输出:["a", "b", "c", "d", "e", "f", "g"]
console.log(Array.of(1,2,3,4,5)) //输出: [1, 2, 3, 4, 5]
//2、copyWithin()
var arr = [1,2,3,4,5];
var main = arr.copyWithin(0,3);
console.log(main); //输出:[4,5,3,4,5]
//3、find()
var arr = [1,-5,2,9,-6];
var main = arr.find(n => n < 0);
console.log(main); //输出:-5
//4、fill()
var arr = ["a","b","c","d"];
console.log(arr.fill(7,1,2));//输出:["a",7,"c","d"]
//5、keys() values() entries()
var arr = ["a","b","c","d"];
for(let index of arr.keys()){
console.log(index);
}
for(let elem of arr.values()){
console.log(elem);
}
for(let [index,elem] of arr.entries()){
console.log(index,elem);
}
//6、includes()
let arr = [12,34,223,45,67]
console.log(arr.includes(45)) //输出:true
[1, 2, NaN].includes(NaN) // true
[1, 2, NaN].indexOf(NaN) // -1
//7、Map
var m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);
m.get('Michael'); // 95
//初始化Map需要一个二维数组,或者直接初始化一个空Map。Map具有以下方法:
var m = new Map(); // 空Map
m.set('Adam', 67); // 添加新的key-value
m.set('Bob', 59);
m.has('Adam'); // 是否存在key 'Adam': true
m.get('Adam'); // 67
m.delete('Adam'); // 删除key 'Adam'
m.get('Adam'); // undefined
//由于一个key只能对应一个value,所以,多次对一个key放入value,后面的值会把前面的值冲掉:
var m = new Map();
m.set('Adam', 67);
m.set('Adam', 88);
m.get('Adam'); // 88
//8、Set
//要创建一个Set,需要提供一个Array作为输入,或者直接创建一个空Set:
var s1 = new Set(); // 空Set
var s2 = new Set([1, 2, 3]); // 含1, 2, 3
//重复元素在Set中自动被过滤:
var s = new Set([1, 2, 3, 3, '3']);
s; // Set {1, 2, 3, "3"} 注意:数字3和字符串'3'是不同的元素
//通过add(key)方法可以添加元素到Set中,可以重复添加,但不会有效果:
s.add(4);
s; // Set {1, 2, 3, 4}
s.add(4);
s; // 仍然是 Set {1, 2, 3, 4}
//通过delete(key)方法可以删除元素:
var s = new Set([1, 2, 3]);
s; // Set {1, 2, 3}
s.delete(3);
s; // Set {1, 2}
```
***\*ES6字符串的常用方法:\****
1、for···of:遍历
2、模板字符串:` `
***\**ES6新增数据类型:Symbol\**\***
***\**ES10新增数据类型BigInt:BigInt数据类型的目的是比Number数据类型支持的范围更大的整数值\**\***
17.ES6解构赋值
***\*①数组的解构赋值:\****
```
let [a,b,c,d] = [1,2,3,4];
console.log(a,b,c,d);
let [x,y,...z] = [1,2,3,4,5,6,7,8];
console.log(x,y,z);
let arr = [1,2,3,4,5];
console.log(...arr); //输出:1 2 3 4 5
let [, , third] = ["foo","bar","baz"];
console.log(third); //输出: baz
let [x,y,...z] = ["a"];
console.log(x,y,z); //输出:"a" undefined []
//报错:
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
let [foo = true] = [];
console.log(foo) //输出: true
let [x = 1] = [undefined];
let [y = 1] = [null];
console.log(x) //输出: 1
console.log(y) //输出: 1
```
***\*②对象的解构赋值:\****
对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
```
let {foo,bar} = {bar:"acb",foo:"dce"};
console.log(foo,bar); //输出:dce abc
let {baz} = {foo:"aaa",bar:"bbb"};
console.log(baz); //输出:undefined
let {foo:baz} = {foo:"aaa",bar:"bbb"};
console.log(baz); //输出:aaa
console.log(foo); //输出:报错
let x;
{x} = {x:1} //输出:报错
let x;
({x} = {x:1});
console.log(x) //输出:1
```
***\*③字符串的解构赋值:\****
```
const [a,b,c,d,e] = "hello";
console.log(a,b,c,d,e) //输出:h e l l o
let {length : len} = "hello";
console.log(len); //输出: 5
```
***\*④函数参数的解构赋值\****
```
function add([x,y]){
return x + y;
}
console.log(add([1,2])); //输出:3
```
18.ES6解构赋值的用处
用处1:***\*交换两个变量的值\****
```
let a = 10,b = 20;
console.log([a,b] = [b,a]);
```
用处2:***\*从函数返回多个值\****
```
function fn(){
return [1,2,3,4,5];
}
var [a,b,c,d,e] = fn();
console.log(a,b,c,d,e);
```
用处3:***\*函数参数的定义\****
```
function fn3([x,y,z]){
return x+y+z;
}
console.log(fn3([4,5,6]))
```
用处4:***\*函数参数的默认值\****
```
function fn4([x=1,y=2,z=3]){
return x+y+z;
}
console.log(fn4([4,5,6]))
```
用处5:***\*提取JSON数据\****
```
var dataJson = {
"id":1,
"status":"ok",
"data":[1,2,3,4]
}
var {id,status,data:arr} = dataJson;
console.log(id,status,arr);
//输出: 1 "ok" [1,2,3,4]
```
用处6:***\*遍历Set、Map数据结构\****
```
var map = new Map();
map.set("first","hello");
map.set("second","world");
console.log(map);
for(var [key,value] of map){
console.log(key+"is:"+value)
}
```
用处7:***\*输入模块的指定方法\****
```
var {sourceSort,sourceNumber} = require("soure-map")
```
19.数组去重
```
var arr = [1,2,45,44,45,2,89,1,1,2,1,2];
```
***\*第一种:new Set()\****
```
var box = Array.from(new Set(arr));
var box = [...new Set(arr)];
console.log(box);
```
***\*第二种:indexOf()\****
```
var box = [];
for(var i = 0;i < arr.length;i++){
if(box.indexOf(arr[i]) == -1){
box.push(arr[i])
}
}
console.log(box);
```
***\*第三种:splice()\****
```
for(var i = 0;i < arr.length;i++){
for(var j = i+1; j < arr.length;j++){
if(arr[i] == arr[j]){
arr.splice(j,1);
}
}
}
console.log(arr);xxxxxxxxxx for(var i = 0;i < arr.length;i++){ for(var j = i+1; j < arr.length;j++){ if(arr[i] == arr[j]){ arr.splice(j,1); } }}console.log(arr);var box = Array.from(new Set(arr));var box = [...new Set(arr)];console.log(box);
```
***\*第四种:sort()+splice()\****
```
arr.sort();
for(var i = 0; i < arr.length;i++){
if(arr[i] == arr[i+1]){
arr.splice(i,1);
i--;
}
}
console.log(arr);
```
***\*第五种:递归函数\****
```
Array.prototype.unique = function(){
var arr = this;
len = arr.length;
arr.sort(function(a,b){
return a - b;
})
function loop(index){
if(index >= 1){
if(arr[index] === arr[index-1]){
arr.splice(index,1);
}
loop(index-1);
}
}
loop(len-1);
return arr;
}
var b = arr.unique();
console.log(b);
```
***\*简单写法如上,还有很多写法,在此不一一列举。\****
20.ES7新增(不定时添加)
**①** ***\*求幂运算符(\*\*)\****:
```
var x = 5 ** 3; // 5的三次方
console.log(x); //输出: 125
```
***\*② async、await异步解决方案\****:async函数返回的是一个 Promise 对象,可以使用 then 方法添加回调函数,async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。await是等待的意思,它等待的是promise 对象的执行完毕,并返回Promise 对象
### 16.ES8新增(不定时添加)
***\*① Object.entries( ):\****该方法会将某个对象的可枚举属性与值按照二维数组的方式返回。(如果目标对象是数组,则会将数组的下标作为键值返回)
```
var obj = {
name:"Bob",
age:25
}
var arr = Object.entries(obj)
console.log(arr); //输出:[['name', 'Bob'], ['age', 25]]
var arr = ["Bob","Tom"];
var Create = Object.entries(arr);
console.log(Create); //输出:[['0', 'Bob'], ['1', 'Tom']]
```
**2** ***\*Object.values():\****它的工作原理和Object.entries()方法很像,但是它只返回键值对中的值,结果是一维数组。
```
var obj = {
name:"Bob",
age:25
}
var arr = Object.values(obj);
console.log(arr); //输出:["Bob", 25]
var obj = {
2:"a",
1:"b",
3:"c"
}
var arr = Object.values(obj);
console.log(arr); //输出:["b", "a", "c"]
var obj = [1,3];
var arr = Object.values(obj);
console.log(arr); //输出:[1,3]
```
③字符串填充***\*padStart()、padEnd()\****:字符串填充方法,该方法可以使得字符串达到固定长度。它有两个参数,字符串目标长度和填充内容。
```
var str = "create"
var newStr1 = str.padStart(10,"x");
var newStr3 = str.padStart(6,"x");
var newStr2 = str.padEnd(10,"x");
var newStr4 = str.padEnd(6,"x");
console.log(newStr1,newStr2,newStr3,newStr4)
//输出:xxxxcreate createxxxx create create
```
***\*④ Object.assign()\****:方法用于对象的合并。
```
var obj1 = {name:"Bob"};
var obj2 = {age:25};
var newObj = Object.assign(obj1,obj2);
console.log(newObj); //输出: {name: "Bob", age: 25}
```
21.总结异步编程的6种方式:
① 回调函数;
② 事件监听;
③ 发布订阅模式;
④ Promise;
⑤ Generator(ES6);
⑥ async。
```
//Generator函数:
function* add(){
yield "1";
yield "2";
yield "3";
reutrn;
}
var h = add();
console.log(h.next()); //输出: {value:"1",done:false}
console.log(h.next()); //输出: {value:"2",done:false}
console.log(h.next()); //输出: {value:"3",done:false}
console.log(h.next()); //输出: 报错
//如果去掉return 则,输出:{value: undefined, done: true}
```
22.设计模式(下面只列举了常用10大)
***\**推荐学会前4种,了解后6种\**\***
***\*① 工厂模式:\****去做同样的事情,实现同样的效果,解决多个相似的问题这时候需要使用工厂模式
```
function CreatePerson(){
var obj = new Object();
obj.name = name;
obj.age = age;
obj.sayName = function(){
return this.name;
}
return obj;
}
var p1 = new CreatePerson("Bob",28);
var p2 = new CreatePerson("Tom",25);
```
(2)get请求中文参数出现乱码解决方法有两个:
①修改tomcat配置文件添加编码与工程编码一致,如下:
```
var Singleton = function(name){ //单体模式
this.name = name;
this.instance = null;
}
Singleton.prototype.getName = function(){
return this.name;
}
function getInstance(name){ //获取实例对象
if(!this.instance){
this.instance = new Singleton(name);
}
return this.instance;
}
//测试单体模式
var a = getInstance("Bob");
var b = getInstance("Tom");
console.log(a); //输出:Singleton {name: "Bob", instance: null}
console.log(b); //输出:Singleton {name: "Bob", instance: null}
console.log(a === b); //输出:true
```
***\*③ 模块模式:\****以对象字面量的方式来创建单体模式,***\*为单体模式添加私有变量和私有方法\****能够减少全局变量的使用。使用场景:如果我们必须创建一个对象并以某些数据进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么我们这个时候就可以使用模块模式了。
```
var singleMode = (function(){
var privateNum = 112; //创建私有变量
function privateFunc(){
//实现自己业务逻辑的代码
}
//返回一个对象包含公有方法和属性
return {
publicMethod1:publicMethod1,
publicMethod2:publicMethos1,
}
})()
```
***\*④ 代理模式:\****代理是一个对象,它可以用来控制对本体对象的访问,它与本体对象实现了同样的接口,代理对象会把所有的调用方法传递给本体对象的;代理模式最基本的形式是对访问进行控制,而本体对象则负责执行所分派的那个对象的函数或者类,简单的来讲本地对象注重的去执行页面上的代码,代理则控制本地对象何时被实例化,何时被使用;
```
//比如现在京东ceo想送给奶茶妹一个礼物,但是呢假如该ceo不好意思送,或者由于工作忙没有时间送,
//那么这个时候他就想委托他的经纪人去做这件事
var TeaAndMilkGirl = function(name) { // 先申明一个奶茶妹对象
this.name = name;
};
var Ceo = function(girl) { // 这是京东ceo先生
this.girl = girl;
this.sendMarriageRing = function(ring) { // 送结婚礼物 给奶茶妹
console.log("Hi " + this.girl.name + ", ceo送你一个礼物:" + ring);
}
};
var ProxyObj = function(girl){ // 京东ceo的经纪人是代理,来代替送
this.girl = girl;
this.sendGift = function(gift) { // 经纪人代理送礼物给奶茶妹
(new Ceo(this.girl)).sendMarriageRing(gift); // 代理模式负责本体对象实例化
}
};
// 初始化
var proxy = new ProxyObj(new TeaAndMilkGirl("奶茶妹"));
proxy.sendGift("结婚戒"); // Hi 奶茶妹, ceo送你一个礼物:结婚戒
```
***\*⑤ 职责链模式:\**** 消除请求的发送者与接收者之间的耦合。职责链是由多个不同的对象组成的,发送者是发送请求的对象,而接收者则是链中那些接收这种请求并且对其进行处理或传递的对象。请求本身有时候也可以是一个对象,它封装了和操作有关的所有数据。
***\*⑥ 命令模式:\****命令模式中的命令指的是一个执行某些特定事情的指令。命令模式使用的场景有:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道请求的操作是什么,此时希望用一种松耦合的方式来设计程序代码;使得请求发送者和请求接受者消除彼此代码中的耦合关系。
***\*⑦ 模板方法模式:\****模板方法模式由二部分组成,第一部分是抽象父类,第二部分是具体实现的子类,一般的情况下是抽象父类封装了子类的算法框架,包括实现一些公共方法及封装子类中所有方法的执行顺序,子类可以继承这个父类,并且可以在子类中重写父类的方法,从而实现自己的业务逻辑。
***\*⑧ 策略模式:\****定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
优点:1. 策略模式利用组合,委托等技术和思想,有效的避免很多if条件语句。
\2. 策略模式提供了开放-封闭原则,使代码更容易理解和扩展。
\3. 策略模式中的代码可以复用。
***\*⑨ 发布-订阅者模式:\****发布---订阅模式又叫观察者模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。
***\*⑩ 中介者模式:\****中介者模式的作用是解除对象与对象之间的耦合关系,增加一个中介对象后,所有的相关对象都通过中介者对象来通信,而不是相互引用,所以当一个对象发送改变时,只需要通知中介者对象即可。中介者使各个对象之间耦合松散,而且可以独立地改变它们之间的交互。
23.Ajax的原生写法:
```
var xhr; //创建ajax对象
if(window.XMLHttpRequest){ //兼容IE
xhr = new XMLHttpRequest();
}else{
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
xhr.open("get",url,true); //建立连接
xhr.send(); //发送
xhr.onreadystatechange = function(){ //获取数据
if(xhr.readyState == 4 && xhr.status == 200){
var data = JSON.parse(xhr.responseText);
}
}
```
24.图片的懒加载、预加载:
***\*原理:\****
****预加载原理:****就是在网页全部加载之前,提前加载图片,当用户需要查看时可直接从本地缓存中渲染,以提供给用户更好的体验,减少等待的时间。
****图片懒加载原理(缓载):通过监听onscroll事件判断资源位置,****延迟加载图片或符合某些条件时才加载某些图片。首先为所有懒加载的静态资源添加自定义属性字段,比如如果是图片,可以指定data-src为真实的图片地址,src指向loading的图片。 然后当资源进入视口的时候,将src属性值替换成data-src的值。 可以使用元素的getBoundingRect().top判断是否在视口内,也可以使用元素距离文档顶部的距离offsetTop和scrollTop是否小于视口高度来判断
```
//懒加载简单代码实现
//《JS代码》
window.onload = function(){
//获取当前浏览器视口高度
var viewHeight = document.documentElement.clientHeight;
console.log(viewHeight);
//鼠标滚动回调
function lazyload(){
var img = document.getElementsByClassName("img");
for(let item of img){
//获取每张图片距离顶部的距离
var imgHeight = item.getBoundingClientRect();
console.log(imgHeight)
//判断当图片出现在视口160px的时候把地址放入src中,显示出图片
if(imgHeight.top < (viewHeight - 10)){
item.src = item.getAttribute("data-original");
}
}
}
lazyload(); //页面加载时把当前视口中的图片加载进来
document.addEventListener("scroll",lazyload);
}
//《HTML代码》
<img class="img" lazyload="true" data-origin="图片http地址">
<img class="img" lazyload="true" data-origin="图片http地址"><img class="img" lazyload="true" data-origin="图片http地址"><img class="img" lazyload="true" data-origin="图片http地址"><img class="img" lazyload="true" data-origin="图片http地址">
```
户更好的体验,减少等待的时间。
****图片懒加载原理(缓载):通过监听onscroll事件判断资源位置,****延迟加载图片或符合某些条件时才加载某些图片。首先为所有懒加载的静态资源添加自定义属性字段,比如如果是图片,可以指定data-src为真实的图片地址,src指向loading的图片。 然后当资源进入视口的时候,将src属性值替换成data-src的值。 可以使用元素的getBoundingRect().top判断是否在视口内,也可以使用元素距离文档顶部的距离offsetTop和scrollTop是否小于视口高度来判断
```
//懒加载简单代码实现
//《JS代码》
window.onload = function(){
//获取当前浏览器视口高度
var viewHeight = document.documentElement.clientHeight;
console.log(viewHeight);
//鼠标滚动回调
function lazyload(){
var img = document.getElementsByClassName("img");
for(let item of img){
//获取每张图片距离顶部的距离
var imgHeight = item.getBoundingClientRect();
console.log(imgHeight)
//判断当图片出现在视口160px的时候把地址放入src中,显示出图片
if(imgHeight.top < (viewHeight - 10)){
item.src = item.getAttribute("data-original");
}
}
}
lazyload(); //页面加载时把当前视口中的图片加载进来
document.addEventListener("scroll",lazyload);
}
//《HTML代码》
<img class="img" lazyload="true" data-origin="图片http地址">
<img class="img" lazyload="true" data-origin="图片http地址"><img class="img" lazyload="true" data-origin="图片http地址"><img class="img" lazyload="true" data-origin="图片http地址"><img class="img" lazyload="true" data-origin="图片http地址">
```
```
//预加载简单代码实现
//style
#box{width:0;height:30px;background:red;transition:1s;}
//html
<div id="box"></div>
<ul>
<li><img src="图片http地址"></li>
<li><img src="图片http地址"></li>
<li><img src="图片http地址"></li> <li><img src="图片http地址"></li>
<li><img src="图片http地址"></li> <li><img src="图片http地址"></li> <li><img src="图片http地址"></li> <li><img src="图片http地址"></li>
</ul>
//JS代码
window.onload = function(){
var oBox = document.getElementById("box");
var arr = [
"https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3916481728,2850933383&fm=26&gp=0.jpg",
"https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=291222564,3369340489&fm=26&gp=0.jpg",
"https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=3974834430,2578081919&fm=26&gp=0.jpg",
"http://img3.imgtn.bdimg.com/it/u=2332284096,307149879&fm=26&gp=0.jpg",
"http://img0.imgtn.bdimg.com/it/u=2667939876,1138687338&fm=26&gp=0.jpg",
"http://img0.imgtn.bdimg.com/it/u=2981065320,3294183154&fm=26&gp=0.jpg",
"http://img4.imgtn.bdimg.com/it/u=2647953258,2919321872&fm=26&gp=0.jpg",
"http://img1.imgtn.bdimg.com/it/u=221171515,1209752772&fm=26&gp=0.jpg"
]
var num = 0;
for(var i = 0; i < arr.length;i++ ){
var oImg = new Image(); //新建一个图片对象
console.log(oImg);
oImg.onload = function(){
num++; //图片预加载完成,执行onload里面的代码
oBox.style.width = num/arr.length * 100 + "%"; //通过图片的加载顺序,来控制进度条的宽度
}
oImg.src = arr[i]; //给图片对象添加路径,注意这条代码必须加在onload后面
}
}
```
25.防抖、节流
防抖:所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
节流:所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。两种方式可以实现,分别是时间戳版和定时器版。
```
//节流
// 思路:在规定时间内只触发一次
function throttle (fn, delay) {
// 利用闭包保存时间
let prev = Date.now()
return function () {
let context = this
let arg = arguments
let now = Date.now()
if (now - prev >= delay) {
fn.apply(context, arg)
prev = Date.now()
}
}
}
function fn () {
console.log('节流')
}
addEventListener('scroll', throttle(fn, 1000))
```
```
//防抖
// 思路:在规定时间内未触发第二次,则执行
function debounce (fn, delay) {
// 利用闭包保存定时器
let timer = null
return function () {
let context = this
let arg = arguments
// 在规定时间内再次触发会先清除定时器后再重设定时器
clearTimeout(timer)
timer = setTimeout(function () {
fn.apply(context, arg)
}, delay)
}
}
function fn () {
console.log('防抖')
}
addEventListener('scroll', debounce(fn, 1000))
```
26.页面加载进度条的实现
①定时器加载(原理):设置固定的时间后将遮罩层和加载图片隐藏,显示页面内容。
②通过加载状态事件实现进度条(原理):document.onreadystatechange(页面加载状态改变时的事件),document.readyState(返回当前文档的状态);
③通过CSS3来实现进度条;
④实时获取加载数据的进度条(原理):通过加载图像来实现效果。
⑤根据加载进度来改变进度条的长度(width值);
⑥根据文件的加载顺序来实现加载进度条。
27.this关键字(指向)
this是JavaScript语言的一个关键字,它是函数运行时,在函数体内部自动生成一个对象,只能在函数体内部使用。
函数的不同使用场合,this有不同的值。总的来说this就是函数运行时所在的环境对象。
this的绑定规则有四种:默认绑定,隐式绑定,显式绑定,new绑定.
①函数是否在 new 中调用(new绑定),如果是,那么 this 绑定的是new中新创建的对象。
②函数是否通过 call,apply 调用,或者使用了 bind (即硬绑定),如果是,那么this绑定的就是指定的对象。
③根据加载进度来改变进度条的长度(width值);函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this 绑定的是那个上下文对象。一般是 obj.foo()
④如果以上都不是,那么使用默认绑定。如果在严格模式下,则绑定到 undefined,否则绑定到全局对象。
⑤如果把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind, 这些值在调用时会被忽略,实际应用的是默认绑定规则。
⑥箭头函数没有自己的 this, 它的this继承于上一层代码块的this。
情况一:纯粹的函数调用:这是函数的最通常的用法,属于全局调用,因此this就代表全局对象
var x = 1;
function test(){
console.log(this.x);
}
test(); // 1
情况二:作为对象方法复制代码情况二:作为对象方法的调用:函数还可以作为某个对象的方法调用,这时this就指这个上级对象。f
unction test(){
console.log(this.x);
}
var obj = {};
obj.x = 1;
obj.m = test;
obj.m();
情况三:
作为构造函数调用:所谓构造函数,就是通过这个函数,可以生成一个新对象。这时this就指这个新对象。
function test(){
this.x = 1;
}
var obj = new test();
console.log(obj.x); // 1
情况四:apply的调用:apply( )是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。
因此这时this指的就是这第一个参数。apply( )的参数为空时,默认调用全局对象。这时运行结果为0,证明this指的是全局对象。
var x = 0;
function test(){
console.log(this.x);
}
var obj = {};
obj.x = 1;
obj.m = test;
obj.m.apply(); // 0 this指的是全局对象
obj.m.apply(obj); // 1 this指的是obj
情况五:隐式绑定:函数的调用是在某个对象上触发的,即调用位置上存在上下文对象,典型隐士调用:
xxx.fn()
function info(){
console.log(this.age);
}
var person = {
age: 20,
info
}
var age = 28;
person.info(); //20;执行的是隐式绑定
情况六:箭头函数:箭头函数没有自己的this,继承外层上下文绑定的this;
let obj = {
age: 20,
info: function() {
return () => {
console.log(this.age); //this继承的是外层上下文绑定的this
}
}
}
let person = {age: 28};
let info = obj.info();
info(); //20
let info2 = obj.info.call(person);
info2(); //28
28.对象和面向对象
对象:属性和方法的集合叫做对象(万物皆对象)。
面向对象:首先就是找对象,如果该对象不具备所需要的方法或属性,那就给它添加。
面向对象是一种编程思维的改变。通过原型的方式来实现面向对象编程。
创建对象的方式(4种):new Object、字面量、构造函数、原型。
29、函数式编程含义:
函数式编程是一种强调以函数为主的软件开发风格。通过组合纯函数,避免共享状态、可变作用和副作用来构建软件的过程。
目的:使用函数来抽象作用在数据之上的控制流和操作,从而在系统中消除副作用并减少对状态的改变。
特点:函数式编程是声明性的而不是命令式的,应用状态流经纯函数中。相比于面向对象编程,其中的应用状态经常是共享的,并且和方法一起定义在一些对象中。
30、怎么判断两个对象是否相等?
① 首先比较两个对象的长度,如果长度不相等使flag为false,为不相等;
② 如果长度相等那就遍历对象1(对象2也可以),利用hasOwnProperty()方法查看对象1中是否包含对象2中的属性或者方法,如果不包含则使flag为false,为不想等。
③ 接下来判断两对象的内存地址是否相同,不同则为true
```
function compreObj(obj1, obj2) {
var flag = true;
function compre(obj1, obj2) {
if (Object.keys(obj1).length != Object.keys(obj2).length) {
flag = false;
} else {
for (let x in obj1) {
if (obj2.hasOwnProperty(x)) {
if (obj1[x] !== obj2[x]) {
compre(obj1[x], obj2[x]);
}
} else {
flag = false;
}
}
}
if (flag === false) {
return false;
} else {
return true;
}
}
return compre(obj1, obj2)
}
console.log(compreObj(对象1, 对象2));
```
31、事件模型:事件委托、代理?如何阻止事件冒泡?
事件模型
DOM0级模型: ,这种模型不会传播,所以没有事件流的概念,但是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,
也可以通过 js属性来指定监听函数。这种方式是所有浏览器都兼容的。
IE 事件模型: 在该事件模型中,一次事件共有两个过程,事件处理阶段,和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。
然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。
这种模型通过 attachEvent 来添加监听函数,可以添加多个监听函数,会按顺序依次执行。
DOM2 级事件模型: 在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从 document 一直向下传播到目标元素,
依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,
事件绑定的函数是 addEventListener,其中第三个参数可以指定事件是否在捕获阶段执行。
事件委托
事件委托指的是把一个元素的事件委托到另外一个元素上。一般来讲,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,
当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。
事件传播(三个阶段)
1.捕获阶段–事件从 window 开始,然后向下到每个元素,直到到达目标元素事件或event.target。
2.目标阶段–事件已达到目标元素。
3.冒泡阶段–事件从目标元素冒泡,然后上升到每个元素,直到到达 window。
事件捕获
当事件发生在 DOM 元素上时,该事件并不完全发生在那个元素上。在捕获阶段,事件从window开始,一直到触发事件的元素。
window----> document----> html----> body ---->目标元素
事件冒泡
事件冒泡刚好与事件捕获相反,当前元素---->body ----> html---->document ---->window。当事件发生在DOM元素上时,该事件并不完全发生在那个元素上。
在冒泡阶段,事件冒泡,或者事件发生在它的父代,祖父母,祖父母的父代,直到到达window为止。
如何阻止事件冒泡
w3c的方法是e.stopPropagation(),IE则是使用e.cancelBubble = true。例如:
```
window.event?window.event.cancelBubble = true : e.stopPropagation();
```
return false也可以阻止冒泡。
32、JS延迟加载的方式
JavaScript会阻塞DOM的解析,因此也就会阻塞DOM的加载。所以有时候我们希望延迟JS的加载来提高页面的加载速度。
1.把JS放在页面的最底部
2.script标签的defer属性:脚本会立即下载但延迟到整个页面加载完毕再执行。该属性对于内联脚本无作用 (即没有 「src」 属性的脚本)。
3.是在外部JS加载完成后,浏览器空闲时,Load事件触发前执行,标记为async的脚本并不保证按照指定他们的先后顺序执行,
该属性对于内联脚本无作用 (即没有 「src」 属性的脚本)。
4.动态创建script标签,监听dom加载完毕再引入js文件
33、window的onload事件和domcontentloaded
window.onload:当一个资源及其依赖资源已完成加载时,将触发onload事件。
document.onDOMContentLoaded:当初始的HTML文档被完全加载和解析完成之后,
DOMContentLoaded事件被触发,而无需等待样式表、图像和子框架的完成加载。
区别:
①onload事件是DOM事件,onDOMContentLoaded是HTML5事件。
②onload事件会被样式表、图像和子框架阻塞,而onDOMContentLoaded不会。
③当加载的脚本内容并不包含立即执行DOM操作时,使用onDOMContentLoaded事件是个更好的选择,会比onload事件执行时间更早。
34、for…of,for…in,forEach,map的区别?
for...of(不能遍历对象)
在可迭代对象(具有 iterator 接口)(Array,Map,Set,String,arguments)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句,不能遍历对象
for...in
for...in循环:遍历对象自身的和继承的可枚举的属性, 不能直接获取属性值。可以中断循环。
①从遍历数组角度来说,for···in遍历出来的是key(即下标),for···of遍历出来的是value(即数组的值);
```
var arr = [99,88,66,77];
for(let i in arr){
console.log(i); //0,1,2,3
}
for(let i of arr){
consoel.log(i); //99,88,66,77
}
```
②从遍历字符串的角度来说,同数组一样。
③从遍历对象的角度来说,for···in会遍历出来的为对象的key,但for···of会直接报错。
```
var obj = {name:"Bob",age:25};
for(var i in obj){
console.log(i) // name age
}
for(var i of obj){
console.log(i) //报错
}
// 我们发现它是不可以的 我们可以搭配Object.keys使用
for(let item of Object.keys(obj)){
console.log(obj[item])
}
// Bob 25
```
forEach
forEach: 只能遍历数组,不能中断,没有返回值(或认为返回值是undefined)。
```
let arr=[1,2,3];
const res = arr.forEach(item=>{
console.log(item*3)
})
// 3 6 9
console.log(res) //undefined
console.log(arr) // [1,2,3]
```
map
map: 只能遍历数组,不能中断,返回值是修改后的数组。
```
let arr=[1,2,3];
const res = arr.map(item=>{
return res+1
})
console.log(res) //[2,3,4]
console.log(arr) // [1,2,3]
```
总结
forEach 遍历列表值,不能使用 break 语句或使用 return 语句
for in 遍历对象键值(key),或者数组下标,不推荐循环一个数组
for of 遍历列表值,允许遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等.
在 ES6 中引入的 for of 循环,以替代 for in 和 forEach() ,并支持新的迭代协议。
for in循环出的是key,for of循环出的是value;
for of是ES6新引入的特性。修复了ES5的for in的不足;
for of不能循环普通的对象,需要通过和Object.keys()搭配使用。
35、函数柯里化(卡瑞化、加里化)?
概念:把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
容易理解的概念:Currying概念其实很简单,只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数(主要是利用闭包实现的)。
特点:
①接收单一参数,将更多的参数通过回调函数来搞定;
②返回一个新函数,用于处理所有的想要传入的参数;
③需要利用call/apply与arguments对象收集参数;
④返回的这个函数正是用来处理收集起来的参数。
作用:能进行部分传值,而传统函数调用则需要预先确定所有实参。如果你在代码某一处只获取了部分实参,然后在另一处确定另一部分实参,这个时候柯里化和偏应用就能派上用场。
用途:我认为函数柯里化是对闭包的一种应用形式,延迟计算、参数复用、动态生成函数(都是闭包的用途)。
```
function add(x,y){ //普通函数
console.log(x+y);
}
function curryingAdd(x){ //柯里化函数(闭包)
return function(y){
console.log(x+y);
}
}
add(1,2) //3
curryingAdd(1)(2) //3
```
36、HTML5常用的几种存储方式
本地存储:localStorage, sessionStorage, cookies
离线缓存:application cache
前端数据库:indexedDB, webSQL
37、JS预解析(变量提升),它导致了什么问题??
JS代码在执行前,浏览器会对js代码进行扫描,默认的把所有带var和function声明的变量进行提前的声明或者定义,遵循先解析后使用的原则。
变量提升的表现是,在变量或函数声明之前访问变量或调用函数而不会报错。
原因
JavaScript引擎在代码执行前有一个解析的过程(预编译),创建执行上线文,初始化一些代码执行时需要用到的对象。
当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,
它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。
首先要知道,JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。
1.在解析阶段
JS会检查语法,并对函数进行预编译。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,
变量先赋值为undefined,函数先声明好可使用。在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,
不过函数执行上下文会多出this、arguments和函数的参数。
全局上下文:变量定义,函数声明
函数上下文:变量定义,函数声明,this,arguments
2.在执行阶段,就是按照代码的顺序依次执行。
那为什么会进行变量提升呢?主要有以下两个原因:
1、提高性能
2、容错性更好
(1)提高性能 在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,
那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。
在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、
不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),
并且因为代码压缩的原因,代码执行也更快了。
(2)容错性更好 变量提升可以在一定程度上提高JS的容错性,看下面的代码:
```
a = 1
var a
console.log(a) //1
```
如果没有变量提升,这段代码就会报错
导致的问题
```
var tmp = new Date();
function fn(){
console.log(tmp);
if(false){
var tmp = 'hello nanjiu';
}
}
fn(); // undefined
```
在这个函数中,原本是要打印出外层的tmp变量,但是因为变量提升的问题,内层定义的tmp被提到函数内部的最顶部,
相当于覆盖了外层的tmp,所以打印结果为undefined。
```
var tmp = 'hello nan jiu';
for (var i = 0; i < tmp.length; i++) {
console.log(tmp[i]);
}
console.log(i); // 13
```
由于遍历时定义的i会变量提升成为一个全局变量,在函数结束之后不会被销毁,所以打印出来13。
总结
解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行
函数是一等公民,当函数声明与变量声明冲突时,变量提升时函数优先级更高,会忽略同名的变量声明
38、call、applyd的区别,原生JS实现bind。(call,apply,bind三者用法和区别:角度可为参数、绑定规则,运行效率、运行情况)。
定义:
apply():调用一个对象的一个方法,用另一个对象替换当前对象,例如:B.apply(A,arguments);即A对象应用B对象的方法。
call():调用一个对象的一个方法,用另一个对象替换当前对象,例如:B.call(A,args1,args2,···);即A对象调用用B对象的方法。
作用:
①改变this指向;
②借用别的对象的方法;
③单纯的调用函数;
④实现继承;
```
function add(a,b){
return a+b;
}
function sub(a,b){
return a-b;
}
var a1 = add.apply(sub,[4,2]); //sub调用add的方法
var a2 = sub.apply(add,[4,2]); //add调用sub的方法
var a3 = add.call(sub,4,2); //sub调用add的方法
console.log(a1); //6
console.log(a2); //2
console.log(a3); //6
```
```
//改变this指向
var obj = {
name:"Bob"
}
var name = "Tom";
function test(){
console.log(this.name);
console.log(this);
}
test(); // Tom Window
test.call(obj); //Bob {name:"Bob"}
//借用别的对象的方法
var Person1 = function(){
this.name = "Bob";
}
var Person2 = function(){
this.getName = function(){
console.log(this.name);
}
Person1.call(this);//this指向Person2,结果相当于给Person2加了name属性
}
var person = new Person2();
person.getName(); //Bob
//单纯的函数调用:
function add(){
alert(1);
}
add.call();
```
apply、call和bind的区别:
相似之处:
①都是用来改变函数的this对象的指向的;
②第一个参数都是this要指向的对象;
③都可以利用后续参数传参;
区别:
①apply、call、bind的第一个参数都是this要指向的对象,但apply只有两个参数,第二个参数为一个数组,
需要传输的参数值须全部放到数组中。而call、bind一样,参数用逗号分开。
②apply、call返回的的是一个值,而bind返回的是一个函数,需要执行这个函数才会得到值。
39、立即执行函数和使用场景
立即执行函数:( function( ){ })( ) 返回值可以为基本数据类型,也能返会任何类型的值。
写法原因:因为在 javascript 里,括号内部不能包含语句,当解析器对代码进行解释的时候,先碰到了(),
然后碰到function关键字就会自动将()里面的代码识别为函数表达式而不是函数声明。
作用:立即执行函数会形成一个单独的作用域,我们可以封装一些临时变量或者局部变量,避免污染全局变量。
使用场景:
①代码在页面加载完成之后,不得不执行一些设置工作,比如时间处理器,创建对象等等。
②所有的这些工作只需要执行一次,比如只需要显示一个时间。
③需要一些临时的变量,但是初始化过程结束之后,就再也不会被用到,我们可以用立即执行函数——去将我们所有的代码包裹在它的局部作用域中,
不会让任何变量泄露成全局变量。
40、iframe的优缺点有哪些?
优点:
①iframe能够原封不动的把嵌入的网页展现出来;
②如果有多个网页引用iframe,那么你只需要修改iframe的内容,就可以实现调用的每一个页面内容的更改,方便快捷。
③网页如果为了统一风格,头部和版本都是一样的,就可以写成一个页面,用iframe来嵌套,可以增加代码的可重用。
④如果遇到加载缓慢的第三方内容如图标和广告,这些问题可以由iframe来解决。
缺点:
①会产生很多页面不易管理;
②iframe框架结构有时会让人感到迷惑,如果框架个数多的话,可能会出现上下、左右滚动条,会分散访问者的注意力,用户体验度差。
③代码复杂,无法被一些搜索引擎索引到,这一点很关键,现在的搜索引擎爬虫还不能很好的处理iframe中的内容,所以使用iframe会不利于搜索引擎优化。
④很多的移动设备(PDA 手机)无法完全显示框架,设备兼容性差。
⑤iframe框架页面会增加服务器的http请求,对于大型网站是不可取的。
本题延申:
frame框架:
优点:
①重载页面时不需要重载整个页面,只需要重载页面中的一个框架页(减少了数据的传输,加快了网页下载速度);
②技术易于掌握,使用方便,使用者众多,可主要应用于不需搜索引擎来搜索的页面;
③方便制作导航栏 ;
缺点:
①搜索引擎程序不能解读这种页面;
②不能打印全框架;
③浏览器的后退按钮无效;
④手机等终端设备无法显示全部框架内容;
iframe和frame区别:
①frame不能脱离frameSet单独使用,iframe可以;
②frame不能放在body中,否则不能正常显示,frame不能和body同时使用,iframe可以;
③嵌套在frameSet中的iframe必需放在body中,不嵌套在frameSet中的iframe可以随意使用;
④frame的高度只能通过frameSet控制;iframe可以自己控制,不能通过frameSet控制;
⑤iframe 可以放到表格里面。frame 则不行。
41、查找数组重复项
查找该元素首次出现的位置和最后出现的位置下标是否相同,同时判断新数组中是否不存在该元素,如果都满足则添加进新数组中去。
```
var arr = [1,2,45,44,45,2,89,1,1,2,1,2,44];
Array.prototype.unique = function(){
var arr = this;
var box = [];
for(var str of arr){
if(arr.indexOf(str) != arr.lastIndexOf(str) && box.indexOf(str) == -1){
box.push(str);
}
}
return box;
}
console.log(arr.unique());
```
42、数组扁平化
数组扁平化:将多维数组转为一维数组。
var arr = [1,2,[3,4,[5,6,[7,8,[9,10]]]]];
第一种:
```
function flatten(arr){
var box = [];
arr.map(v => {
if(Array.isArray(v)){
box = box.concat(flatten(v))
}else{
box.push(v);
}
})
return box;
}
console.log(flatten(arr));
```
第二种(不推荐):
```
function flatten(arr){
return arr.toString().split(",").map(v => {
return Number(v);
})
}
console.log(flatten(arr));
```
第三种:
```
function flatten(arr){
console.log(arr.join(","))
return arr.join(",").split(",").map(v => {
return parseInt(v);
})
}
console.log(flatten(arr));
```
第四种:
```
var arr = [1,2,[3,4,[5,6,[7,8,[9,10]]]]];
function flatten(arr){
return arr.reduce((result,item) => {
console.log(result,item)
return result.concat(Array.isArray(item) ? flatten(item) : item);
},[]);
}
console.log(flatten(arr));
```
第五种:
```
console.log([].concat(...arr));
function flatten(arr){
while(arr.some(item => Array.isArray(item))){
arr = [].concat(...arr);
}
return arr;
}
console.log(flatten(arr));
```
43、BOM属性对象方法
BOM:浏览器对象模型;即 JavaScript可以进行操作的浏览器的各个功能部件的接口。
⑴window对象:
①window的方法:confirm(确认框),open(url)打开新的窗口,close()关闭窗口;
②window的属性:closed,opener。
我们有的时候需要代开我们的子窗体,域名下面还有一个新的域名,也就是子域名,子网页
var myWindow = window.open("xxx.html"); 我们打开一个子网页,会返回给我们一个值的,
这个值代表另一个页面的window属性可以通过myWindow.closed来查看另一个页面的是否关闭。
⑵Navigator对象(导航器对象):
appCodeName:返回浏览器的代码名;
appName:返回浏览器名字;
appVersion:返回浏览器的平台和版本信息;
cookieEnabled:返回指明浏览器中是否启用cookie的布尔值;
platform:返回运行浏览器的操作系统平台;
userAgent:返回客户机发送服务器的user-agent头部的值;
⑶screen(显示器对象):
avaiHeight:返回显示屏幕的可用高度;
availWidth:返回显示屏幕的可用宽度;
height:返回屏幕的像素高度;
width:返回屏幕的像素宽度;
colorDepth:返回屏幕颜色的位数;
⑷history(历史对象):
back():返回前一个URL;
forward():返回下一个URL;
go():返回某个具体页面;
⑸localtion(位置对象):
hash:返回或设置从井号(#)开始的URL;
host:设置或返回主机名和当前URL的端口号;
href:设置或者返回完整的URL;
hostname:设置或返回当前URL主机名;
search:设置或者返回从问号(?)开始的URL;
port:设置或返回当前URL的端口号;
44、服务端渲染
定义:将组件或页面通过服务器生成html字符串,在发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。
解释:服务端渲染的模式下,当用户第一次请求页面时,由服务器把需要的组件或页面渲染成 HTML 字符串,然后把它返回给客户端。
客户端拿到手的,是可以直接渲染然后呈现给用户的 HTML 内容,不需要为了生成 DOM 内容自己再去跑一遍 JS 代码。
使用服务端渲染的网站,可以说是“所见即所得”,页面上呈现的内容,我们在 html 源文件里也能找到。
有了服务端渲染,当请求用户页面时,返回的body里已经有了首屏的html结构,之后结合css显示出来。
优点:
①首屏渲染快(关键性问题):相比于加载单页应用,我只需要加载当前页面的内容,而不需要像 React 或者 Vue 一样加载全部的 js 文件;
②SEO(搜索引擎)优化:不同爬虫工作原理类似,只会爬取源码,不会执行网站的任何脚本(Google除外,据说Googlebot可以运行javaScript)。
使用了React或者其它MVVM框架之后,页面大多数DOM元素都是在客户端根据js动态生成,可供爬虫抓取分析的内容大大减少。另外,
浏览器爬虫不会等待我们的数据完成之后再去抓取我们的页面数据。服务端渲染返回给客户端的是已经获取了异步数据并执行JavaScript脚本的最终HTML,
网络爬中就可以抓取到完整页面的信息。
③可以生成缓存片段、节能;
缺点:用户体验较差,不容易维护、通常前端改了部分html或者css,后端也需要改;
使用场景:vue全家桶或者react全家桶,都是推荐通过服务端渲染来实现路由的。
45、垃圾回收机制
什么是垃圾:一般来说没有被引用的对象就是垃圾,就是要被清除, 有个例外如果几个对象引用形成一个环,互相引用,
但根访问不到它们,这几个对象也是垃圾,也要被清除。
方法:
①JS具有垃圾自动回收的机制:周期性执行,找出那些不在继续使用的变量,然后释放其内存。
②标记清除(常见):当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。
标记“离开环境”的就回收内存。垃圾回收器完成内存清除工作,销毁那些带标记的值并回收他们所占用的内存空间。
③引用计数:
原理:跟踪记录每个值被引用的次数。
工作流程:当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。
如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,
则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的
内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。
46、 eventloop(事件循环):进程和线程,任务队列;
浏览器内核是多线程,JavaScript是单线程;
JS单线程详解:因为 js 是面向客户端的一门语言,主要是用户交互,操作dom,渲染数据。试想一下,
如果是多线程,我们在一个线程删除了一个dom节点,另外一个线程添加了一个dom节点,以那个线程为主呢,
就会出现混乱的情况。当然你可以说我们在操作一个dom之后加上锁,只允许一个线程操作,这样其实增加了程序的复杂度,
并不是一个好办法。
单线程产生的问题:必须要等待前一个程序执行完毕才执行下一个,所以将程序分为了两类:同步任务和异步任务。
异步任务又可以分为宏任务和微任务。
栈:先进后出的数据结构,存储基本数据类型的变量。
堆:主要负责引用数据类型的存储。
任务队列:为什么会有任务队列呢,还是因为 javascript 单线程的原因,单线程,就意味着一个任务一个任务的执行,
执行完当前任务,执行下一个任务,这样也会遇到一个问题,就比如说,要向服务端通信,加载大量数据,如果是同步执行,
js 主线程就得等着这个通信完成,然后才能渲染数据,为了高效率的利用cpu, 就有了同步任务和异步任务之分。
同步任务: 指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
异步任务: 指的是不进入主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
宏任务macrotask: 可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到
执行栈中执行)。
常见的宏任务:script, setTimeout, setInterval, setImmediate, I/O, UI rendering。
微任务microtask(异步): 可以理解是在当前task执行结束后立即执行的任务。
常见的微任务:process.nextTick(Nodejs),Promise.then(), MutationObserver。
47、如何快速让字符串变成已千为精度的数字
var str = "10000000000";
第一种:把数字转换成字符串后,打散为数组,再从末尾开始,逐个把数组中的元素插入到新数组(result)的开头。 每插入一个元素,counter就计一次数(加1),
当counter为3的倍数时,就插入一个逗号,但是要注意开头(i为0时)不需要逗号。最后通过调用新数组的join方法得出结果。
```
String.prototype.toThousands = function(){
var num = this;
var result = [ ], counter = 0;
num = (num || 0).toString().split('');
for (var i = num.length - 1; i >= 0; i--) {
counter++;
result.unshift(num[i]);
if (!(counter % 3) && i != 0) { result.unshift(','); }
}
return result.join('');
}
console.log(str.toThousands());
```
第二种:通过正则表达式循环匹配末尾的三个数字,每匹配一次,就把逗号和匹配到的内容插入到结果字符串的开头,
然后把匹配目标(num)赋值为还没匹配的内(RegExp.leftContext)。如果数字的位数是3的倍数时,最后一次匹配到的内容肯定是三个数字,
但是最前面的三个数字前不需要加逗号;如果数字的位数不是3的倍数,那num变量最后肯定会剩下1到2个数字,循环过后,要把剩余的数字插入到结果字符串的开头。
```
function toThousands(num) {
var num = (num || 0).toString(), re = /\d{3}$/, result = '';
while ( re.test(num) ) {
result = RegExp.lastMatch + result;
if (num !== RegExp.lastMatch) {
result = ',' + result;
num = RegExp.leftContext;
} else {
num = '';
break;
}
}
if (num) { result = num + result; }
return result;
}
console.log(toThousands(str));
```
第三种:第二种的改良版
```
function toThousands(num) {
var num = (num || 0).toString(), result = '';
while (num.length > 3) {
result = ',' + num.slice(-3) + result;
num = num.slice(0, num.length - 3);
}
if (num) { result = num + result; }
return result;
}
console.log(toThousands(str));
```
第四种:懒人版
```
function toThousands(num) {
return (num || 0).toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,');
}
console.log(toThousands(str));
```
48、Promise的解释:
Promise是异步b编程解决方案之一。最大的好处是提供了一个then,来为异步提供回调函数。其先进之处是可以在then方法中继续写Promise对象并f返回,
然后继续用then来进行回调操作,并且能够在外层捕获异步函数的异常信息。
⑴Promise用法:
```
const fn = new Promise(function(resolve,reject){
axios.get(url).then(res => {
resolve(res);
}).catch(err => {
reject(err);
})
}).then((res) => {
console.log(res);
}).catch((err) => {
console.log(err);
})
```
⑵Promise原理:
在Promise的内部,有一个状态管理器的存在,有三种状态:pending、fulfilled、rejected。
①promised对象初始化为pending;
②当调用resolve(成功),会由pending => fulfilled。
③当调用reject(失败),会由pending => rejected。
看上面的的代码中的resolve(num)其实是将promise的状态由pending改为fulfilled,然后向then的成功回掉函数传值,
reject反之。但是需要记住的是注意promsie状态 只能由 pending => fulfilled/rejected, 一旦修改就不能再变。
promise.then方法每次调用,都返回一个新的promise对象 所以可以链式写法(无论resolve还是reject都是这样)
⑶Promise的三个状态:
pending:异步任务正在进行中;
resolved(也可以叫fulfilled),异步任务执行成功;
rejected,异步任务执行失败。
⑷Promise对象初始化:
① new Promise(fn);
②Promise.resolve(fn);
⑸Promise特点:
①对象的状态不受外界影响;
②一旦状态改变,就不会再变,任何时候都可以得到这个结果;
⑹Promise方法:
①Promise.all( [promise1,promise2,promise3] ).then( );作为参数的几个promise对象一旦有一个的状态为rejected,
则all的返回值就是rejected。当这几个作为参数的函数的返回状态为fulfilled时,至于输出的时间就要看谁跑的慢了。
```
var p1 = Promise.resolve(1),
p2 = Promise.reject(2),
p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then((res)=>{
//then方法不会被执行
console.log(results);
}).catch((err)=>{
//catch方法将会被执行,输出结果为:2
console.log(err);
});
```
②promise.race( ):从字面意思上理解就是竞速,那么理解起来上就简单多了,也就是说在数组中的元素实例那个率先改变状态,
就向下传递谁的状态和异步结果。但是,其余的还是会继续进行的。
```
let p1 = new Promise((resolve)=>{
setTimeout(()=>{
console.log('1s') //1s后输出
resolve(1)
},1000)
})
let p10 = new Promise((resolve)=>{
setTimeout(()=>{
console.log('10s') //10s后输出
resolve(10) //不传递
},10000)
})
let p5 = new Promise((resolve)=>{
setTimeout(()=>{
console.log('5s') //5s后输出
resolve(5) //不传递
},5000)
})
Promise.race([p1, p10, p5]).then((res)=>{
console.log(res); // 最后输出
})
//结果:
1s
1
5s
10s
```
49、模块化
模块定义:将一个复杂的程序依据一定的规则封装成几个块(文件),并进行组合在一起,块的内部数据是私有的,
只是向外部暴露一些接口(方法)与外部其它模块通信。
模块的组成:数据(内部属性)、操作数据的行为(内部的函数);
模块化:编码时是按照模块一个一个编码的, 整个项目就是一个模块化的项目。能够帮助开发者拆分和组织代码,解决JS全局作用域的污染问题。
第一种:CommonJS(同步加载)主要用在Node开发上,每个文件j就是一个模块,每个文件都有自己的一个作用域。
通过module.exports暴露public成员,通过let xm = require(模块路径)来引用。
第二种:AMD:AMD规范的被依赖模块是异步加载的,而定义的模块是被当作回调函数来执行的,依赖于require.js模块管理工具库。
当然,AMD规范不是采用匿名函数自调用的方式来封装,我们依然可以利用闭包的原理来实现模块的私有成员和公有成员。
```
// 定义AMD规范的模块
define([function() {
return 模块
})
//私有成员
define(['module1', 'module2'], function(m1, m2) {
let x = 1;
function add() {
x += 1;
return x;
}
return { add };
})
```
第三种:CMD:CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。AMD 推崇依赖前置,CMD 推崇依赖就近。
CMD集成了CommonJS和AMD的的特点,支持同步和异步加载模块。CMD加载完某个依赖模块后并不执行,只是下载而已,
在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,
这样模块的执行顺序和书写顺序是完全一致的。因此,在CMD中require函数同步加载模块时没有HTTP请求过程。
```
define(function(require, exports, module) {
// 同步加载模块
var a = require('./a');
a.doSomething();
// 异步加载一个模块,在加载完成时,执行回调
require.async(['./b'], function(b) {
b.doSomething();
});
// 对外暴露成员
exports.doSomething = function() {};
});
// 使用模块
seajs.use('path');
```
第四种:module:ES6的模块化已经不是规范了,而是JS语言的特性。随着ES6的推出,AMD和CMD也随之成为了历史。
ES6模块与模块化规范相比,有两大特点:
模块化规范输出的是一个值的拷贝,ES6 模块输出的是值的引用。模块化规范是运行时加载,ES6 模块是编译时输出接口。
模块化规范输出的是一个对象,该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,ES6 module 是一个多对象输出,
多对象加载的模型。从原理上来说,模块化规范是匿名函数自调用的封装,而ES6 module则是用匿名函数自调用去调用输出的成员。
```
//react种运用的就是ES6的模块化
var module1 = value1;
var module2 = value2;
export {module1,module2} //导出模块
import {module1,module2} from "模块路径/模块名" //引入模块
```
50、new的原理
new实际上是在堆内存中开辟一个空间。
①创建一个空对象,构造函数中的this指向这个空对象;
②这个新对象被执行[ [ 原型 ] ]连接;
③执行构造函数方法,属性和方法被添加到this引用的对象中;
④如果构造函数中没有返回其它对象,那么返回this,即创建的这个的新对象,否则,返回构造函数中返回的对象。
```
function _new(){
let target = {}; //创建的新对象
let [constructor,...args] = [...arguments];
//执行[[原型]]连接,target是constructor的实例
target.__proto__ = constructor.prototype;
//执行构造函数,将属性或方法添加到创建的空对象上
let result = constructor.prototype;
if(result && (typeof (result) == "object" || typeof (result) == "function")){
//如果构造函数执行的结构返回的是一个对象,那么返回这个对象
return result;
}
//如果构造函数返回的不是一个对象,返回创建的对象
return target;
}
```
自己理解的new:
new实际上是在堆内存中开辟一个新的空间。首先创建一个空对象obj,然后呢,
把这个空对象的原型(__proto__)和构造函数的原型对象(constructor.prototype)连接(说白了就是等于);
然后执行函数中的代码,就是为这个新对象添加属性和方法。最后进行判断其返回值,如果构造函数返回的是一个对象,
那就返回这个对象,如果不是,那就返回我们创建的对象。
51、数组和类数组类数组:
①拥有length属性,其它属性(索引)为非负整数;
②不具有数组所具有的方法;
③类数组是一个普通对象,而真实的数组是Array类型。
常见的类数组:arguments,document.querySelectorAll得到的列表,jQuery对象($("div"));
52、call、apply、bind封装与区别
区别:实际上call与apply的功能是相同的,只是两者的传参方式不一样,而bind传参方式与call相同,但它不会立即执行,而是返回这个改变了this指向的函数。
封装:
call函数
```
// 思路:将要改变this指向的方法挂到目标this上执行并返回
Function.prototype.mycall = function (context) {
if (typeof this !== 'function') {
throw new TypeError('not funciton')
}
context = context || window
context.fn = this
let arg = [...arguments].slice(1)
let result = context.fn(...arg)
delete context.fn
return result
}
```
apply函数
```
// 思路:将要改变this指向的方法挂到目标this上执行并返回
Function.prototype.myapply = function (context) {
if (typeof this !== 'function') {
throw new TypeError('not funciton')
}
context = context || window
context.fn = this
let result
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
```
bind函数
```
// 思路:类似call,但返回的是函数
Function.prototype.mybind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
let _this = this
let arg = [...arguments].slice(1)
return function F() {
// 处理函数使用new的情况
if (this instanceof F) {
return new _this(...arg, ...arguments)
} else {
return _this.apply(context, arg.concat(...arguments))
}
}
}
```
53、如何让(a == 1 && a == 2 && a == 3)的值为true?
" == "操作符在左右数据不一致的时候,会先进行隐式转换,该值意味着不是基本数据类型,
因为如果a是null或者undefined、bool类型都不可能返回true;可以推测a是复杂数据类型。
方法一:数组的 toString 接口默认调用数组的 join 方法,重新 join 方法
```
let a = [1,2,3];
a.join = a.shift;
console.log(a == 1 && a == 2 && a == 3) //true
```
方法二:利用数据劫持(Proxy/Object.definedProperty)
```
let i = 1;
let a = new Proxy({},{
i:1,
get:function(){
return () => this.i++
}
});
console.log(a == 1 && a == 2 && a == 3);
```
54、var、let和const的区别
①变量提升:var存在变量提升,可以在声明之前使用,而let和const在声明之前使用会报错。
②重复声明:同一作用域下let,const声明的变量不允许重复声明,而var可以。
③暂时性死区:let,const声明的变量不能在声明之前使用,而var可以
```
var tmp = 123;
if(true){
tmp = "abc";
let tmp; //报错
}
```
④初始值:const声明的是一个常量,不可以改变,一旦声明就必须给赋值。
const所说的一旦声明值就不能改变,实际上指的是:变量指向的那个内存地址所保存的数据不得改动。
⑤块级作用域:let,const 是块作用域,即其在整个大括号 {} 之内可见,var:只有全局作用域和函数作用域概念,没有块级作用域的概念。
55、简述js继承的方式
1.原型链继承:继承父类构造函数里边的属性和方法,也继承父类原型上的属性和方法 缺点--不能向父类传参数
2.借用构造函数继承:可以父类传递参数 缺点--继承不了父类原型对象的方法
3.组合继承:借用构造函数继承+原型链继承
4.混入式继承:把父类的所有方法都拷贝到子类上
5.原型式继承:只继承父类原型上的属性和方法
6.寄生式继承:混入式继承+原型式继承
56、javascript 的垃圾回收机制
定义:指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。
JavaScript 在创建对象(对象、字符串等)时会为它们分配内存,不再使用对时会“自动”释放内存,这个过程称为垃圾收集。
内存生命周期中的每一个阶段:
分配内存 — 内存是由操作系统分配的,它允许您的程序使用它。在低级语言(例如 C 语言)中,这是一个开发人员需要自己处理的显式执行的操作。
然而,在高级语言中,系统会自动为你分配内在。 使用内存 — 这是程序实际使用之前分配的内存,在代码中使用分配的变量时,就会发生读和写操作。
释放内存 — 释放所有不再使用的内存,使之成为自由内存,并可以被重利用。与分配内存操作一样,这一操作在低级语言中也是需要显式地执行。
四种常见的内存泄漏:全局变量,未清除的定时器,闭包,以及 dom 的引用
1.全局变量 不用 var 声明的变量,相当于挂载到 window 对象上。如:b=1; 解决:使用严格模式
2.被遗忘的定时器和回调函数
3.闭包
4.没有清理的 DOM 元素引用