JavaScript

什么是闭包

闭包的实质是因为函数嵌套而形成的作用域链

比如说:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包

  • 优点:可以避免变量被全局变量污染
  • 缺点:函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包
  • 解决方法:在退出函数之前,将不使用的局部变量全部删除
for(var i = 0; i < 10; i++) {  
    setTimeout(function() {  
        console.log(i);    
    }, 1000);  
}  
//输出结果都是10

for(var i = 0; i < 10; i++) {  
    (function(j) {  
        setTimeout(function() {  
            console.log(j);    
        }, 1000);  
    })(i);  
} 
//外部匿名函数立即执行,把 i 作为参数,赋值给 j ,因为是立即执行,所以每次循环输出不同值

原型和原型链

  • 每个对象都会在其内部初始化一个属性,就是prototype(原型)
  • 每个对象有一个私有属性,记作prototype,属性又有自己的prototype,形成了原型链
function Foo() {
    this.value = 42;
}
Foo.prototype = {
    method: function() {}
};

function Bar() {}

// 设置Bar的prototype属性为Foo的实例对象
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';

// 修正Bar.prototype.constructor为Bar本身
Bar.prototype.constructor = Bar;

var test = new Bar() // 创建Bar的一个新实例

// 原型链
test [Bar的实例]
    Bar.prototype [Foo的实例] 
        { foo: 'Hello World' }
        Foo.prototype
            {method: ...};
            Object.prototype
                {toString: ... /* etc. */};

上面的例子中,test 对象从Bar.prototypeFoo.prototype 继承下来;因此, 它能访问 Foo 的原型方法 method。同时,它也能够访问那个定义在原型上的 Foo 实例属性 value。 需要注意的是 new Bar() 不会创造出一个新的 Foo 实例,而是 重复使用它原型上的那个实例;因此,所有的 Bar 实例都会共享相同的 value 属性。

prototype 是什么

prototype 是函数对象上面预设的对象属性

作用域与命名空间

尽管 JavaScript 支持一对花括号创建的代码段,但是并不支持块级作用域; 而仅仅支持 函数作用域。

function test() { // 一个作用域
    for(var i = 0; i < 10; i++) { // 不是一个作用域
        // count
    }
    console.log(i); // 10
}

如果 return 对象的左括号和 return 不在一行上就会出错

//下面输出 undefined
function add(a, b) {
    return 
        a + b;
}
console.log(add(1, 2));

隐式的全局变量

  • 不使用 var 声明变量将会导致隐式的全局变量产生
// 全局作用域
var foo = 42;
function test() {
    // 局部作用域
    foo = 21;
}
test();
foo; // 21

在函数 test 内不使用 var 关键字声明 foo 变量将会覆盖外部的同名变量。 起初这看起来并不是大问题,但是当有成千上万行代码时,不使用 var 声明变量将会带来难以跟踪的 BUG

局部变量

JavaScript 中局部变量只可能通过两种方式声明,一个是作为函数参数,另一个是通过 var 关键字声明。

// 全局变量
var foo = 1;
var bar = 2;
var i = 2;

function test(i) {
    // 函数 test 内的局部作用域
    i = 5;

    var foo = 3;
    bar = 4;
}
test(10);

fooi 是函数 test 内的局部变量,而对 bar 的赋值将会覆盖全局作用域内的同名变量。

var someVar = "Hat";
function myFun() {
    var someVar = "Head";
    return someVar;
}

函数myFun将会返回"Head",因为局部变量优先级更高

变量声明提升(Hoisting)

bar();
var bar = function() {};
var someValue = 42;

test();
function test(data) {
    if (false) {
        goo = 1;

    } else {
        var goo = 2;
    }
    for(var i = 0; i < 100; i++) {
        var e = data[i];
    }
}

JavaScript 会提升变量声明。这意味着 var 表达式和 function 声明都将会被提升到当前作用域的顶部。

// var 表达式被移动到这里
var bar, someValue; // 缺省值是 'undefined'

// 函数声明也会提升
function test(data) {
    var goo, i, e; // 没有块级作用域,这些变量被移动到函数顶部
    if (false) {
        goo = 1;

    } else {
        goo = 2;
    }
    for(i = 0; i < 100; i++) {
        e = data[i];
    }
}

bar(); // 出错:TypeError,因为 bar 依然是 'undefined'
someValue = 42; // 赋值语句不会被提升规则(hoisting)影响
bar = function() {};

test();

如果没有提升规则(hoisting)的知识,下面的代码看起来会抛出异常 ReferenceError:

// 检查 SomeImportantThing 是否已经被初始化
if (!SomeImportantThing) {
    var SomeImportantThing = {};
}

实际上,上面的代码正常运行,因为 var 表达式会被提升到全局作用域的顶部。

var SomeImportantThing;

// 其它一些代码,可能会初始化 SomeImportantThing,也可能不会

// 检查是否已经被初始化
if (!SomeImportantThing) {
    SomeImportantThing = {};
}

命名空间

只有一个全局作用域导致的常见错误是命名冲突。在 JavaScript中,这可以通过 匿名包装器 轻松解决。

(function() {
    // 函数创建一个命名空间

    window.foo = function() {
        // 对外公开的函数,创建了闭包
    };

})(); // 立即执行此匿名函数

匿名函数被认为是 表达式;因此为了可调用性,它们首先会被执行

( // 小括号内的函数首先被执行
function() {}
) // 并且返回函数对象
() // 调用上面的执行结果,也就是函数对象

有一些其他的调用函数表达式的方法,比如下面的两种方式语法不同,但是效果一模一样。

// 另外两种方式
function(){}();
(function(){}());

继承

既然要实现继承,那么首先我们得有一个父类,代码如下:

// 定义一个动物类
function Animal (name) {
  // 属性
  this.name = name || 'Animal';
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉!');
  }
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};
  1. 原型链继承

核心: 将父类的实例作为子类的原型

function Cat(){ 
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

// Test Code
var cat = new Cat();
console.log(cat.name); //cat
console.log(cat.eat('fish')); //cat正在吃fish
console.log(cat.sleep()); //cat正在睡觉!
console.log(cat instanceof Animal); //true 
console.log(cat instanceof Cat); //true
  • 特点:

    1. 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
    2. 父类新增原型方法/原型属性,子类都能访问到
    3. 简单,易于实现
  • 缺点:

    1. 要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
    2. 无法实现多继承
    3. 来自原型对象的所有属性被所有实例共享
    4. 创建子类实例时,无法向父类构造函数传参
  1. 构造继承

核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
  • 特点:
    1. 解决了1中,子类实例共享父类引用属性的问题
    2. 创建子类实例时,可以向父类传递参数
    3. 可以实现多继承(call多个父类对象)
  • 缺点:
    1. 实例并不是父类的实例,只是子类的实例
    2. 只能继承父类的实例属性和方法,不能继承原型属性/方法
    3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
  1. 实例继承

核心:为父类实例添加新特性,作为子类实例返回

function Cat(name){
  var instance = new Animal();
  instance.name = name || 'Tom';
  return instance;
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false
  • 特点:
    1. 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果
  • 缺点:
    1. 实例是父类的实例,不是子类的实例
    2. 不支持多继承
  1. 拷贝继承
function Cat(name){
  var animal = new Animal();
  for(var p in animal){
    Cat.prototype[p] = animal[p];
  }
  this.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
  • 特点
    1. 支持多继承
  • 缺点
    1. 效率较低,内存占用高(因为要拷贝父类的属性)
    2. 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)
  1. 组合继承

核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
Cat.prototype = new Animal();

Cat.prototype.constructor = Cat;

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
  • 特点
    1. 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
    2. 既是子类的实例,也是父类的实例
    3. 不存在引用属性共享问题
    4. 可传参
    5. 函数可复用
  • 缺点
    1. 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
  1. 寄生组合继承

核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
(function(){
  // 创建一个没有实例方法的类
  var Super = function(){};
  Super.prototype = Animal.prototype;
  //将实例作为子类的原型
  Cat.prototype = new Super();
})();

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true
Cat.prototype.constructor = Cat; // 需要修复下构造函数

ES6中class 的继承

class Person {
    //调用类的构造方法
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    //定义一般的方法
    showName() {
        console.log("调用父类的方法")
        console.log(this.name, this.age);
    }
}
let p1 = new Person('kobe', 39)
console.log(p1) //Person {name: "kobe", age: 39}
//定义一个子类
class Student extends Person {
    constructor(name, age, salary) {
        super(name, age)//通过super调用父类的构造方法
        this.salary = salary
    }
    showName() {//在子类自身定义方法
        console.log("调用子类的方法")
        console.log(this.name, this.age, this.salary);
    }
}
let s1 = new Student('wade', 38, 1000000000)
console.log(s1) //Student {name: "wade", age: 38, salary: 1000000000}
s1.showName()

Class与构造函数的区别

  • Class在语法上更贴合面向对象的写法
  • Class实现继承更加易读、易理解
  • 本质是语法糖,使用prototype原型的继承方式

对象使用和属性

  • 访问对象属性:有两种方式来访问对象的属性,点操作符或者中括号操作符
var foo = {name: 'kitten'}
foo.name; // kitten
foo['name']; // kitten

var get = 'name';
foo[get]; // kitten

foo.1234; // SyntaxError
foo['1234']; // works

两种语法是等价的,但是中括号操作符在下面两种情况下依然有效

  • 动态设置属性
  • 属性名不是一个有效的变量名

删除属性

删除属性的唯一方法是使用delete操作符;设置属性为 undefined 或者 null 并不能真正的删除属性, 而仅仅是移除了属性和值的关联

var obj = {
    bar: 1,
    foo: 2,
    baz: 3
};
obj.bar = undefined;
obj.foo = null;
delete obj.baz;

for(var i in obj) {
    if (obj.hasOwnProperty(i)) {
        console.log(i, '' + obj[i]);
    }
}

上面的输出结果有 bar undefinedfoo null - 只有 baz 被真正的删除了,所以从输出结果中消失。

函数

  • 声明
function foo() {}

上面的方法会在执行前被 解析(hoisted),因此它存在于当前上下文的任意一个地方, 即使在函数定义体的上面被调用也是对的。

foo(); // 正常运行,因为foo在代码运行前已经被创建
function foo() {}
  • 函数赋值表达式
var foo = function() {};

这个例子把一个匿名的函数赋值给变量 foo

foo; // 'undefined'
foo(); // 出错:TypeError
var foo = function() {};

由于 var 定义了一个声明语句,对变量 foo 的解析是在代码运行之前,因此 foo 变量在代码运行时已经被定义过了。

但是由于赋值语句只在运行时执行,因此在相应代码执行之前, foo 的值缺省为 undefined

  • 命名函数的赋值表达式 另外一个特殊的情况是将命名函数赋值给一个变量。
var foo = function bar() {
    bar(); // 正常运行
}
bar(); // 出错:ReferenceError

bar 函数声明外是不可见的,这是因为我们已经把函数赋值给了 foo; 然而在 bar 内部依然可见。这是由于 JavaScript 的 命名处理 所致, 函数名在函数内总是可见的

arguments 对象

JavaScript 中每个函数内都能访问一个特别变量 arguments。这个变量维护着所有传递到这个函数中的参数列表。

注意: 由于 arguments 已经被定义为函数内的一个变量。 因此通过 var 关键字定义 arguments 或者将 arguments 声明为一个形式参数, 都将导致原生的 arguments 不会被创建。

arguments 变量不是一个数组(Array)。 尽管在语法上它有数组相关的属性 length,但它不从 Array.prototype 继承,实际上它是一个对象(Object)。

因此,无法对 arguments 变量使用标准的数组方法,比如 push, pop 或者 slice。 虽然使用 for 循环遍历也是可以的,但是为了更好的使用数组方法,最好把它转化为一个真正的数组。

  • 转化为数组 下面的代码将会创建一个新的数组,包含所有 arguments 对象中的元素
Array.prototype.slice.call(arguments)
  • 传递参数
function foo() {
    bar.apply(null, arguments);
}
function bar(a, b, c) {
    // do something
}

事件绑定的方式

  • 嵌入dom
<button onclick="func()">按钮</button>
  • 直接绑定
btn.onclick = function(){}
  • 事件监听
btn.addEventListener('click',function(){})

事件委托

事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术, 使用事件委托可以节省内存。

<ul>
  <li>苹果</li>
  <li>香蕉</li>
  <li>凤梨</li>
</ul>

// good 减少dom操作
document.querySelector('ul').onclick = (event) => {
  let target = event.target
  if (target.nodeName === 'LI') {
    console.log(target.innerHTML)
  }
}

// bad
document.querySelectorAll('li').forEach((e) => {
  e.onclick = function() {
    console.log(this.innerHTML)
  }
}) 

DOM操作——怎样添加、移除、移动、复制、创建和查找节点

  1. 创建新节点
    • createDocumentFragment() //创建一个DOM片段
    • createElement() //创建一个具体的元素
    • createTextNode() //创建一个文本节点
  2. 添加、移除、替换、插入
    • appendChild()
    • removeChild()
    • replaceChild()
    • insertBefore() //在已有的子节点前插入一个新的子节点
  3. 查找
    • getElementsByTagName() //通过标签名称
    • getElementsByName() //通过元素的Name属性的值(IE容错能力较强,会得到一个数组,其中包括id等于name值的)
    • getElementById() //通过元素Id,唯一性
// 添加新节点
var p1 = document.createElement('p')
p1.innerHTML = 'this is p1'
div1.appendChild(p1) // 添加新创建的元素

// 移动已有节点。注意,这里是“移动”,并不是拷贝
var p2 = document.getElementById('p2')
div1.appendChild(p2)

// 获取父元素
var div1 = document.getElementById('div1')
var parent = div1.parentElement

// 获取子元素
var div1 = document.getElementById('div1')
var child = div1.childNodes

// 删除节点
var div1 = document.getElementById('div1')
var child = div1.childNodes
div1.removeChild(child[0])

this对象

  • this总是指向函数的执行上下文,根据调用方式不同,this的指向不同
  • 以匿名函数或直接调用的函数;this指向全局上下文(浏览器window NodeJS global
  • 以方法调用则this指向调用方法的那个对象
  • 以构造函数调用时,this指向新创建的对象
  • 使用callapply调用时this为指定的那个对象
  • 在事件响应函数中,指向触发这个事件的对象,特殊的是IE中attachEvent中的this总是指向全局对象window
  • 箭头函数中this的指向取决于箭头函数声明的位置,捕获其所在的上下文的this值,作为自己的this

这段代码里的this是什么

1. fn()
   this => window
2. obj.fn()
  this => obj
3. fn.call(xx)
  this => xx
4. fn.apply(xx)
  this=> xx
5. fn.bind(xx)
  this => xx
6. new Fn()
  this =>新的对象
7. fn = ()=> {}
  this =>外面的 this

箭头函数带大括号和不带大括号的区别(大括号:函数体)

当不需要函数体,只返回一个值的时候,箭头函数允许你省略return关键字和外面的大括号。这样就可以将一个简单的函数简化成一个单行语句

const date = ()=> new Date()

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

var sum = (num1, num2) => { return num1 + num2; }

由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错

// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });

构造函数

JavaScript 中的构造函数和其它语言中的构造函数是不同的。 通过 new 关键字方式调用的函数都被认为是构造函数。

在构造函数内部 - 也就是被调用的函数内 - this 指向新创建的对象 Object。 这个新创建的对象的 prototype 被指向到构造函数的 prototype

如果被调用的函数没有显式的 return 表达式,则隐式的会返回 this 对象 - 也就是新创建的对象

function Foo() {
    this.bla = 1;
}

Foo.prototype.test = function() {
    console.log(this.bla);
};

var test = new Foo();

上面代码把 Foo 作为构造函数调用,并设置新创建对象的 prototypeFoo.prototype

显式的 return 表达式将会影响返回结果,但仅限于返回的是一个对象

function Bar() {
    return 2;
}
new Bar(); // 返回新创建的对象

function Test() {
    this.value = 2;

    return {
        foo: 1
    };
}
new Test(); // 返回的对象 {foo: 1}

new Bar() 返回的是新创建的对象,而不是数字的字面值 2。 因此 new Bar().constructor === Bar,但是如果返回的是数字对象,结果就不同了,如下所示:

function Bar() {
    return new Number(2);
}
new Bar().constructor === Number

这里得到的 new Test()是函数返回的对象,而不是通过new关键字新创建的对象,因此:

(new Test()).value === undefined
(new Test()).foo === 1

如果 new 被遗漏了,则函数不会返回新创建的对象。

function Foo() {
    this.bla = 1; // 获取设置全局参数
}
Foo(); // undefined
  • 工厂模式 为了不使用 new 关键字,构造函数必须显式的返回一个值:
function Bar() {
    var value = 1;
    return {
        method: function() {
            return value;
        }
    }
}
Bar.prototype = {
    foo: function() {}
};

new Bar();
Bar();

上面两种对 Bar 函数的调用返回的值完全相同,一个新创建的拥有 method 属性的对象被返回, 其实这里创建了一个闭包。

还需要注意, new Bar() 并不会改变返回对象的原型(也就是返回对象的原型不会指向 Bar.prototype)。 因为构造函数的原型会被指向到刚刚创建的新对象,而这里的 Bar 没有把这个新对象返回(而是返回了一个包含 method 属性的自定义对象)。

new操作符

  • 创建一个空对象,链接原型,绑定this指向
  • 属性和方法被加入到 this 引用的对象中,即执行构造函数
  • 返回新创建的对象

JS 有哪些数据类型

基本数据类型包括UndefinedNullBooleanNumberStringSymbol (ES6新增)六种。 引用数据类型只有Object一种,主要包括对象、数组和函数。

null和undefined的区别

  • null 表示一个对象是“没有值”的值,也就是值为“空”
  • undefined表示一个变量声明了没有初始化(赋值)
  • undefinednullif语句中,都会被自动转为false
  • undefined 不是一个有效的JSON,而 null
  • undefined 的类型(typeof)是 undefined
  • null 的类型(typeof)是 object
  • Javascript将未赋值的变量默认值设为undefined
  • Javascript从来不会将变量设为 null。它是用来让程序员表明某个用var声明的变量时没有值的
  • null转为数字类型值为0,而undefined转为数字类型为NaN(Not a Number)

同步和异步的区别

同步的概念应该是来自于OS中关于同步的概念:不同进程为协同完成某项工作而在先后次序上调整(通过阻塞,唤醒等方式).同步强调的是顺序性.谁先谁后.异步则不存在这种顺序性

同步:浏览器访问服务器请求,用户看得到页面刷新,重新发请求,等请求完,页面刷新,新内容出现,用户看到新内容,进行下一步操作

异步:浏览器访问服务器请求,用户正常操作,浏览器后端进行请求。等请求完,页面不刷新,新内容也会出现,用户看到新内容

防抖和节流

  • 防抖和节流都是防止短时间内高频触发事件的方案。
    • 防抖的原理是:如果一定时间内多次执行了某事件,则只执行其中的最后一次。
    • 节流的原理是:要执行的事件每隔一段时间会被冷却,无法执行。
  • 应用场景有:搜索框实时搜索,滚动改变相关的事件。

防抖

function debounce(delay, cb) {
    let timer
    return function () {
        if (timer) clearTimeout(timer)
        timer = setTimeout(function () {
            cb()
        }, delay)
    }
}

节流

function throttle(cb, delay) {
    let startTime = Date.now()
    return function () {
        let currTime = Date.now()
        if (currTime - startTime > delay) {
            cb()
            startTime = currTime
        }
    }
}

怎么理解Promise对象

promise是异步编程的一种解决方案,解决了回调地狱的问题

  1. Promise对象共有三种状态pendingfulfilledrejected
  2. Promise的缺点:
    • Promise一旦执行便无法被取消
    • Promise一旦执行便无法被取消;
    • 当处于pending状态时,无法得知其具体发展到了哪个阶段
  3. 常用的方法有:
    • Promise.prototype.then() :Promise实例的状态发生改变时,会调用then内部的回调函数
    • Promise.prototype.catch().then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

怎么理解宏任务,微任务

  • 宏任务有:script(整体代码)setTimeoutsetIntervalI/O、页面渲染
  • 微任务有:Promise.thenObject.observeMutationObserver

执行顺序大致如下:

主线程任务——>宏任务——>微任务——>微任务里的宏任务——>.......——>直到任务全部完成

require/import之间的区别

  1. requireCommonJS语法,importES6语法
  2. require只在后端服务器支持,import在高版本浏览器及Node中都可以支持
  3. require引入的是原始导出值的复制,import则是导出值的引用
  4. require是运行时动态加载,import是静态编译
  5. require调用时默认不是严格模式,import则默认调用严格模式

call()、apply()、bind()的区别

call()apply()bind()是用来改变this的指向的

  • call():Function.call(obj, param1,param2,param3) 一个一个的参数
  • apply():Function.apply(obj, [param1,param2,param3]) 参数是数组
  • bind():const newFn = Funtion.bind(obj, param1,param2) 返回值是一个function,调用 newFn(param3,param4) 接收到的是param1,param2,param3,param4四个参数

事件模型

W3C中定义事件的发生经历三个阶段:捕获阶段(capturing)、目标阶段(targetin)、冒泡阶段(bubbling

  • 冒泡型事件:当你使用事件冒泡时,子级元素先触发,父级元素后触发
  • 捕获型事件:当你使用事件捕获时,父级元素先触发,子级元素后触发
  • DOM事件流:同时支持两种事件模型:捕获型事件和冒泡型事件
  1. DOM0 直接绑定
<input onclick="sayHi()"/>

btn.onclick = function() {}
btn.onclick = null
  1. DOM2

    • DOM2级事件可以冒泡和捕获
    • 通过addEventListener绑定
    • 通过removeEventListener解绑
    // 绑定
    btn.addEventListener('click', sayHi)
    // 解绑
    btn.removeEventListener('click', sayHi)
    
  2. DOM3

    • DOM3具有更多事件类型
    • DOM3级事件在DOM2级事件的基础上添加了更多的事件类型,全部类型如下:

    UI事件,当用户与页面上的元素交互时触发,如:loadscroll
    焦点事件,当元素获得或失去焦点时触发,如:blurfocus
    鼠标事件,当用户通过鼠标在页面执行操作时触发如:dbclickmouseup
    滚轮事件,当使用鼠标滚轮或类似设备时触发,如:mousewheel
    文本事件,当在文档中输入文本时触发,如:textInput
    键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydownkeypress
    合成事件,当为IME(输入法编辑器)输入字符时触发,如:compositionstart
    变动事件,当底层DOM结构发生变化时触发,如:DOMsubtreeModified

target和currentTarget区别

  • event.target返回触发事件的元素
  • event.currentTarget返回绑定事件的元素

阻止冒泡

  • W3C中,使用stopPropagation()方法
  • IE下设置cancelBubble = true

阻止事件的默认行为

  • W3C中,使用preventDefault()方法
  • IE下设置window.event.returnValue = false

javascript有哪些方法定义对象

  1. 对象字面量: var obj = {};
  2. 构造函数: var obj = new Object();
  3. Object.create(): var obj = Object.create(Object.prototype);

map与forEach的区别

  • forEach方法,是最基本的方法,就是遍历与循环,默认有3个传参:分别是遍历的数组内容item、数组索引index、和当前遍历数组Array
  • map方法,基本用法与forEach一致,但是不同的,它会返回一个新的数组,所以在callback需要有return值,如果没有,会返回undefined

for in 和 for of 区别

for...in循环出的是keyfor...of循环出的是value







 









 



Object.prototype.objCustom = function() {};
Array.prototype.arrCustom = function() {};

let iterable = [3, 5, 7];
iterable.foo = 'hello';

for (let i in iterable) {  // 循环的是索引
  console.log(i); // 打印 0, 1, 2, "foo", "arrCustom", "objCustom"
}

for (let i in iterable) {
  if (iterable.hasOwnProperty(i)) {
    console.log(i); // 打印 0, 1, 2, "foo"
  }
}

for (let i of iterable) {  // 迭代的是值
  console.log(i); // 打印 3, 5, 7
}

如何做到修改url参数页面不刷新

HTML5引入了history.pushState()history.replaceState() 方法,它们分别可以添加和修改历史记录条目。

let stateObj = {
    foo: "bar",
};

history.pushState(stateObj, "page 2", "bar.html");

动态添加script标签

function loadScript(url, callback){
    var s = document.createElement('script');
    s.type = 'text/javascript';
    if(s.readyState){
        s.onreadystatechange = function(){  //兼容IE
            if(s.readyState == 'complete' || s.readyState == 'loaded'){
                callback();
            }
        }
    }else{
        s.onload = function(){  //safari chrome opera firefox
            callback();
        }
    }
    s.src = url;
    document.head.appendChild(s);
}
//loadScript('https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js',function(){alert(1)})

JSON

  • JSON字符串转换为JSON对象
var obj = eval('(' + str + ')');
var obj = str.parseJSON;
var obj = JSON.parse(str);

应该避免使用eval,不安全,非常耗性能(2次,一次解析成js语句,一次执行)

  • JSON对象转换为JSON字符串
var str = obj.toJSONString();
var str = JSON.stringify(obj);

document load 和 document ready 的区别

页面加载完成有两种事件:

  1. load是当页面所有资源全部加载完成后(包括DOM文档树,css文件,js文件,图片资源等),执行一个函数
    问题:如果图片资源较多,加载时间较长,onload后等待执行的函数需要等待较长时间,所以一些效果可能受到影响

  2. $(document).ready()是当DOM文档树加载完成后执行一个函数 (不包含图片,css等)所以会比load较快执行
    在原生的js中不包括ready()这个方法,只有load方法也就是onload事件

XML和JSON的区别

  • 数据体积:JSON相对于XML来讲,数据的体积小。
  • 数据交互:JSONJavaScript的交互更加方便,更容易解析处理
  • 数据描述:JSON对数据的描述性比XML较差
  • 传输速度:JSON的速度要远远快于XML

WebSocket

由于http存在一个明显的弊端,即消息只能由客户端推送到服务器端,而服务器端不能主动推送到客户端,导致如果服务器如果有连续的变化,这时只能使用轮询。而轮询效率过低,并不适合,于是 WebSocket被发明出来,相比与http 具有以下优点:

  • 支持双向通信,实时性更强
  • 可以发送文本,也可以二进制文件
  • 协议标识符是 ws,加密后是 wss
  • 较少的控制开销。连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小
  • 支持扩展
  • 无跨域问题
  • 实现比较简单,服务端库如 socket.iows,可以很好的帮助入门。客户端也只需要参照 api 实现即可

白屏时间

白屏时间是指浏览器从输入网址,到浏览器开始显示内容的时间。
Performance 接口可以获取到当前页面中与性能相关的信息,该类型的对象可以通过调用只读属性 Window.performance 来获得
performance.timing.navigationStart 是一个返回代表一个时刻的 unsigned long long 型只读属性,为紧接着在相同的浏览环境下卸载前一个文档结束之时的 Unix 毫秒时间戳。如果没有上一个文档,则它的值相当于 PerformanceTiming.fetchStart
所以将以下脚本放在 </head> 前面就能获取白屏时间

<script>
	new Date() - performance.timing.navigationStart
</script>