前端面试题-Javascript

257 阅读29分钟

一、let、var、const

首先,我们带着问题去学习:

  1. let、var、const的区别?
  2. 什么是块级作用域?
  3. 如何使用?

简述: 在js中,一共有3种变量声明的方式:

  • var
  • let
  • const 最初就只有var,为了解决作用域的问题,所以新增了let和const。

1. 作用域

作用域简单来说就是声明的变量可使用的范围,ES5种分为全局作用域函数作用域,ES6新增了块级作用域,块级作用域由一对花括号包裹,{},if语句,for语句,while语句里的{}也都属于块级作用域。

扩展:作用域与作用域链

2. var

  • 没有块级作用域的概念
// Global Scope
{
    // Block Scope
    var a=666;
    console.log(a);// 输出666 
}
console.log(a);// 输出666   
  • 有全局作用域和函数作用域的概念
// Global Scope
var a=999;
function fn(){
    // Local Scope
    var b=1;
    console.log(a);//999
    console.log(b);//1
}
fn();
console.log(a);//999
console.log(b);//ReferenceError,b os not defined
  • 不会初始化,默认值为undefined
var a;
console.log(a);// undefined
  • 变量的声明提升
console.log(a);//undefined
var a=999;

fn();
function fn(){
    // Local Scope
    console.log(a);// 就近原则,输出undefined
    var a;
}

// 这里存在一个变量的声明提升,而赋值不提升的过程,等价于
var a;
console.log(a);
a=999;

js会经历编译期和执行期,在编译期变量的声明和函数的声明都会提升,然后默认值一个为undefined,函数的为函数体。

  • 全局作用域用var声明的变量会挂载到window对象下(这是在浏览器环境下运行,在node环境下全局对象是global)
// Global Scope
var a = 888;
console.log(window.a);// 888
  • 同一作用域中可以重复声明
var a=666;
var a=888;
console.log(a);//888
fn()
function fn(){
    var b=888;
    var b=999;
    console.log(b);//999
}

3. let

  • 有块级作用域的概念
{
    let a=10;
    console.log(a);//10
}
console.log(a);//ReferenceError:a is not defined
  • 不存在变量提升
{
    console.log(a);//ReferenceError:cannot access 'a' before initialization
    let a=666;
}
// 报错,无法在初始化之前访问,说明不存在变量提升
  • 暂时性死区-temporal dead zone
 console.log(a);//ReferenceError:cannot access 'a' before initialization
    let a=666;
    
    if(true){
        // tdz开始
        console.log(a);//报错:初始化前不能使用
        let a;//tdz结束
        console.log(undefined);
        
        a=123;
        console.log(a);//123
    }

在代码块内,使用let/const命令声明变量之前,该变量都是不可以用的,在语法上称之为‘暂时性死区’。

4. const

  • 必须在声明的同时立即初始化
    const a=1;// 正确
    
    const b;// SyntaxError:missing initializer in const declaration
  • 变量的值不能修改,也就是声明了一个常量
    const a=1;
    a=2;// TypeError:Assignment to constant variable

二、值和引用

问题:

  1. js的基本数据类型有哪些?
  2. js的引用类型有哪些?
  3. 基本类型和引用类型的区别? 简述: js中的数据类型分为两类:
  • 基本类型-原始值、简单值

string、number、bool、null、undefined、symbol

  • 引用类型

object(数组、对象、函数、Date等)

1. 简单值

  • 简单值是js中可用的数据或信息的最底层形式或最简单形式。之所以称之为简单值是因为他们是不可细化的。
  • 简单值数据大小是固定的,所以简单值的数据是存储于内存中的栈区里面的。(存取方式:像口袋、羽毛球罐,先进后出)
var a=null;
var b=undefined;
var c=1;
var d='d';
var e=true;
var f=Symbol();
console.log(typeof a);//'object'
console.log(typeof b);//'undefined'
console.log(typeof c);//'number'
console.log(typeof d);//'string'
console.log(typeof e);//'boolean'
console.log(typeof f);//'symbol'
console.log(typeof {});//'object'
console.log(typeof []);//'object'

特殊情况:

  1. null的类型为object是历史遗留问题,因为null是对象的占位。所有对象在底层都表现为二进制,在js中二进制前三位都为0的话,会被判断为object类型,null的二进制全部为0,所以typeof null,就是object。

  2. null==undefined返回true,这两个类型都表示‘无’的意思,但是undefined表示的是某个变量不存在或者没有赋值(js会主动将声明时没有初始化的变量设为undefined)。null值表示空,不会被js自动赋值,需要开发人员手动赋值,var a=null;

  3. 先有的null,后有的undefined,null的提出是为了表示‘无’,但是因为null转为数值时为0,所以提出了undefined来满足js的错误处理机制而。

  4. null是一个表示‘空’的对象(空对象指针),Numer(null)返回0,null的典型用法:

  • 作为函数的参数,表示函数的参数不是对象
  • 作为对象原型链的终点
  1. undefined表示的是‘无’的原始值,Numer(undefined)返回undefined,undefined的典型用法
  • 变量被声明了,却没有赋值,就为undefined
  • 调用函数时,应该提供的函数参数没有被提供,该参数就为undefined
  • 对象没有赋值的属性,该属性为undefined
  • 函数没有返回值时,默认返回undefined

2. 复杂值-引用值

由多个简单值或复杂之组成。也可以说是:可以先下拆分成多个简单值或复杂值的数据。 复杂值在内存中的大小是未知的,因为都杂志可以包含任何值,所以复杂值的数据都存储在堆区里面。而栈区里存的是变量名-堆区的值对应的地址。

var obj = {
    name:'a',
    age:12,
    eat:function(){
    }
}

3. 访问方式

  • 简单值-对值进行存储和使用
var a = 12;
var b = a;
var a=23;
console.log(a,b);//23,12
  • 复杂值-对引用地址的存储或使用
var obj = {};
var obj1 = obj;
obj.name = 'ming';
console.log(obj.name,obj1.name);//'ming','ming'
变量名
a12->23
b12
变量引用地址
objox0001
obj1ox0001

ox0001:{}->{name:'ming'}

4. 比较方式

  • 简单值-比较的是值
  • 复杂值-比较的是地址

只有先引用地址是同一个的时候,这个对象才相等。(new一个新对象即,包含相同属性和值,若引用地址不同,也还是不相等的)

var a=1;
var b=1;
console.log(a===b);//true

var obj = {name:'hong'};
var obj1 = {name:'hong'};
console.log(obj===obj1);//false

5. 动态属性

简单值无法添加动态属性,复杂值可以添加动态属性。

var obj = {};
obj.name = 'lan';
console.log(obj.name);//'lan'

var str = 'huang';
str.age = 12;
console.log(str.age);//undefined

6. 变量赋值

简单值赋值的是值,复杂值赋值的是地址。

var a=3;
var b=a;
b=5;
console.log(a,b);// 3,5

var obj = {val:1};
var obj1 = a;
obj1.val = 10;
console.log(obj.val,obj1.val);//10,10

三、包装类型

问题:

  1. 是否了解包装类型

包装类型

基本类型和引用类型最大的区别:引用类型有自己的内置方法。 但是部分基本类型也有:Boolean、Number、String

var s1='hello';
var s2=new String('hello');
console.log(s1==s2);//为true,
console.log(s1===s2);//为false。

基本类型调用方法的底层原理

var str = 'hello';// 当使用属性方法时,内部调用new String('hello'),生成一个临时的包装对象
var str1 = new String('hello');

var n= 12.1234;
var n1 = new Number(12.1234);// 当使用属性方法时,内部调用new Number(12.1234),生成一个临时的包装对象

var bool = true;
var bool1 = new Boolean(true);// 当使用属性方法时,内部调用new Boolean(true),生成一个临时的包装对象

console.log(n.toFixed(2));// n=12.12
console.log(n1.toFixed(1));//n1=12.1
console.log((1).toFixed(2),2..toFixed(2));//1.00,2.00
 

以上代码可以发现,普通数据类型也可以使用属性方法,因为在使用属性和方法时,js内部会自动进行一个转换,生成一个包装对象,步骤:

  1. 自动创建String类型的一个实例,和基本类型的值不同,这个实例就是一个基本包装类型的对象。
  2. 调用实例对象上指定的方法
  3. 销毁这个实例

为什么基本类型有内置方法但不能动态添加属性

var str = 'hhh';
var str1 = str.charAt(0);
{
    var _str = new String('hhh');//找到对应的包装对象类型,然后创建出一个和基本类型值相同的对象。
    var str1 = str.charAt(0);// 调用charAt方法
    _str = null// 销毁临时创建的对象
}
  1. 基本类型的值没有方法可以调用,但是后台临时创建的包装对象上有内置方法可以让我们调用,这样就可以对字符串、数字、布尔值这三种基本类型的数据进行更多操作。

  2. 那么,什么时候后台会自动为基本类型创建一个对应的包装类型的对象? 这取决于当前执行的代码是否是为了获取它的值(读取模式)。

  3. 基本包装类型的对象和引用类型的对象最大的区别:对象的生存期不同,所以包装类型的对象无法自定义自己的方法。

var test = 'hhh';
test.say = 'hello';// 给自动创建的String实例对象添加了say属性,但是这行执行完后,实例对象被销毁,say属性也就不存在了。
console.log(undefined);// 读取模式,又创建了新的String实例,而这个新的String没有say方法,所以返回undefined

var t = new String('hello');
t.say = 'world';
console.log(t.say);//'world'

如何给基本类型添加属性呢

答案是:给基本包装对象的原型上添加,每个对象都有原型。

String.prototype.last = function(){
    return this.charAt(this.length);
};
var str = '123';
console.log(n.last());//'3'--执行到这一句时,仍然会自动创建String类型的实例
{
    var _str = new String('123');
    _str.last();
    _str=null;
}

总结

  • 是否了解js中的包装类型?

包装对象,就是当基本类型以对象的方式去使用时,js会转换成对应的包装类型,相当于new一个对象,内容和基本类型的内容一样,然后当操作完成再去访问的时候,这个临时对象会被销毁,然后再访问的时候就是undefined。

number、string、boolean都有包装类型。

因为有了包装类型,所以js中的基本类型才可以被当作对象来访问。

基本类型特征;

  1. 每个转包类型都映射到同名的基本类型
  2. 在读取模式下访问基本类型时,就会创建对应的基本包装类型的一个对象,从而可以调用一些方法或属性。
  3. 操作基本类型值的语句一执行完,就会立刻销毁新创建的临时的包装对象。

四、数据类型转换

js是一种动态类型的语言,变量没有类型限制,可以随时赋给任意类型的数据。

'4'-'3'//1

强制转换-显示类型转换

强制类型的转换,指的是通过Number()、String()和Boolean()三个函数,手动的将任意类型的数据转为数字、字符串或布尔值。

  1. Number()
// 数字-不变
Number(234);//234
// 字符串
Numer('12,12')//12.12
Numer('')//0
Numer('hhhh')//NaN
Numer('12hhh')//NaN
parseInt('12hhh')//12
parseInt('hhh12')//NaN
// 布尔值
Numer(true)//1
Numer(false)//0
// null
Number(null)//0
parseInt(null)// NaN
// undefined
Number(undefined)// NaN
parseInt(undefined)// NaN


// 复杂类型的转换步骤
// 第一步:调用对象自身的valueOf方法,如果返回原始类型的值,则直接使用强制转换方法
// 第二步,若第一步返回的还是对象,则改为调用自身的toString方法,如果toString返回的是原始类型的值,那么进行强制类型类型转换
// 第三步,如果还是返回对象,则报错
var obj = {a:1};
Number(obj)//NaN
if(typeof obj.valueOf() === 'object'){
    Number(obj.toString());// '[object,object]'
}else{
    Number(obj.valueOf());
}
Number([1,2,3])//NaN
Number([1])//1
Number([1,2])//NaN

  • parseInt和Number都会自动过滤首位的空格
  • valueOf和toString方法可以自己定义
  1. String()
// 原始数据类型
String('abc')//'abc'
String(123)//'123'
String(true)//'rrue'/'false'
String(undefined)//'undefined'
String(null)//'null'
String(Symbol());//'Symbol()'
// 对象
String({a:1});//"[object Object]"
String([1,2,3]);//"1,2,3"
// 复杂类型的转换步骤
// 先调用toString方法,再调用valueOf方法,如果还不是字符串类型的,就报错。
var obj = {
    a:2,
    toString:fucntion(){
        return 'this is a test';// {}
    }
}
console.log(String(obj));//'this is a test'
  1. Boolean()
Boolean('');//false
Boolean('123');//true
Boolean(0);//false
Boolean(1);//true
Boolean(null);//false
Boolean(undefined);//false
Boolean(NaN);//false

Boolean({});//true
Boolean([]);//true
Boolean(new Boolean(false));//true

除了null、undefined、''、0(-0,+0)、NaN

自动转换-隐式类型转换

  1. 不同类型的数据相加

123+'abc' //'123abc'

  1. 非布尔类型的值求布尔值

if('abc'){console.log(1)}// 1

  1. 对数据使用一元运算符,就是正负

+'123'// 123

+'abc'//NaN

+{a:1}//NaN

  1. 自动转换为布尔值:Boolean()
  • 写法一:expression? true:false
  • 写法二:!!expression
  1. 自动转换为字符串:String()
'5'+1          //'51'
'5'+true       //'5true'
'5'+{}          //'5[object Object]'
'5'+[]          //'5'
'5'+function(){}//'5function(){}'
'5'+undefined//'5undefined'
'5'+null //'5null'
  1. 自动转换为数字
'5'-'2' //3
'5'*'2' //10
true-1 //0
false-1//-1

false/5 //0
'abc'-1 //NaN
null+1//1
undefined +1//NaN

+'1'//1
+'abc'//NaN
-true//-1

五、运算符

(1). 常见的算数运算符

  1. +-*/ 加减乘除
// 特殊情况
// 非数值类型的先转为数值
true + 1 //2
// 有一个字符串就拼接
'a' + true //atrue
// 对象:valueOf()-toString()
[]+1 //'1'
{}+1 //'[object Object]1'

// 错误写法
function isOdd(n){
    return n%2===1;
}
isOdd(-5)// false,-5%2=-1
isOdd(-4)// false,-4%2=-0

// 正确
function isOdd(n){
    return Math.abs(n)%2==1;
}
  1. x**y 指数运算 x%y 余数运算 x++(++x)x--(--x) 自增自减

  2. +x,-x 正数负数值运算

六、原型和原型链

问题:

  1. 对js中原型和原型链的理解?
  2. 对一个函数实例化后,它的原型指向什么?

js是一门基于原型的语言,对象的产生是通过原型对象而来的。

ES5提供了Object.create()方法,用来克隆对象。

 var person = {
     arms: 2,
     legs: 2,
     say: function () {
         return 'hello world';
     }
 }
 var zhangsan = Object.create(person, {
     name: {
         value: '张三',
         enumerable: true
     },
     age: {
         value: 42,
         enumerable: true
     }
 });
 console.log(zhangsan);//{name: '张三', age: 42}
 console.log(zhangsan.say());//'hello world'
 console.log(zhangsan == person, zhangsan === person);//false false
 console.log(zhangsan.__proto__ === person);//true

 // 为什么newObj会是个{}?
 // 因为Object.create()方法让newObject的原型对象为obj
 // newObj.__proto__===obj

 // 第一个参数:原型对象;
 // 第二个参数:也是一个对象包含多个键值对,给newObj1自身添加的属性。
 var zhangxiaosan = Object.create(zhangsan, {
     born: {
         value: '北京',
         enumerable: true
     }
 })
 console.log(zhangxiaosan);// {born: '北京'}
 console.log(zhangxiaosan.__proto__);//{name: '张三', age: 42}

 console.log(zhangxiaosan.gender);//先在自身找,没有去原型找,没有再去原型的原型找
 // 直到找到Object的原型null,找到就返回,找不到就返回undefined

 // 张小三的原型是张三,张三的原型是person,就形成了一条原型链

 // 随着js的发展,大家还是希望能像标准的面向对象语言一样,通过类来批量的生产对象
 // 早期的js 通过构造函数来模拟其他语言里面的类
 function Computor(name, price) {
     this.name = name;
     this.price = price;
     // this.show = function () {
     //     console.log(`${this.name}电脑的价格为${this.price}`);
     // }
 }
 // 将方法挂在原型对象上
 Computor.prototype.show = function () {
     console.log(`${this.name}电脑的价格为${this.price}`);
 }
 var apple = new Computor("苹果", 15000);
 console.log(apple);
 apple.show();
 var huawei = new Computor("华为", 12000);
 console.log(huawei);
 huawei.show();

 // 虽然这种方式模拟出了其他面向对象的语言创建对象的方法
 // 但是在js底层还是基于原型来创建的对象。
 // Computor.prototype是 Computor的实例对象 的原型对象

 // 构造函数Computer 通过new关键字创建的 实例对象apple/huawei 
 // 的原型对象(__proto__) 就是 构造函数的
 // apple.__proto__的constructor指向构造函数???
 console.log(apple.__proto__ === Computor.prototype);// true
 console.log(apple.constructor === Computor);// 实例对象的constructor属性是从他的原型对象上面访问到的
 
 // 总结
 // 1. 当查找对象的某个属性的时候,先在自身找,找不到,往原型找,再找不到,就去原型的原型上,顶层是Object的原型null。
 // 2. js中的每一个对象都有原型对象,可以通过__proto__属性来访问到对象的原型对象
 // 3. 构造函数的prototype属性指向一个对象,这个对象是该构造函数实例化出来的对象的原型对象。
 // 4. 原型对象的constructor属性也指向其构造函数
 // 5. 实例对象的constructor属性是从他的原型对象上面访问到的。
 // 6. 所有构造函数的原型对象都是Object

 /*  
         ---> Object.prototype---__proto__-->null
         |
         __proto__
         |
         |    ---constructor--->
     原型对象                   构造函数()
         ^    <----prototype---      |
         |                           |
         |                           new
     __proto__                       |
         |                           |
         |------实例对象<--------------

 */

 // 验证
 var arr=[1,2];
 console.log(Array.prototype === arr.__proto__);
 console.log(arr.constructor === Array);

 // 每个对象都有自己的原型对象,二原型对象本身,也有自己的原型对象,从而形成了一条原型链。
 // 当访问一个对象的属性时,它不仅在该对象身上搜寻,还会查找该对象的原型,以及对象的原型的原型,
 // 依次向上查找,直到找到为止,找不到就返回undefined。

七、执行栈和执行期上下文

问题:

  1. 谈谈你对执行期上下文的理解?

执行上下文-execution context

简述:执行上下文又称‘执行上下文环境’,是代码执行前的准备工作。

运行js代码时,就会为这段代码创建一个执行上下文环境,该环境确定作用域,创建局部变量对象等。 js执行环境分为:全局环境、函数环境和eval环境(已不推荐)。

js执行上下文环境:全局执行上下文、函数执行上下文和eval执行上下文。

全局执行上下文

js运行时,首先进入全局环境,对应会生成全局上下文,程序代码中基本都会存在函数,那么调用函数时,就会进入函数上下文环境,对应就会生成该函数的执行上下文。 由于代码中有多个函数,就会生成多个函数上下文,在js中,通过栈的存区方式来管理执行上下文,称之为‘执行栈’,或‘函数调用栈’

栈数据结构

就像羽毛球盒子一样,栈内存的存取方式为:先进后出,后进先出。 由于js在运行时,最先进入‘全局环境’,所以,处于栈底的永远是全局环境的上下文,而处于栈顶的是当前正在执行函数的执行上下文。 函数上下文环境会在执行完后从栈顶推出。 全局上下文环境在页面关闭后才会从执行栈中被退出。

function fn(){
    function bar(){
        return 'i am bar';
    }
    return bar();
}
fn();
执行栈
bar EC
fn EC
Blobal EC

执行上下文的数量限制

执行上下文可存在多个,虽然没有明确的数量限制,但如果超出栈分配的空间,就会造成堆栈溢出,常见于递归调用,没有终止条件造成死循环的场景

// 递归调用自身
function fn(){
    fn();
}
fn();
// 报错:Uncaught RangeError:Maximum Call Stack size exceeded。

执行上下文生命周期

那么为js执行环境创建一个上下文时都做了什么呢? 首先来了解一下上下文的生命周期

  • 第一阶段创建阶段,函数被调用时,进入函数环境,为其创建一个执行上下文,此时就是创建的这个阶段。
  • 第二阶段执行阶段,执行函数中的代码时,就进入了执行阶段。

创建阶段做的事情

  1. 创建变量对象(Variable Object)
    • 变量声明,函数表达式声明(未赋值)
    • 函数声明(并赋值)
    • 函数环境会初始化创建Arguments对象(并赋值)
  2. 确定this指向(函数的this指向由调用者决定)
  3. 确定作用域(词法环境,哪里声明定义,就在那里确定变量值) 上下文环境包含三个属性:
executionContext = {
    variableObject : {}//变量对象,包含变量、函数、局部变量和Arguments
    scopeChain:{}//作用域链,包含内部上下文所有变量对象的列表
    this : {}//上下文中的this指向对象
}
// 例一
var fn = function(i){
    var a=123;
    var b = function innerB(){};
    function c(){}
}
fn(10);
// 上下文环境
fnEC = {
    VO:{
        arguments:{0:10,length:1},// 确定实参对象
        i:10// 确定形式参数
        c:function innerB(){},确定函数引用
        a:undefined,//局部变量,初始值为undefined
        b:undefined//局部变量,初始值为undefined
    },
    scopeChain:{},
    this:{}
}
    
// 例二
(
    function(){
        console.log(foo);//function
        var foo = 'hello';
        console.log(foo);//'hello'
        function foo(){
        console.log('123');
        }
        console.log(foo);//'hello'
    }
)()

总结

八、作用域和作用域链

问题:

  1. 对作用域和作用域链的理解

简述: 作用域是在运行时代码中的某些特定部分的变量、函数和对象的可访问性。 换句话说,作用域决定了代码块中变量、函数和对象的可见性。

function fn(){
    var a=1;
}
fn();
console.log(a);// ReferenceError

作用域就是一个个独立的空间,让变量不会外泄暴露出去。作用域最大的作用就是隔离变量,在不同作用域下同名变量不hi有冲突。

ES6之前js没有块级作用域,只有全局作用域和函数作用域。

ES6,新增了let和const,和块级作用域的概念。

全局作用域

  1. 特点:在代码中任何地方都能访问到的对象拥有全局作用域,例如:
    • 最外层函数和在最外层函数外面定义的变量,拥有全局作用域。
    • 所有未定义直接赋值的变量自动声明为全局作用域(a=1,函数声明)

函数作用域

  1. 特点:在函数内部声明的变量、函数和对象,只在函数内可以访问,外面访问不到。

作用域时分层的,内层可以访问外层作用域的变量 块语句{},如if、switch、for和while,和function不一样,不会创建新的作用域,将块语句中定义的变量保留在他们所在的作用域中

for(var i=0;i<2;i++){
    var a = 2;
    console.log(i,a);// 0 2   1 2
}
console.log(i,a);// 2 2

if(true){
    var b = 123;
}
console.log(b);// 123
var j=0
while(j<1){
    j++;
    console.log(i);// 2
    var c=321;
}
console.log(c);//321

块级作用域

块级作用域可通过新增的let和const声明,所声明的变量在指定块的作用域外无法被访问。 如下情况可以创建一个块级作用域:

  • 在函数内部
  • 在一个代码块内部,所有带{}的都是 块级作用域的特点:
  • 声明变量不会提升到代码块顶部
  • 禁止重复声明
  • 循环中的绑定块作用域的妙用

作用域链

自由变量:当前作用域中没有定义的变量。

在当前作用域找不到的变量,就去外层查找,依次向上,最终找到全局作用域,找到就使用,找不到就返回undefined。

这一条查找链路就成为作用域链。

关于自由变量需要着重理解一下

var x=10;
function fn(){
    console.log(x);
}
function show(f){
    var x=20;
    (function(){
        fn();// 10
    })();
}

无论fn函数在哪里调用,自由变量都要去创建这个函数的作用域中查找。而不是调用的那个作用域,这就是‘静态作用域’

const food = 'rice';
const eat = function(){
    console.log(`eat${food}`);
}
(function(){
    const food = 'noodle';
    eat(); //eat rice
})();

在本例中,eat函数定义在全局上下文,所有输出的时'eat rice'。

const food = 'rice';

(function(){
    const food = 'noodle';
    const eat = function(){
        console.log(`eat ${food}`);
    }
    eat(); //eat noodle
})();

在本例中,eat函数定义在全局上下文,所有输出的时'eat noodle'。

九、this指向

this总是会返回一个对象 ‘谁调用它this就指向谁’,this的指向规律包含以下几条:

  • 在函数体中,非显示或隐式的调用函数时,严格模式下,函数内的this被绑定到undefined上。非严格模式下,会被绑定到全局对象window/global上。
  • 一般使用new方法调用构造函数时,构造函数中的this会被绑定到新创建的实例对象上。
  • 一般通过call/apply/bind方法显示调用函数时,函数体内的this会被绑定到指定参数的对象上。
  • 一般通过上下文对象调用函数时,函数体内的this会被绑定到该对象上
  • 在箭头函数中,this的指向是由外层(函数或全局)作用域来决定的。

例如:

全局环境中的this

// 1. 在全局作用域下,this指向window
console.log(this === window);
var a = 123;
console.log(this.a, window.a);
// 2. 函数以普通调用的方式,this指向window或undefined
function fn() {
    // 'use strict'; 严格模式下this指向undefined
    console.log(this); //window
}
fn();

上下文环境中调用函数

//  1. 通过对象调用函数时,this指向对象
var obj = {
    name:'obj',
    fn:function(){
        console.log(this);// obj
    }
}
obj.fn();
// 2. 通过对象的对象属性来调用函数
var father = {
    name:'father',
    son:{
        name:'son',
        fn:function(){
            console.log(this);// father.son
        }
    }
};
father.son.fn();// son
//
  // 5. 多级嵌套时
var o1 = {
    text:'o1',
    fn(){
        setTimeout(() => {
             console.log("o1-setTimeout",this);
        }, 500);
        return this.text;
    }
};
var o2 = {
    text:'o2',
    fn(){
        setTimeout(() => {
            console.log("o2-setTimeout",this);
        }, 500);
        return o1.fn();
    }
}
var o3 = {
    text:'o3',
    fn(){
        var fn2 = o1.fn;
        return fn2();
       //return function()就是全局调用,全局没有text
    }
}
//o1.fn()-'o1','o1',o1-setTimeout--o1
console.log(o1.fn());
//o2.o1.fn()---'o1',o1-sT-o1,o2-sT--o2
console.log(o2.fn());
//undefined,o1-setTimeout-window,o3-setTimeout-o3
console.log(o3.fn());

this指向绑定事件的元素

DOM元素绑定了事件,事件处理函数中的this指向的是绑定了事件的元素。 this--绑定了事件的元素。 target-指向的触发事件的元素。

<ul class="list">
    <li class="li">11111</li>
    <li class="li">22222</li>
    <li class="li">33333</li>
    <li class="li">44444</li>
</ul>
// 绑定事件的事件处理函数中的this
var list = document.querySelector(".list");
list.onclick = function(event){
    console.log(this);// ul元素
    console.log(event.target);//li/ul
    console.log(event.srcElement);//li/ul
}

事件处理函数内部定义一个回调函数,回调中的this指向,是指向window的,想要指向绑定事件的元素

 // 事件处理函数中,定义一个回调函数(局部的),回调函数内的this
var box = document.querySelector(".box");
box.onclick = function(){
    console.log(this.className);// 'dox'
    var callback = function(){
        console.log("callback",this);// window
    }
    // 局部函数普通调用,函数内的this指向window
    callback();

// 如果想要callback中的this和外层函数的this一致
box.onclick = function(){
    console.log(this.className);// 'dox'
    // 用一个self变量保存this,在callback中使用
    var self = this;
    var callback = function(){
        // 指向外层函数的this
        console.log("callback",self);
    }
    callback();// 闭包
}

改变this指向

  • call A.call(B,a,b,c); A通常是一个函数,B通常是一个对象,abc是传给A的参数。 调用A方法,它内部的this指向B。
  • apply A.apply(B,[a,b,c])
  • bind var newFunc = A.bind(B);

十、垃圾回收与内存泄露

  • 介绍一下js中的垃圾回收机制

什么是内存泄露

程序运行需要内存。 对于持续运行的服务进程,必须及时释放不再使用的内存,否则内存约占越高,影响系统的性能,甚至导致进程崩溃。 不在用到的内存没有及时释放,就叫做内存泄露(memory leak).

js中的垃圾回收

浏览器的js具有自动垃圾回收机制(GC,garbage,collecation).执行环境会负责管理代码执行过程中使用的内存,原理是:垃圾收集器会定期(周期性)找出那些不再使用的变量,然后释放其内存。

这个过程不是实时的,因为实时的监测开销会比较大并且GC时停止响应其他操作。所以垃圾回收是以固定的时间间隔周期性的执行。

不再使用的变量,也就是生命周期结束的变量。只能是局部变量。全局变量的生命周期直到浏览器卸载页面才结束。 局部变量只在函数执行过程中存在,而在这个过程中,为局部变量分配相应的栈内存或堆内存空间,来存储值。然后在函数中使用这些变量,当函数执行完毕,就释放。

闭包,由于内部函数的原因,外部函数并不能算是结束,一直不能释放外部的变量占用的内存空间。

垃圾回收有两种方式:标记清楚和引用计数。

标记清除-常用

当变量进入环境时,例如声明了一个函数,就将这个变量标记为‘进入环境’。 从逻辑上讲,永远不能释放进入环境的变量,因为‘流’进入相应的环境就可能用到他们。 当变量离开环境时,就标记为‘离开环境’

function fn(){
    var a=1; // 标记为‘进入环境’
}
fn();// 执行完毕后,a又被标记为‘离开环境’,等到了时机,就被回收

垃圾回收器在运行的时候,给存储在内存中的所有变量都加上标记(可以使用任何标记方式)(进入?离开?准备删除?)。 然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包)。在此之后再被加上‘离开环境’标记的变量,将被视为准备删除的变量。 因为加了‘离开标记’的变量在环境中已经无法访问到了。 最后,垃圾回收器完成内存清理工作,销毁那些带‘离开环境’标记的值并回收它们所占的内存。

so far,IE9+、FirFox、OPen、Chrome、safari的js实现,使用的都是标记清除或类似的垃圾回收策略,只不过垃圾收集的时间间隔互不相同。

引用计数-不常用了

跟踪记录每个值被引用的次数。

  1. 当声明一个变量,并将一个引用类型的值赋给该变量时,这个引用类型的值的引用次数就是1,如果同一个变量又赋给了另一个变量就加一。
  2. 相反,如果指向这个引用类型的值的变量又重新取得了另一个值,则这个引用类型的值的引用次数就减一。
  3. 当引用类型的值的引用次数为0时,就说明没法再访问这个值了,这时就可以将其占用的内存空间回收。
  4. 垃圾回收器下次再运行时,就会释放引用次数为0的值占用的内存。
function fn(){
    var a = {}; //a指向对象的引用次数为1
    var b=a; //a指向对象的引用次数+1,为2
    var c=a; //a指向对象的引用次数+1,为3
    var b = {};//a指向对象的引用次数-1,为2.且b指向的引用对象的引用次数为1
}

NetScope Navigator3是最早使用引用计数策略的浏览器,但是他遇到一个很棘手的问题,两个对象互相引用。如果fn函数被大量调用就会造成内存泄露。IE7和IE8,内存直线上升。

function fn(){
    var a = {};
    var b = {};
    a.pro = b;
    b.pro = a;
}
fn();

十一、DOM中事件的注册和移除

事件注册并不是一开始就设计了多种,而是一个个改进设计出来的

html元素注册事件

又称之为‘行内事件监听器’,这是浏览器中处理时间最原始的方法,

<button onclick="test(12)">点击</button>
function test(val){
    console.log(val);
}

缺点:

  • 破坏了结构行为相分离的理念
  • 每个元素只能绑定一个这种类型的事件
  • 事件处理函数的代码隐藏于标记中,很难找到 优点:
  • 简单的测试的话,还是很方便快捷的

DOM0级方式注册事件

  • 获取dom元素
  • 给该元素的事件属性赋值
var div = document.querySelector("div");
div.onclick = function(){
    console.log(1);
}
div.onclick = function(){
    console.log(2);
}

缺点:

  • 依然每个元素只能绑定一个同类型的事件,第二个覆盖前者。

DOM2级方式注册事件

  • 获取dom元素。
  • dom.addEventListener(eventtype,function(){},bool);
  • 第三个参数true-捕获阶段,false-冒泡阶段-默认值。
var div = document.querySelector("div");
div.addEventListener("click",function(){
    console.log(1);
});  
div.addEventListener("click",function(){
    console.log(1);
}); 

优点:

  • 同类型的事件在一个元素可以绑定多个,按绑定的顺序执行。
  • 还可以设置事件发生在哪个阶段,冒泡/捕获 缺点:
  • IE dom.attach()兼容

移除事件

  • div.onclick = null;
var div = document.querySelector("div");
div.onclick = function(){// 只执行一次
    console.log(1);
    div.onclick = null;
}
div.onclick = function(){
    console.log(2);
    div.onclick = null;
}
var div = document.querySelector("div");
function fn1(){
    console.log(1);   
    // fn1只执行依次
    div.removeEventListener(type,fn1);
}
function fn2(){
    console.log(1);
}
//绑定事件
div.addEventListener("click",fn1);  
div.addEventListener("click",fn2); 

//div.removeEventListener(type,fn1,false);
//div.removeEventListener(type,fn2,false);

十一、事件与事件流

起初事件都由服务器完成,后端压力太大,一些界面上的东西和简单的运算由js完成,会减轻后端的压力。所以有了js,有了js的事件。

html元素是有多层的,所以给元素绑定一个事件,比如点击事件,点了button,也会点到父元素div,也会点到body,html,document,window。就形成了事件流的概念。 IE的事件流是事件冒泡,NetScape的事件流是事件捕获。

事件冒泡:button->div->body->html->document->window

事件捕获:window->document->html->body->div->button

标准DOM事件流

DOM标准采用的是:捕获+目标元素(属于冒泡的一部分)+冒泡 document->body->div->button->div->body->document

事件委托

  • 将事件委托给父元素
  • 通过判断e.target区分具体点击的元素 (事件委托的原理就是利用了事件冒泡)
<ul class="list">
    <li class="li">11111</li>
    <li class="li">22222</li>
    <li class="li">33333</li>
    <li class="li">44444</li>
</ul>

// 给每一个li绑定点击事件,代码里增加且重复
ul.addEventListener("click",function(e){
    if(e.target.tagName==='LI'){
        console.log(e.target);
    }
})

优点:

  • 父元素上绑定事件,事件更少,效率更高
  • 占用更少的内存、整体的运行性能更好

十二、阻止事件的默认行为

a标签,设置href,会自动跳转; form表单有action属性,当点击form内的提交按钮时,会自动提交

这些就是HTML元素中的默认行为

有些时候不需要默认行为,比如form想自己采用ajax发送,就需要阻止默认行为

阻止默认行为

  • cancelable属性,只读

cancelable 事件属性返回一个布尔值,指示事件是否为可取消事件。 如果可以阻止事件默认操作,则该事件是可取消的。

<a htef="www.baidu.com">百度</a>
var a = document.querySelector("a");
a.onclick = function(event){
//  cancelable用来判断默认事件是否可以取消
    console.log( event.cancelable);
//  event.preventDefault() 最标准的取消浏览器默认行为的方法,无返回值
    event.preventDefault();
}
  • preventDefault()方法

如果事件是可取消的,则 preventDefault() 方法会取消该事件,这意味着属于该事件的默认操作将不会发生。

举例,在以下情况下有用:

  1. 单击“提交”按钮,阻止其提交表单
  2. 单击链接,防止链接跟随 URL

注释:并非所有活动都可以取消。请使用 cancelable 属性 来确定事件是否可取消。

注释:preventDefault() 方法不会阻止事件通过 DOM 进一步传播。请使用 stopPropagation() 方法来解决

  • returnValue属性-可读写

阻止默认事件同preventDefault方法一样。 最早出现在IE,现在很多浏览器实现了该方法。

<a htef="www.baidu.com">百度</a>
var a = document.querySelector("a");
a.onclick = function(event){
//  cancelable用来判断默认事件是否可以取消
    console.log( event.cancelable);
//  event.returnValue=false 取消浏览器默认行为的方法
    event.returnValue=false;
}
  • return false 该语句可以阻止默认行为。 但是在Jquery中,能够同时阻止冒泡和捕获,在js中只阻止默认行为。
<a htef="www.baidu.com">百度</a>
var a = document.querySelector("a");
a.onclick = function(event){
//  cancelable用来判断默认事件是否可以取消
    console.log( event.cancelable);
//  return false 取消浏览器默认行为的方法
    return false;
}
  • defaultPrevented方法 也是event上的,表示默认行为是否可以阻止,返回true-表示被阻止,false-未被阻止
<a htef="www.baidu.com">百度</a>
var a = document.querySelector("a");
a.onclick = function(event){
//  cancelable用来判断默认事件是否可以取消
    console.log( event.cancelable);
//  return false 取消浏览器默认行为的方法
    return false;
}

十三、递归

递归:方法自己调用自己,需要满足两个条件:

  • 先要确定方法的结束条件,否则会形成死循环
  • 每次调用的时候都需要根据需求改变传递的参数内容
// 斐波那契数列:1 1 2 3 5 8...
function fn(n) {
    if (n === 1 || n === 2 || n < 1) {
        return 1;
    } else {
        return fn(n - 1) + fn(n - 2);
    }
}
console.log(fn(0));

// 1-100累加,sum(3)=3+sum(2)=3+2+sum(1)
function sum(n) {
    if (n === 1) {
        return 1;
    } else {
        return n + sum(n - 1);
    }
}
console.log(sum(3));       

注意:

  1. 递归的优点是:定义简单,逻辑清晰。理论上,所有的递归函数都可以用循环的方法实现。
  2. 需要注意防止栈溢出,函数调用用的是栈结构实现的,每当调用一个,栈就会加一层。每当函数返回,栈就会减一层。栈的大小不是无限的。所以递归调用的系数过多,会导致栈溢出。

十四、属性描述符

十五、严格模式

什么是严格模式

严格模式是从ES5开始新增的一种方式,是采用具有限制性js变体的一种 方式,从而使代码隐式的脱离‘马虎模式/稀松模式/懒散模式’(sloppy)模式。 提出'严格模式'的目的:

  1. 消除js语法的一些不合理、不严谨之处,减少一些怪异行为。
  2. 消除代码运行的一些不安全之处,使代码安全运行;
  3. 提高编译器效率,增加运行速度
  4. 为未来新版本的js做好铺垫

使用方法

"use strict"

"严格模式"体现了js向更合理、更严谨、更安全的方向发展,支持严格模式的浏览器有:IE10+、Firfox、4+、Chrome13+、Safari5.1+、Opera12+。

使用方法:

  • 针对整个脚本:放在文件的第一行
  • 针对某个函数:放在函数体第一行
    "use strict";
    console.log("这是严格模式");
<script>
    "use strict";
    console.log("这是严格模式");
</script>
<script>
    console.log("这不是严格模式");
    
    function fn(){
        "use strict";
        console.log("这是严格模式");
    }
    
    (function(){
        "use strict";
        console.log("这是严格模式");
    })();
</script>

严格模式和普通模式区别

  1. 严格模式下没有用var声明的变量,使用会报错;普通模式下会会挂到全局对象上,称为全局变量。
"use strict";
 a=12;// RenferenceError:a is not defined
 console.log(a);
 function f(){
     var a = 13;
     console.log(a);
 }
 f();
  1. 严格模式删除变量和不允许删除的属性会报错;普通模式虽然会失败,但是是‘静默失败’。
"use strict";
 var a=12;
 delete a;// SyntaxError
 delete Object.prototype;// SyntaxError
  1. 函数中相同的形参名会报错
// SyntaxError:Duplicate parameter name...
function f(a,a){
}
  1. 对象不能有重名的属性--ES6中已解决,重名会覆盖,不会报错了。
var obj = {
    name:1,
    name:2
}
  1. 严格模式下八进制使用0o开头
var a = 010;// 建议使用0o开头
console.log(a);//8

6.严格模式下函数普通调用,this为undefined;普通模式下为全局对象。

function fn(){
    "use strict";
    console.log(this);
}
fn();
  1. 创设eval作用域;普通模式下只有全局和函数。
// 普通模式下,eval的作用域取决于它在函数还是全局。
// 严格模式下,eval就是一个作用域,外部不能访问。
var x=10;
console.info(eval("var x=5;x"));
console.info(x);
  1. 保留字 新增了:implements、interface、let、package、private、public、static、yield。用这些给变量命名会报错。
  2. 严格模式下的箭头函数的this指向undefined

十六、深浅拷贝

  • 深浅拷贝的区别?如何实现?

概念

  • 浅拷贝:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用。我们把这个拷贝叫浅拷贝或浅复制。浅拷贝只复制某个对的指针(引用地址),而不是复制对象本身,新旧对象还是共享同一块内存。
  • 深拷贝:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象完全隔离,互不影响。

浅拷贝方法

  • 直接等号赋值
var a = 111;
var b = a;
console.log(a === b);

var obj1 = { name: 'zhangsan' };
var obj2 = obj1;
obj2.name = 'lisi';
console.log(obj1 == obj2);
console.log(obj1.name);
// 修改新对象也会影响源对象的值。
// 因为浅拷贝拷贝的是引用地址
// obj1和obj2在堆内存中共用一个内存。
  • Object.assign()方法
// 将所有可枚举属性的值 从一个或多个源对象分配到新对象。
// 第一个参数是指复制到哪个对象上,目标对象。
// 添加的属性名重复会被覆盖。
var stu1 = {
    name: 'shanshan'
}
// 法一:第一个参数是源对象
var stu2 = Object.assign(stu1, { age: 12 }, { age: 18 }, { gender: 'famale' });
console.log(stu2);// {name:'shanshan',age:12,gender:'famale'}
stu2.name = 'hello';
console.log(stu1, stu2);

// 法二:第一个参数是{}
var stu3 = Object.assign({}, stu2);
stu3.name = 'word';
        console.log(stu1.name, stu2.name, stu3.name);

// 特殊情况:若源对象是层层嵌套的,那么还是浅拷贝
var per = {
    name: 'lisi',
    info: {
        age: 12
    }
}

var per1 = Object.assign({}, per);
per1.name = 'zhangsan';
per1.info.age = 18;
// 第一层是值拷贝,第二层是引用地址拷贝,所以name不同,age都为18
console.log(per, per1);
// 所以Object.assign()也是浅拷贝
  • ES6的扩展运算符
// 一、数组的拷贝
var arr = [1, 2, 3];
var arr1 = [1, 2, 3, ...arr];
console.log(arr1);

        // 二、对象拷贝
        var animal = {
            name: 'dog'
        };
        // 1. 只有一层,值互不影响
        var animal1 = { ...animal };
        animal1.name = 'cat';
        console.log(animal, animal1);//{name: 'dog'} {name: 'cat'}
        // 2. 两层或以上,里面的对象就是引用地址拷贝
        var taidi = {
            name: 'taidi',
            info: {
                legs: '4',
                color: 'zong'
            }
        }
        var xiaobai = { ...taidi };
        xiaobai.name = 'xiaobai';
        xiaobai.info.color = 'bai';
        console.log(taidi, xiaobai);// name不同,color都是bai

        // 所以使用扩展运算符也是浅拷贝

  • 数组的slice和concat方法 concat返回一个浅复制了原数组中的元素的新数组
// 一、slice方法
var names = ['zhangsan', 'lisi', 1, true];
var names1 = names.concat();
names1[2] = 'beauty';
console.log(names, names1);// 两数组的第三个不同,因为原始类型是值的复制

// 二、slice方法
var names2 = names.slice();
names2[0] = 'names2';
console.log(names, names2);

var objArr = [{ name: 'zhang', age: 12 }];
var objArr1 = objArr.concat();
objArr1[0].age = 188;
console.log(objArr, objArr1);// 两个的age都变成了188

var objArr3 = objArr.slice();
objArr3[0].name = 'objarr3';
console.log(objArr, objArr1, objArr3);// 三个[{name:'objarr3',age:188}]

// 所以数组的concat和slice也是浅拷贝
  • jQuery的.extend(),可以实现深浅拷贝使用方法:.extend(),可以实现深浅拷贝 使用方法:.extend(boool,targetObj,sourceObj),boool:true-深拷贝,false浅拷贝
var food = {
    name: 'rice',
    info: {
        from: 'xinjiang'
    }
}
// 深拷贝
var cloneFood = {};
$.extend(true, cloneFood, food);
cloneFood.name = 'noodle';
cloneFood.info.from = 'beijing';

// * bool为默认值:name不同,from都是beijing
// * bool为true:name不同,from也不同
console.log(food, cloneFood);

深拷贝方法

  • JSON.stringify和JSON.parse方法
var apple = {
    name: 'apple',
    info: {
        from: 'shanxi'
    }
}
var apple1 = JSON.parse(JSON.stringify(apple));
apple1.name = 'hongfushi';
apple1.info.from = 'keerle';
console.log(apple, apple1);// name,from都不同,实现了深拷贝

// 缺点:这两个方法都是json的,json的值是不允许有函数的,如果有函数,直接忽略。
  • $.extend(true,target,source)

  • 原生js,用递归自己封装

function deepClone(target) {
    var result;// 克隆的结果
    if (typeof target === 'object') {
        if (target === null) {
            result = null;
        } else if (target.constructor === RegExp || target.constructor === Date) {
            result = target;
        } else if (Array.isArray(target)) {
            result = [];
            for (let i = 0; i < target.length; i++) {
                if (target[i] === "object") {
                    result.push(deepClone(target[i]));
                } else {                                   
                    result.push(target[i]);
                }
            }
        }else {
            result = {};
            for (const key in target) {
                if (Object.hasOwnProperty.call(target, key)) {
                    const element = target[key];
                    result[key] = deepClone(element);
                }
            }
        }
    } else {
        result = target;
    }
    return result;
}

var cat = {
    name: 'bosimao',
    info: {
        age: 2,
        color: 'bai'
    }
}
var huahua = deepClone(cat);
huahua.name = 'huahua';
huahua.info.age = 4;

console.log('猫', cat, huahua);

十七、尺寸和位置

DOM对象相关的尺寸和位置属性

  • 只读属性
  1. clientWitd和clientHeight
  2. offsetWidth和offsetHeight
  3. clientTop和clientLeft
  4. offsetTop和offsetTop
  5. scrollWidth和scrollHeight
 body {
     background-color: aquamarine;
 }

 .box {
     /* width: 500px; */
     height: 500px; 
     background-color: blueviolet;
     border: 2px solid red;
     overflow: auto;
     margin: 0 auto;
 }

 .content {
     width: 500px;
     height: 800px;
     border: 10px solid green;
     padding: 10px;
     margin: 10px;
     background-color: blanchedalmond;
 }

<div class="box">
    <div class="content">
        123
    </div>
</div>

<script>
var box = document.querySelector(".box");
var content = document.querySelector(".content");
content.onclick = function (e) {
// dom元素的的宽高,content+padding
    console.log(`clientWidth=${this.clientWidth},clientHeight=${this.clientHeight}`);
    // dom元素的宽高,content+padding+border
    console.log(`offsetWidth=${this.offsetWidth},offsetHeight=${this.offsetHeight}`)
    // 元素的border的宽高
    console.log(`clientTop=${this.clientTop},clientLeft=${this.clientLeft}`);

    // offsetParent:指的是离自己最近的定位元素,没有定位元素则返回body
    // offsetTop和offsetLeft的border就是距离offsetParent的border的距离
    console.log(`offsetTop=${this.offsetTop},offsetLeft=${this.offsetLeft}`);
    // scrollHeight和scrollWidth,内部元素的实际宽高:margin+padding+content+border
    console.log(`scrollWidth=${this.scrollWidth},scrollHeight=${this.scrollHeight}`);
    console.log(`scrollWidth=${box.scrollWidth},scrollHeight=${box.scrollHeight}`);
            
    box.onscroll = function(e){
        // 元素的内容超过元素高度时,被卷起来的高度
        console.log(`scrollTop=${box.scrollTop},scrollLeft=${box.scrollLeft}`);
        box.style.backgroundColor = "yellow";
    }
</script>
  • 可读写属性
  1. scrollTop和scrollLeft属性
  2. dom.style.xxx

event事件对象相关的尺寸和位置属性

  1. clientX和clientY
  2. screenX和screenY
  3. offsetX和offsetY
  4. pageX和pageY
 // 鼠标点击的位置距离窗口左上角的距离,窗口大小变化没有影响
console.log(`clientX=${e.clientX},clientY=${e.clientY}`)
// 鼠标点击位置,距离屏幕的左上角的距离,可以缩放窗口测试
console.log(`screenX=${e.screenX},screenY=${e.screenY}`)
// 鼠标点击位置距离当前元素的content的左上角的距离
console.log(`offsetY=${e.offsetX},offsetY=${e.offsetY}`)
// 相对于页面的位置,页面没有滚动条时与client一致,有滚动条时比clientXY大
console.log(`pageY=${e.pageX},pageY=${e.pageY}`);

十八、Node的事件循环

十九、eval

二十、函数的防抖和节流

函数被频繁调用的几个场景:

  • mousemove事件:拖拽功能,需要一路监听路径变化,在回调中获取鼠标的位置,并设置拖拽元素的位置,来进行样式改变。如果不加以控制,这个回调被调用的次数的非常大的,回调中又有dom操作,继而引发浏览器的重排和重绘,性能差的浏览器可能就直接假死了。
  • window.onresize:回调的调用次数也很大,如果再伴随着dom操作是非常销毁性能的,浏览器可能会因为吃不消而出现卡顿。
  • 射击游戏的mousedown/keydown,单位时间内只能攻击依次。
  • 监听滚动条滚动事件:判断是否到页面底部,自动加载更多(scroll事件)
  • 百度搜索联想(keyup事件) 为了解决以上问题,引出了防抖和节流(debounde and throttle)。实现原理就是限制某个方法的频繁触发

函数防抖

简述:函数防抖是指防止函数在极短时间内反复调用,造成资源的浪费。 如:电梯一个一个进人,最后一个进入后,等三秒,没人则关门,有人则重新等三秒。

// 每次只执行最后一次
function debounce(fn,timeout){
    var timerId;
    return function(){
        if(timerId){
            //进入if,说明目前处于等待timeout中
            // 但是由于又调用了,所以需要重新开始计时
           clearTimeout(timeId);
        }
        // 开始计时
        var timerId = setTimeout(function(){
            fn(...arguments);
        },timeout)
    }
}
// 只执行第一次的
function debounce(fn,timeout){
    var timerId=null;
    return function(){
        if(timerId){
            //进入if,说明目前处于等待timeout中
            // 但是由于又调用了,所以需要重新开始计时
           return;
        }
        // 开始计时
        var timerId = setTimeout(function(){
            fn(...arguments);
            clearTimeout(timeId);
        },timeout)
    }
}

函数节流

函数节流也是为了防止函数在短时间内被频繁调用。 原理:让连续的函数执行,变为固定时间段间断地执行。 比如人眨眼睛,一定时间内眨眼一次。实现方式:

  • 使用时间戳
function throttle(fn,wait){
    var args;
    var previous=0;// 一开始的默认时间
    return function(){
        var now = new Date();//最新时间
        args = arguments;
        // 判断时间间隔
        if(now-previous>wait){
            fn.apply(null,args);
            previous = now;
        }
    }
}

  • 设置定时器
function throttle(fn,interval){
    var timerId=null;
    return function(){
       if(!timerId){
           fn.apply(null,arguments);
           timerId = setTimeout(function(){
               timeId=null;
           },interval);
        }
    }
}

区别

函数防抖:执行一段时间内的最后一次。 函数节流:每隔一段时间执行一次。

二十一、class和构造函数的区别

二十二、浮点数精度问题

二十三、WeakSet和WeakMap

二十四、函数柯里化

二十五、更多知识