JS技巧总结

288 阅读5分钟

数据类型

基本数据类型

  • 是一种既非对象也无方法的数据。在 JavaScript 中,共有7种基本类型:string,number,bigint,boolean,null,undefined,symbol

  • 所有基本类型的值都是不可改变的。但需要注意的是,基本类型本身和一个赋值为基本类型的变量的区别。变量会被赋予一个新值,而原值不能像数组、对象以及函数那样被改变。

  • 除了 null 和 undefined之外,所有基本类型都有其对应的包装对象:

    • String 为字符串基本类型。
    • Number 为数值基本类型。
    • BigInt 为大整数基本类型。
    • Boolean 为布尔基本类型。
    • Symbol 为字面量基本类型。

null

  • 指对象的值未设置,在布尔运算中被认为是false
  • null 是表示缺少的标识,指示变量未指向任何对象

undefined

  • 全局属性undefined表示原始值 undefined
  • 一个没有被赋值的变量的类型是 undefined
  • 一个函数如果没有使用return语句指定返回值,就会返回一个undefined值。

string

运算符

==与===

  • 等于运算符(==)检查其两个操作数是否相等,并返回Boolean结果。与严格相等运算符(===)不同,它会尝试强制类型转换并且比较不同类型的操作数。
  • 相等运算符(==和!=)使用抽象相等比较算法比较两个操作数

相等运算符(==和!=)使用抽象相等比较算法比较两个操作数

  • 如果两个操作数都是对象,则仅当两个操作数都引用同一个对象时才返回true
  • 如果一个操作数是 null,另一个操作数是undefined,则返回true
  • 如果两个操作数是不同类型的,就会尝试在比较之前将它们转换为相同类型:
  • 当数字与字符串进行比较时,会尝试将字符串转换为数字值。
  • 如果操作数之一是Boolean,则将布尔操作数转换为1或0。
    • 如果是true,则转换为1。
    • 如果是 false,则转换为0。 如果操作数之一是对象,另一个是数字或字符串,会尝试使用对象的valueOf()toString()方法将对象转换为原始值。 如果操作数具有相同的类型,则将它们进行如下比较:
  • String:true 仅当两个操作数具有相同顺序的相同字符时才返回。
  • Number:true 仅当两个操作数具有相同的值时才返回。+0并被-0视为相同的值。如果任一操作数为 NaN,则返回 false
  • Boolean:true 仅当操作数为两个 true 或两个 false 时才返回 true

此运算符与严格等于(===)运算符之间最显着的区别在于,严格等于运算符不尝试类型转换。相反,严格相等运算符始终将不同类型的操作数视为不同。

自增/自减运算符(++/--)

  • 如使用后置(Postfix)自增,操作符在操作数后(例如 x++/x--), 操作数将在自增前返回。
let x = 3;
y = x++;

// y = 3
// x = 4
let x = 3;
y = x--;

// y = 3
// x = 2
  • 如使用前置(Prefix)自增,操作符在操作数前(例如 ++x/--x), 操作数将先自增后返回。
let a = 2;
b = ++a;

// a = 3
// b = 3
let a = 2;
b = --a;

// a = 1
// b = 1

原型及原型链

牢记两点:

  • __proto__constructor 属性是对象所独有的;

  • prototype 属性是函数所独有的,因为函数也是一种对象,所以函数也拥有 __proto__constructor 属性。 理解关键

  • __proto__属性的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象(父对象)里找,一直找,直到__proto__属性的终点null,然后返回undefined,再往上找就相当于在null上取值,会报错。通过__proto__属性将对象连接起来的这条链路即我们所谓的原型链

  • prototype属性的作用就是让该函数所实例化的对象们都可以找到公用的属性和方法,即f1.__proto__ === Foo.prototype

  • constructor属性的含义就是指向该对象的构造函数,所有函数和对象最终的构造函数都指向Function

  • 每个函数在创建的时候,JS会同时创建一个该函数对应的prototype对象,而函数创建的对象.__proto__ === 该函数.prototype,该函数.prototype.constructor===该函数本身

<script>
    // __proto__ constructor 对象独有  prototype 函数独有(函数也是对象,所以也拥有 __proto__ constructor 属性)
    function Parent(name, age) { // 构造函数
        this.name = name
        this.age = age
    }
    Parent.prototype.sex = '男'
    var p1 = new Parent('fx', 18) // p1是对象实例
    // p1.prototype.sex = '女' // 对象不能设置原型 prototype 是函数独有
    // console.log(p1.name, p1.age, p1.sex) // fx 18 女 如果
    // p1.sex = '女' // 给对象实例新增属性
    // 如果没给p1单独设置sex属性则p1会沿着__proto__找到其构造函数上的属性 找不到则 undefined
    console.log('对象实例p1的属性:', p1)
    console.log(p1.name, p1.age, p1.sex, p1.birthday) // fx 18 男 undefined
    console.log('p1的数据类型是:', judgeType(p1)) // p1的数据类型是: object
    console.log('p1的隐式原型', p1.__proto__) // p1的隐式原型 指向 其构造函数的 显示原型
    console.log('构造函数的显示原型', Parent.prototype) // 构造函数的显示原型
    console.log('对象实例的隐式原型 === 函数的显式原型', p1.__proto__ === Parent.prototype) // true 对象实例的隐式原型 === 函数的显式原型
    console.log('构造函数的隐式原型 === Object的显示原型', Parent.prototype.__proto__ === Object.prototype) // 构造函数的隐式原型 === Object的显示原型
    console.log('Object的隐式原型为null, 原型链的尽头', Object.prototype.__proto__) // null
    console.log('prototype 是函数独有属性, p1.prototype:', p1.prototype) // undefined prototype 是函数独有属性
    // constructor是从一个对象指向一个函数,指向该对象的构造函数
    // 每个对象都有构造函数 constructor 可能是对象自己本身显式定义的或者通过__proto__在原型链中找到的。而单从constructor这个属性来讲,只有prototype对象才有。
    console.log('Parent的数据类型是:', judgeType(Parent)) // Parent的数据类型是: function
    console.log('p1的构造函数', p1.constructor) // p1本身没有constructor属性,但是可以通过__proto__找到,构造函数的.prototype,
    console.log('构造函数原型的构造器是其本身', Parent.prototype.constructor) // 对象本身
    console.log('对象实例的构造器 === 其构造函数原型的构造器', p1.constructor === Parent.prototype.constructor) // true
    console.log('由于p1.__proto__ === Parent.prototype', p1.__proto__.constructor === Parent.prototype.constructor) // true
    // 所有的函数都是构造函数Function的实例
    console.log(Parent.constructor === Function) // Function() { [native code] }
    console.log('Function 的显式原型 === Function 的隐式原型', Function.prototype === Function.__proto__) // Function 的显式原型 === Function 的隐式原型
    console.log('Function 的构造器是他本身 Function.constructor === Function:', Function.constructor === Function)
    console.log('原型链的尽头是null 上一级是Object 所以万物皆对象', Function.prototype.__proto__ == Object.prototype) // 原型链的尽头是null 上一级是Object 所以万物皆对象
    console.log('所有的函数都是构造函数Function的实例', Object.constructor === Function)
    console.log('Object.__proto__ === Function.prototype:', Object.__proto__ === Function.prototype)
    function judgeType (obj) { // 判断数据类型
        const class2type = {}
        'Array Date RegExp Object Error'.split(' ').forEach(e => class2type[`[object ${e}]`] = e.toLowerCase())
        if (obj === null) return String(obj)
        return typeof obj === 'object' ? class2type[Object.prototype.toString.call(obj)] || 'object' : typeof obj
    }
</script>

prototype

  • 它是函数所独有的,从一个函数指向一个对象
  • 含义是函数的原型对象。也就是这个函数所创建的实例的原型对象
  • 作用就是包含所有实例共享的属性和方法,也就是让该函数所实例化的对象们都可以找到公用的属性和方法
  • 任何函数在创建的时候,其实会默认同时创建该函数的prototype对象。

proto

  • __proto__constructor属性是对象所独有的
  • __proto__属性都是由一个对象指向一个对象,即指向它们的原型对象
  • 作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象(父对象)里找,一直找,直到__proto__属性的终点null,然后返回undefined,再往上找就相当于在null上取值,会报错。通过__proto__属性将对象连接起来的这条链路即我们所谓的原型链

constructor

  • __proto__constructor属性是对象所独有的
  • 从一个对象指向一个函数,含义就是指向该对象的构造函数
  • Function这个对象比较特殊,它的构造函数就是它自己
  • 所有函数和对象最终都是由Function构造函数得来,所以constructor属性的终点就是Function这个函数
  • 所有的函数都是构造函数Function的实例

为什么需要原型

js中万物皆对象,每一个对象都拥有自己的属性。但存在一些共同的方法和属性。不可能让每一个对象都定义一个属性。原型就是让多个对象共享一个或多个方法。

原型链:

JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。它解释了为何一个对象会拥有定义在其他对象中的属性和方法。

函数

函数:简单的说就是重复执行的代码块。它只定义一次,但可能被执行或调用任意次。

定义方式

  • 声明式函数定义function 函数名 (){} 这种定义方式,会将函数声明提升到该函数所在作用域的最开头,在该函数所在作用域的任何位置都可调用此函数。
  • 函数表达式let fun = function(){} 此方式定义的函数,只能在该作用域中,这段赋值代码执行之后才能通过fun()调用函数,否则,由于变量声明提升,fun === undefined
  • new Function 形式var fun1 = new Function (arg1, arg2, arg3, … , argN, body) Function 构造函数所有的参数都是字符串类型。除了最后一个参数, 其余的参数都作为生成函数的参数即形参。这里可以没有参数。最后一个参数, 表示的是要创建函数的函数体。
  • 总结:第一种和第二种函数的定义的方式其实是第三种 new Function 的语法糖,当我们定义函数时候都会通过 new Function 来创建一个函数,只是前两种为我们进行了封装,js 中任意函数都是 Function 的实例。

构造函数

定义:通过 new 函数名来实例化对象的函数叫构造函数。构造函数与普通函数之分,主要从功能上进行区别的,构造函数的主要功能为初始化对象,特点是和 new 一起使用。new 就是在创建对象,构造函数就是在为初始化的对象添加属性和方法。构造函数定义时首字母大写(规范)。

常用构造函数

  • var arr = []var arr = new Array() 的语法糖
  • var obj = {}var obj = new Object() 的语法糖

new 关键字

Javascriptnew对象的过程:

// 创建构造函数
function People(name){
  this.name = name;
}

// 使用new创建实例对象person
var fx = new People("Fx");
fx = {
  name: 'fx'
}

代码执行过程

var obj = {} 或者 var obj = new Object() 创建一个空对象;

obj.__proto__ = People.prototype 将该隐式原型指向构造函数显式原型

People.call(obj, "fx") 将构造函数中this指向创建的obj对象,并传入参数"fx"

return obj 返回 obj 对象,fx 指向创建的 obj 对象(对象类型赋值为按引用传递,objfx 指向同一个对象) 相当于(IIFE)

var People = function () {
    var obj = {}
    obj.__proto__ = People.prototype
    People.call(obj, 'Fx')
    return obj
}() // 使用立即执行函数(IIFE)

1.创建一个空的 Object 对象.var obj = new Object();

2.将构造函数Peoplethis指向刚创建的obj 对象

obj->People.prototype->Object.prototype->null

3.将创建的obj__proto__指向构造函数Peopleprototype。这一步是建立对象和原型直接的对应关系。通过对象的__proto__属性能够访问到原型,IE下则没有暴露出相应的属性。

4.执行构造函数People()中的代码

模拟实现new方法

function newOperator(ctor, ...args) {
    if(typeof ctor !== 'function'){
      throw 'newOperator function the first param must be a function';
    }
    let obj = Object.create(ctor.prototype);
    let res = ctor.apply(obj, args);
    
    let isObject = typeof res === 'object' && res !== null;
    let isFunction = typoof res === 'function';
    return isObect || isFunction ? res : obj;
};

this 关键字

this:

  • 包含它的函数作为方法被调用时所属的对象。
  • 1、包含它的函数。2、作为方法被调用时。3、所属的对象。
  • 随着函数使用场合的不同,this的值会发生变化。this指向什么,完全取决于什么地方以什么方式调用,而不是创建时。

this 的四种绑定规则

默认绑定、隐式绑定、显式绑定、new 绑定。优先级从低到高。new 绑定 > 显式绑定> 隐式绑定 > 默认绑定

new 绑定

function foo (a) {
    this.a = a
}
var bar = new foo(2)
console.log(bar.a) // 2

特别注意 : 如果原函数返回一个对象类型,那么将无法返回新对象,你将丢失绑定this的新对象,例:

function foo () {
    this.a = 10
    return new String('zxx')
}
var obj = new foo()

显式绑定

this绑定的是 call,apply,bind 的第一个参数

call()方法

var a = {
    user: 'fx',
    fn: function () {
        console.log(this.user) // fx
    }
}
var b = a.fn
b.call(a)

通过在call方法,第一个参数表示要把b添加到哪个环境中,简单来说,this就会指向那个对象。

call方法除了第一个参数以外还可以添加多个参数,如下

var a = {
    user: 'fx',
    fn: function (x, xx) {
        console.log(this.user) // zxx
        console.log(x + xx) // 520
    }
}
var b = a.fn
b.call(a, '5', '20')

apply()方法

apply方法和call方法有些相似,它也可以改变this的指向

var a = {
    user: 'fx',
    fn: function () {
        console.log(this.user) // fx
    }
}
var b = a.fn
b.apply(a)

同样apply也可以有多个参数,但是不同的是,第二个参数必须是一个数组,如下:

var a = {
    user: 'fx',
    fn: function (x, xx) {
        console.log(this.user) // fx
        console.log(x + xx) // 520
    }
}
var b = a.fn
b.apply(a, ['5', '20'])

bind()方法

bind方法和call、apply方法有些不同,但是不管怎么说它们都可以用来改变this的指向。

先看下面一段代码:

var a = {
    user:"zxx",
    fn:function(){
        console.log(this.user);
    }
}
var b = a.fn;
b.bind(a);
  • 我们发现代码没有被打印,对,这就是bindcall、apply方法的不同,实际上bind方法返回的是一个修改过后的函数。不会立即调用,而是将函数返回
var a = {
    user:"fx",
    fn:function(){
        console.log(this.user); // fx
    }
}
var b = a.fn;
var c = b.bind(a);
c();

如果要调用的话必须还要加上()

同样bind也可以有多个参数,并且参数可以执行的时候再次添加,但是要注意的是,参数是按照形参的顺序进行的。

var a = {
    user:"zxx",
    fn:function(e,d,f){
        console.log(this.user); // zxx 
        console.log(e,d,f); // 10 1 2
    }
}
var b = a.fn;
var c = b.bind(a,10);
c(1,2);

回调函数的中使用bind

var obj = {
    name: 'zxx'
}
 
setTimeout(function () {
    console.log(this) // Object {name: "zxx"}
}.bind(obj), 1000)
 
========
 
var obj = {
    name: 'zxx'
}
 
setTimeout(function () {
    console.log(this) // Window
}, 1000)

如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:

var a = {
    user:"zxx",
    fn:function(){
        console.log(this); // Window
    }
}
var b = a.fn;
b.apply(null);

总结:call和apply(参数为数组)都是改变上下文中的this并立即执行这个函数,bind方法可以让对应的函数想什么时候调就什么时候调用,并且可以将参数在执行的时候添加,这是它们的区别。

间接引用

你有可能(有意或者无意地)创建一个函数的“间接引用”,调用这个函数会应用默认绑定规则,间接引用最容易在赋值时发生

function foo() {
    console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2

赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是

p.foo() 或者 o.foo()。根据我们之前说过的,这里会应用默认绑定。

隐式绑定 函数是在某个上下文对象下被调用

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2

foo() 被调用时,它的落脚点确实指向 obj 对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调用 foo() 时 this 被绑定到 obj,因此 this.a 和 obj.a 是一样的

如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象

function foo() {
    console.log( this.a );
}
var obj2 = {
    a: 42,
    foo: foo
};
var obj1 = {
    a: 2,
    obj2: obj2
};
obj1.obj2.foo(); // 42
var o = {
    a:10,
    b:{
        a:12,
        fn:function(){
            console.log(this.a); // 12
        }
    }
}
o.b.fn(); // 12
 
var o = {
    a:10,
    b:{
        // a:12,
        fn:function(){
            console.log(this.a); //undefined
        }
    }
}
o.b.fn(); // undefined

this永远指向的是最后调用它的对象, 也就是看它执行的时候是谁调用的, 虽然函数fn是被对象b所引用, 但是在将fn赋值给变量j的时候并没有执行所以最终指向的是window

var o = {
    a: 10,
    b: {
        a: 12,
        fn: function () {
            console.log(this.a) //undefined
            console.log(this) //window
        }
    }
}
var j = o.b.fn
j()
function foo () {
    console.log(this.a)
}
var a = 2
var obj = {
    a: 3,
    foo: foo
}
setTimeout(obj.foo, 100) // 2

隐式丢失

一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。

function foo () {
    console.log(this.a)
}
var obj = {
    a: 2,
    foo: foo
}
var bar = obj.foo // 函数别名!
var a = 'oops, global' // a 是全局对象的属性
bar() // "oops, global"

虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

传入回调函数时:

function foo () {
    console.log(this.a)
}
function doFoo (fn) {
    // fn 其实引用的是 foo
    fn() // <-- 调用位置!
}
var obj = {
    a: 2,
    foo: foo
}
var a = 'oops, global' // a 是全局对象的属性
doFoo(obj.foo) // "oops, global"

如果把函数传入语言内置的函数而不是传入你自己声明的函数

function foo () {
    console.log(this.a)
}
var obj = {
    a: 2,
    foo: foo
}
var a = 'oops, global' // a 是全局对象的属性
setTimeout(obj.foo, 100) // "oops, global"
// JavaScript 环境中内置的 setTimeout() 函数实现和下面的伪代码类似:
function setTimeout (fn, delay) {
    // 等待 delay 毫秒
    fn() // <-- 调用位置!
}

默认绑定 独立函数调用。可以把这条规则看作是无法应用 其他规则时的默认规则。

默认绑定 独立函数调用。可以把这条规则看作是无法应用 其他规则时的默认规则。

function foo () {
    console.log(this.a)
}
var a = 2
foo() // 2

在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用 默认绑定,无法应用其他规则。

如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定 到 undefined:

function foo () {
    'use strict'
    console.log(this.a)
}
var a = 2
foo() // TypeError: this is undefinedthis碰到returnfunction fn () {
    this.user = 'fx'
    return {}
}
var a = new fn
console.log(a.user) // undefined
======
function fn () {
    this.user = 'fx'
    return function () {
    }
}
var a = new fn
console.log(a.user) // undefined
======
function fn () {
    this.user = 'fx'
    return 1
}
var a = new fn
console.log(a.user) // fx
======
function fn () {
    this.user = 'fx'
    return undefined
}
var a = new fn
console.log(a.user) // fx

如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。

还有一点就是虽然null也是对象,但是在这里this还是指向那个函数的实例,因为null比较特殊。

function fn () {
    this.user = 'fx'
    return null
}
var a = new fn
console.log(a.user) // fx

题1

var x = 10
var obj = {
    x: 20,
    f: function () {
        console.log(this.x) // 20
        function foo () {
            console.log(this.x)
        }
        foo()  // 10 默认绑定,这里this绑定的是window
    }
}
obj.f()

题2

function foo (arg) {
    this.a = arg
    return this
}
var a = foo(1)
var b = foo(10)
console.log(a.a) // undefined 
console.log(b.a) // 10

foo(1)执行,应该不难看出是默认绑定吧 , this指向了window,函数里等价于 window.a = 1,return window; var a = foo(1) 等价于 window.a = window , 很多人都忽略了var a 就是window.a?,将刚刚赋值的 1 替换掉了。 所以这里的 a 的值是 window , a.a 也是window , 即window.a = window ; window.a.a = window; foo(10) 和第一次一样,都是默认绑定,这个时候,将window.a 赋值成 10?,注意这里是关键,原来window.a = window ,现在被赋值成了10,变成了值类型,所以现在 a.a = undefined。(验证这一点只需要将var b = foo(10);删掉,这里的 a.a 还是window) var b = foo(10); 等价于 window.b = window; 本题中所有变量的值,a = window.a = 10 , a.a = undefined , b = window , b.a = window.a = 10; 题3

var x = 10
var obj = {
    x: 20,
    f: function () {
        console.log(this.x)
    }
}
var bar = obj.f
var obj2 = {
    x: 30,
    f: obj.f
}
obj.f() // 20
bar() // 10
obj2.f() // 30

题4

function foo () {
    getName = function () {
        console.log(1)
    }
    return this
}
foo.getName = function () {
    console.log(2)
}
foo.prototype.getName = function () {
    console.log(3)
}
var getName = function () {
    console.log(4)
}
function getName () {
    console.log(5)
}
foo.getName()                // ?
getName()                    // ?
foo().getName()              // ?
getName()                    // ?
new foo.getName()            // ?
new foo().getName()          // ?
new new foo().getName()      // ?

答案:2 4 1 1 2 3 3 详解

function foo () {
    getName = function () { // 这里的getName将创建到全局window上
        console.log(1)
    }
    return this
}
foo.getName = function () { // 这个getName和上面的不同,是直接添加到foo上的
    console.log(2)
}
foo.prototype.getName = function () { // 这个getName直接添加到foo的原型上,在用new创建新对象时将直接添加到新对象上
    console.log(3)
}
var getName = function () { // 和foo函数里的getName一样, 将创建到全局window上
    console.log(4)
}

// 同上,但是这个函数不会被使用,因为函数声明的提升优先级最高,所以上面的函数表达式将永远替换这个同名函数,除非在函数表达式赋值前去调用getName(),但是在本题中,函数调用都在函数表达式之后,所以这个函数可以忽略了 function getName () { console.log(5) } 通过上面对 getName的分析基本上答案已经出来了

foo.getName (); // 2

getName (); // 4 这里涉及到函数提升的问题,不知道的小伙伴只需要知道 5 会被 4 覆盖,虽然 5 在 4 的下面,其实 js 并不是完全的自上而下

foo().getName (); // 1 这里的foo函数执行完成了两件事, 1. 将window.getName设置为1,返回window , 故等价于 window.getName(); 输出 1

getName (); // 1刚刚上面的函数刚把window.getName设置为1,故同上 输出 1

new foo.getName (); // 2 new 对一个函数进行构造调用 , 即 foo.getName ,构造调用也是调用啊 该执行还是执行,然后返回一个新对象,输出 2 (虽然这里没有接收新 创建的对象但是我们可以猜到,是一个函数名为 foo.getName 的对象 且__proto__属性里有一个getName函数,是上面设置的 3 函数)

new foo().getName ();// 3 这里特别的地方就来了,new 是对一个函数进行构造调用,它直接找到了离它 最近的函数,foo(),并返回了应该新对象,等价于 var obj = new foo();obj.getName(); 这样就很清晰了,输出的是之前绑定到prototype上的 那个getName 3 ,因为使用new后会将函数的prototype继承给 新对象

new new foo().getName (); // 3 让我们来分解一下: var obj = new foo(); var obj1 = new obj.getName(); 好了,仔细看看, 这不就是上两题的合体吗,obj 有getName 3, 即输出3 obj 是一个函数名为 foo的对象,obj1是一个函数名为obj.getName的对象 在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用 默认绑定,无法应用其他规则。

如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定 到 undefined:

function foo () {
    'use strict'
    console.log(this.a)
}
var a = 2
foo() // TypeError: this is undefinedthis碰到returnfunction fn () {
    this.user = 'fx'
    return {}
}
var a = new fn
console.log(a.user) // undefined
======
function fn () {
    this.user = 'fx'
    return function () {
    }
}
var a = new fn
console.log(a.user) // undefined
======
function fn () {
    this.user = 'fx'
    return 1
}
var a = new fn
console.log(a.user) // fx
======
function fn () {
    this.user = 'fx'
    return undefined
}
var a = new fn
console.log(a.user) // fx

如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。

还有一点就是虽然null也是对象,但是在这里this还是指向那个函数的实例,因为null比较特殊。

function fn () {
    this.user = 'fx'
    return null
}
var a = new fn
console.log(a.user) // fx

题1

var x = 10
var obj = {
    x: 20,
    f: function () {
        console.log(this.x) // 20
        function foo () {
            console.log(this.x)
        }
        foo()  // 10 默认绑定,这里this绑定的是window
    }
}
obj.f()

题2

function foo (arg) {
    this.a = arg
    return this
}
var a = foo(1)
var b = foo(10)
console.log(a.a) // undefined 
console.log(b.a) // 10

foo(1)执行,应该不难看出是默认绑定吧 , this指向了window,函数里等价于 window.a = 1,return window; var a = foo(1) 等价于 window.a = window , 很多人都忽略了var a 就是window.a?,将刚刚赋值的 1 替换掉了。 所以这里的 a 的值是 window , a.a 也是window , 即window.a = window ; window.a.a = window; foo(10) 和第一次一样,都是默认绑定,这个时候,将window.a 赋值成 10?,注意这里是关键,原来window.a = window ,现在被赋值成了10,变成了值类型,所以现在 a.a = undefined。(验证这一点只需要将var b = foo(10);删掉,这里的 a.a 还是window) var b = foo(10); 等价于 window.b = window; 本题中所有变量的值,a = window.a = 10 , a.a = undefined , b = window , b.a = window.a = 10; 题3

var x = 10
var obj = {
    x: 20,
    f: function () {
        console.log(this.x)
    }
}
var bar = obj.f
var obj2 = {
    x: 30,
    f: obj.f
}
obj.f() // 20
bar() // 10
obj2.f() // 30

题4

function foo () {
    getName = function () {
        console.log(1)
    }
    return this
}
foo.getName = function () {
    console.log(2)
}
foo.prototype.getName = function () {
    console.log(3)
}
var getName = function () {
    console.log(4)
}
function getName () {
    console.log(5)
}
foo.getName()                // ?
getName()                    // ?
foo().getName()              // ?
getName()                    // ?
new foo.getName()            // ?
new foo().getName()          // ?
new new foo().getName()      // ?
答案:2 4 1 1 2 3 3 

详解

function foo () {
    getName = function () { // 这里的getName将创建到全局window上
        console.log(1)
    }
    return this
}
foo.getName = function () { // 这个getName和上面的不同,是直接添加到foo上的
    console.log(2)
}
foo.prototype.getName = function () { // 这个getName直接添加到foo的原型上,在用new创建新对象时将直接添加到新对象上
    console.log(3)
}
var getName = function () { // 和foo函数里的getName一样, 将创建到全局window上
    console.log(4)
}
// 同上,但是这个函数不会被使用,因为函数声明的提升优先级最高,所以上面的函数表达式将永远替换这个同名函数,除非在函数表达式赋值前去调用getName(),但是在本题中,函数调用都在函数表达式之后,所以这个函数可以忽略了
function getName () {
    console.log(5)
}

通过上面对 getName的分析基本上答案已经出来了

foo.getName (); // 2

getName (); // 4 这里涉及到函数提升的问题,不知道的小伙伴只需要知道 5 会被 4 覆盖,虽然 5 在 4 的下面,其实 js 并不是完全的自上而下

foo().getName (); // 1 这里的foo函数执行完成了两件事, 1. 将window.getName设置为1,返回window , 故等价于 window.getName(); 输出 1

getName (); // 1刚刚上面的函数刚把window.getName设置为1,故同上 输出 1

new foo.getName (); // 2 new 对一个函数进行构造调用 , 即 foo.getName ,构造调用也是调用啊 该执行还是执行,然后返回一个新对象,输出 2 (虽然这里没有接收新 创建的对象但是我们可以猜到,是一个函数名为 foo.getName 的对象 且__proto__属性里有一个getName函数,是上面设置的 3 函数)

new foo().getName ();// 3 这里特别的地方就来了,new 是对一个函数进行构造调用,它直接找到了离它 最近的函数,foo(),并返回了应该新对象,等价于 var obj = new foo();obj.getName(); 这样就很清晰了,输出的是之前绑定到prototype上的 那个getName 3 ,因为使用new后会将函数的prototype继承给 新对象

new new foo().getName (); // 3 让我们来分解一下: var obj = new foo(); var obj1 = new obj.getName(); 好了,仔细看看, 这不就是上两题的合体吗,obj 有getName 3, 即输出3 obj 是一个函数名为 foo的对象,obj1是一个函数名为obj.getName的对象

变量提升/函数提升

var

前言:直觉上会认为 JavaScript 代码在执行时是由上到下一行一行执行的。但实际上这并不完全正确,有一种特殊情况会导致这个假设是错误的。

考虑以下代码:

a = 2;
var a;
console.log(a); // 输出2

考虑另外一段代码:

console.log(a);
var a = 2;

你可能会认为这个代码片段也会有同样的行为而输出 2。还有人可能会认为,由于变量 a 在使用前没有先进行声明, 因此会抛出 ReferenceError 异常。不幸的是两种猜测都是不对的。输出来的会是 undefined

什么是变量/函数提升

  • 包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理,这种现象称为提升。
  • 但只有声明本身会被提升,而赋值或其他运行逻辑会留在原地
  • javascript并不是严格的自上而下执行的语言

变量声明提升:

  • JavaScript的变量提升是针对var的,而letconst不存在变量提升这一特性
  • 通过var定义的变量,在定义语句之前就可以访问到 值:undefined
  • 变量提升就是变量会被提升到作用域的最顶上去,也就是该变量不管是在作用域的哪个地方声明的,都会提升到作用域的最顶上去。

变量提升详解

  • 当你看到 var a = 2; 时,可能会认为这是一个声明。但 JavaScript 实际上会将其看成两个声明:var a; 和 a = 2;。
  • JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined

第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。 第一个代码片段会以如下形式进行处理:

var a;
a = 2;
console.log(a); 

其中第一部分是编译,而第二部分是执行。

类似地,我们的第二个代码片段实际是按照以下流程处理的:

var a;
console.log(a); 
a = 2; 

打个比方,这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动” 到了最上面。这个过程就叫作提升。

JavaScript只会将变量声明提升,但是不会把初始化提升

但如果变量一直都没有声明过,则会抛出ReferenceError,比如直接输出:

console.log(b) // Uncaught ReferenceError: b is not defined

另外值得注意的是,每个作用域都会进行提升操作

var a = 100

function fn () {
    console.log(a)
    var a = 200
    console.log(a)
}

fn()
console.log(a)
var a
console.log(a)
var a = 300
console.log(a)

这段代码将会依次输出 undefined 200 100 100 300

fn()函数中由于声明了var a = 200, 所以 var a会被提升到fn的作用域顶端,第一输出则为undefined

下面这段代码,由于es6之前,js是没有块级作用域的,所以 if 中声明的a变量会被当成全局变量处理

var a = 1
if (true) {
    var a = 2
}
console.log(a) // 2

比较下面两段代码

var a = 10                        
function fx () {
    console.log(a) // undefined
    var a = 20
    console.log(a) // 20
}
fx()
console.log(a) // 10

====================================

var a = 10
function fx () {
    console.log(a) // 10
    a = 20
    console.log(a) // 20
}
fx()
console.log(a) // 20

第二段代码 fx() 中的a没有使用var定义,会造成fx函数中没有变量声明,所以 fx 里面访问的变量a,其实都是访问的全局变量aa = 20 又相当于给全局变量a重新赋值20

函数声明提升

通过function声明的函数,在之前就可以直接调用

fx() // fx is a great girl 之前之后都可调用
function fx () {
    console.log('fx is a great girl')
}
fx() // fx is a great girl 之前之后都可调用

但如果是这种写法:函数表达式声明的函数

console.log(fx) // undefined
var fx = function () {
    console.log('fx is a great girl')
}

==========================

fx() // 不是 ReferenceError, 而是 TypeErr
var fx = function () {
    console.log('fx is a great girl')
}

这段程序中的变量标识符 fx() 被提升并分配给所在作用域(在这里是全局作用域),因此 fx() 不会导致 ReferenceError。但是 fx 此时并没有赋值(如果它是一个函数声明而不是函数表达式,那么就会赋值)。fx() 由于对 undefined 值进行函数调用而导致非法操作, 因此抛出 TypeError 异常。

当前函数声明和变量声明使用同一个变量名称时,函数的优先级高于变量的优先级

    console.log(fx) // 会输出 fx 定义的函数
    function fx () {
        console.log('fx is a great girl')
    }
    var fx = 'fx'
    console.log(fx) // fx
    console.log(fx()) // TypeError: fx is not a function

有多个同名函数声明的时候,是由最后面的函数声明来替代前面的

    fx() // fx is a great girl
    function fx () {
        console.log('fx')
    }

    function fx () {
        console.log('fx is a great girl')
    }

下面列举几道题,理解了之后就很好懂

function fx () {
    console.log(a) // undefined
    if (false) {
        var a = 1
    }
    console.log(a) // undefined
    console.log(b) // Uncaught ReferenceError: b is not defined
}
fx()

即使if语句的条件是false,也一样不影响a变量提升

    function fx () {
        console.log('fx is a great girl')
    }
    var fx
    console.log(typeof fx) // function

===========================

    function fx () {
        console.log('fx is a great girl')
    }
    var fx = 'good girl'
    console.log(typeof fx) // string

===========================

console.log(typeof fx) // function
var fx = 'good girl'
function fx () {
    console.log('fx is a great girl')
}
console.log(typeof fx) // string

===========================

console.log(typeof fx) // function
var fx
function fx () {
    console.log('fx is a great girl')
}
console.log(typeof fx) // function
if(!(fx in window)) {
    var fx = 1
}
console.log(fx) // undefined
var c = 1
function c(c) {
    console.log(c)
}
c(2) // c is not a function 
// 同名变量与函数。函数先提升,然后又定义变量,最后是变量

========

c(2) // 2
var c = 1
function c(c) {
    console.log(c)
}
console.log(c) // 1

========

var c = function(c) {
    console.log(c)
}
c(2) // 2
var c = 1

========

var c = function(c) {
    console.log(c)
}
var c = 1
c(2) // Uncaught TypeError: c is not a function

========

console.log(c) // undefined
c(2) // Uncaught TypeError: c is not a function
var c = function(c) {
    console.log(c)
}
var c = 1

let & const

let

  • let 声明的变量只在自身的块级作用域有效,存在暂时性死区
  • 不能重复声明(会报错提示已经定义)
  • 不会预处理,不存在变量提升

示例

// 只在所在的代码块生效
{
  let a = 10;
  var b = 1;
}
 
a // ReferenceError: a is not defined.
b // 1
 
============
 
for (var i = 0; i < 5; i++) {
    console.log(i) // 0 1 2 3 4
}
console.log(i) // 5 i成了全局变量
 
==============
 
for (let j = 0; j < 5; j++) {
    console.log(j) // 0 1 2 3 4
}
// let定义的j变量只在for循环的块级作用域中生效,不存在变量提升
console.log(j) // Uncaught ReferenceError: j is not defined
var a = []
for (let i = 0; i < 10; i++) {
    a[i] = function () {
        console.log(i)
    }
}
console.log(a[6]()) // 6
console.log(i) // Uncaught ReferenceError: i is not defined
 
===
var a = []
for (var i = 0; i < 10; i++) {
    a[i] = function () {
        console.log(i)
    }
}
a[6]() // 10 
a[1]() // 10
console.log(i) // 10

上面代码中,变量ivar命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。 如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6

  • for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
for (let i = 0; i < 3; i++) {
    let i = 'abc'
    console.log(i)
}

上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。

不存在变量提升

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
 
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

暂时性死区

  • 当你在一个块里面,利用 let 声明一个变量的时候,在块的开始部分到该变量的声明语句之间,我们称之为暂时性死区,你不可以在这个区域内使用该变量,直到遇到其 let 语句为止
  • ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
var tmp = 123;
 
if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}
 
===============
 
var i = 5;
(function () {
  console.log(i) // undefined
  var i = 10
})()
 
===============
 
let j = 55;
(function () {
  console.log(j) // ReferenceError: j is not defined
  let j = 77
})()
 
===============
 
(function hhh() {
  console.log(j) // j的临时性死区 Uncaught ReferenceError: j is not defined
  let j = 77 // 从这个函数的开始部分到这里,都是新的j的临时性死区
})()

// 不报错
var x = x;
 
// 报错
let x = x;
// ReferenceError: x is not defined

上面代码报错,也是因为暂时性死区。使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量x的声明语句还没有执行完成前,就去取x的值,导致报错 x 未定义。

不能在函数内部重新声明参数

function func(arg) {
  let arg;
}
func() // 报错
 
function func(arg) {
  {
    let arg;
  }
}
func() // 不报错

const

  • 声明一个常量,大部分特点和let一样
  • 只在声明所在的块级作用域内有效。
  • 声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
  • 不可重复声明 声明的时候一定要赋值,否则会报错
const b; // Uncaught SyntaxError: Missing initializer in const declaration

对于引用类型而言,可以修改对象的内部成员,但是不可以修改该变量的地址。

const FX = {
	age: 18,
	name: 'fx'
}
FX ==> {age: 18, name: "fx"}
FX.age = 8 {age: 8, name: "fx"}
FX.weight = 90 {age: 8, name: "fx", weight: 90}
 
FX = {} // Uncaught TypeError: Assignment to constant variable.
// 修改了FX的指针,这是不允许的

const声明的对象冻结方法

const foo = Object.freeze({});
 
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;

上面代码中,常量 foo指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。

除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

var constantize = (obj) => {
    Object.freeze(obj)
    Object.keys(obj).forEach((key, i) => {
        if (typeof obj[key] === 'object') {
            constantize(obj[key])
        }
    })
}

顶层对象的属性 ES6 规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1
 
let b = 1;
window.b // undefined

块级作用域

ES6 中新增了块级作用域。块作用域由 { } 包括,if语句和 for语句里面的{ }也属于块作用域。

为什么需要块级作用域 第一种场景:内部变量会覆盖外部变量

var time = new Date()
function fx () {
    console.log(time) // undefined
    if (false) {
        var time = 'hello'
    }
}
fx() 

{
    var a = 1
    console.log(a) // 1
}
console.log(a) // 1
// 通过var定义的变量可以跨块作用域访问到。

第二种场景:用来计数的循环变量泄漏为全局变量

var s = 'hello';
 
for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}
 
console.log(i); // 5

ES6提供let变量实现块级作用域

function fxFn () { // 这是一个块级作用域
    let fx = 'fx is a great girl'
    if (true) { // 这是一个块级作用域
        let fx = 'fx is 18 years old'
    }
    console.log(fx) // fx is a great girl
}
fxFn()
 
// 块级作用域之间相互不影响

块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。

// IIFE 写法
(function () {
  var tmp = ...;
  ...
}());
 
// 块级作用域写法
{
  let tmp = ...;
  ...
}

ES6 的块级作用域必须有大括号

// 第一种写法,报错
if (true) let x = 1;
 
// 第二种写法,不报错
if (true) {
  let x = 1;
}

JavaScript的匿名函数

什么是匿名函数:没有实际名字的函数

匿名函数的作用:

1、通过匿名函数可以实现闭包(必须掌握的知识点)

2、模拟块级作用域,减少全局变量。执行完匿名函数,存储在内存中相对应的变量会被销毁,使用块级作用域,会大大降低命名冲突的问题,不必担心搞乱全局作用域了。

详解匿名函数:

声明一个普通函数:

function fx () {
    console.log('good girl')
}

将函数的名字去掉

function () { // 此时浏览器会报错
    console.log('good girl')
}

正确定义的匿名函数

(function () {
    // 由于没有执行该匿名函数,所以不会执行匿名函数体内的语句。
    console.log('fx')
})

对去掉名字的函数加入括号后就是一个匿名函数了:

小括号的作用:

小括号能把我们的表达式组合分块,并且每一块,也就是每一对小括号,都有一个返回值。这个返回值实际上也就是小括号中表达式的返回值。所以,当我们用一对小括号把匿名函数括起来的时候,实际上小括号返回的就是一个匿名函数的Function对象。因此,小括号对加上匿名函数就如同有名字的函数般被我们取得它的引用位置了。所以如果在这个引用变量后面再加上参数列表,就会实现普通函数的调用形式。 通俗点讲就是,加入小括号后就实现了和具名函数一样的形式。

匿名函数自执行,也称为立即执行函数表达式(IIFE)

方式一

 // 无参数的匿名函数
    (function () {
        console.log('fx')
    })();
// 带参数的匿名函数
(function (a, b, c) {
    console.log('参数一:', a) // 参数一: 这是普通函数传参的地方
    console.log('参数二:', b) // 参数二: 我是参数二
    console.log('参数三:', c) // 参数三: fx
})('这是普通函数传参的地方', '我是参数二', 'fx')

方式二

  // 推荐使用
    (function () {
        console.log('fx')
    }())

方式三

!function (fx) {
    console.log(fx)
}('fx')

方式四

let fx = function (fx) {
    console.log(fx)
}('fx')

IIFE常用用法

IIFE 的另一个非常普遍的进阶用法是把它们当作函数调用并传递参数进去。
var a = 2;
(function IIFE (global) {
    var a = 3
    console.log(a) // 3
    console.log(global.a) // 2
})(window)
console.log(a) // 2

IIFE 还有一种变化的用途是倒置代码的运行顺序,
将需要运行的函数放在第二位,
在 IIFE 执行之后当作参数传递进去
var a = 2;
(function IIFE (def) {
    def(window)
})(function def (global) {
    var a = 3
    console.log(a) // 3
    console.log(global.a) // 2
})

匿名函数应用场景

1.事件
$('#fx').onclick = function () {
    console.log('给按钮添加点击事件')
}

2.对象
var obj = {
    name: 'fx',
    fx: function () {
        return this.name + ' is' + ' good girl'
    }
}
console.log(obj.fx()) // fx is good girl

3.函数表达式
var fx = function () {
    return 'fx is good girl'
}
console.log(fx()) // fx is good girl

4.回调函数
setInterval(function () {
    console.log('fx is good girl')
}, 1000)

5.作为函数的返回值
function fx () {
    // 返回匿名函数
    return function () {
        return 'fx'
    }
}
console.log(fx()()) // fx

匿名函数模仿块级作用域

if (true) {
    var a = 12 // a为全局变量
}
console.log(a) // 12

for (var i = 0; i < 3; i++) {
    // console.log(i)
}
console.log(i) // 3 for没有自己的作用域,所以当循环结束后i就成为全局变量
if () {}for () {} 等没有自己的作用域。
如果有,出了自己的作用域,
声明的变量就会立即被销毁了。
但可以通过匿名函数来模拟块级作用域:

function fn () {
    (function () { // 这里是我们的块级作用域(私有作用域) 
        var fx = 'good girl!' // 此变量在外部并未定义
        console.log(fx) // good girl!
    })()
    console.log(fx) // 报错Uncaught ReferenceError: fx is not defined
}
fn()

习题一

function test(a, b, c, d){
    console.log(a + b + c + d);
}(1, 2, 3, 4);
// 不执行也不报错

==============

function test(){
    console.log(a + b + c + d);
}();
// 报错:Uncaught SyntaxError: Unexpected token )

习题二

function fxFn (){
    var arr = [];
    for(var i = 0; i < 10; i ++){
        arr[i] = function (){
            console.log(i);
        }
    }
    return arr;
}
var fx = fxFn();
for(var j = 0; j < 10; j++){
    fx[j]();
}

详解

fxFn中由于for不是块级作用域,所以var i 变成 fxFn的局部变量,每次新的i都会覆盖原来的,最终i=10。所以会输出10个10

习题三

function fxFn(){
    var arr = [];
    for(var i = 0; i < 10; i ++){
        (function(j){
            arr[i] = function (){
            console.log(j + " ");
        }
        }(i))
    }
    return arr;
}
var fx= fxFn();
for(var j = 0; j < 10; j++){
    fx[j]();
}

详解:

这题使用了立即执行函数,把fxFn中的i当参数传给了,匿名函数的j,所以每次执行j的状态都会更新,所以会输出0 1 2 3 4 5 6 7 8 9

匿名函数的缺点

  1. 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。

  2. 如果没有函数名,当函数需要引用自身时只能使用已经过期的 arguments.callee 引用, 比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑自身。

  3. 匿名函数省略了对于代码可读性 / 可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。

闭包

闭包就是能够读取其他函数内部变量的函数。只有函数内部的子函数才能读取局部变量,在本质上,闭包是函数内部和函数外部连接起来的桥梁。 闭包的定义: 如果在一个内部函数里,对在外部作用域(但不是全局作用域)的变量进行引用,那么内部函数就被认为是闭包

闭包的特点: 可以读取自身函数外部的变量(沿着作用域链寻找)先从自身开始查找,如果自身没有才会继续往上级查找,自身如果拥有将直接调用。(哪个离的最近就用哪一个) 延长内部变量的生命周期 函数b嵌套在函数a内部 函数a返回函数b

检测对象类型

typeof

  • typeof 操作符返回一个字符串
  • typeof 可能的返回值
类型结果
Undefinedundefined
Nullobject
Booleanboolean
Numbernumber
BigIntbigint
Stringstring
Symbolsymbol
Functionfunction
其他任何对象object
typeof Infinity === 'number';
typeof NaN === 'number'; // 尽管它是 "Not-A-Number" (非数值) 的缩写
typeof Number(1) === 'number'; // Number 会尝试把参数解析成数值
typeof 42n === 'bigint';
typeof String(1) === 'string'; // String 将任意值转换为字符串,比 toString 更安全
typeof '' === 'string';
typeof Boolean(1) === 'boolean'; // Boolean() 会基于参数是真值还是虚值进行转换
typeof !!(1) === 'boolean'; // 两次调用 ! (逻辑非) 操作符相当于 Boolean()
typeof undefined === 'undefined';
typeof undeclaredVariable === 'undefined'; 
typeof {a: 1} === 'object';
typeof [1, 2, 4] === 'object';
typeof new Date() === 'object';
typeof /regex/ === 'object';
typeof new Boolean(true) === 'object';
typeof new Number(1) === 'object';
typeof new String('abc') === 'object'
typeof function() {} === 'function';
typeof class C {} === 'function'
typeof Math.sin === 'function';
typeof console.log // 'function'
typeof null === 'object';
// 除 Function 外的所有构造函数的类型都是 'object'
var str = new String('String');
var num = new Number(100);
typeof str; // 返回 'object'
typeof num; // 返回 'object'
var func = new Function();
typeof func; // 返回 'function'
// 语法中的括号 号有无将决定表达式的类型。
var iData = 99;
typeof iData + ' Wisen'; // 'number Wisen'
typeof (iData + ' Wisen'); // 'string'

es6typeof 总能保证对任何所给的操作数返回一个字符串。即便是没有声明的标识符,typeof 也能返回 'undefined'。 es6加入了块级作用域的 letconst 之后,在其被声明之前对块中的 letconst 变量使用 typeof 会抛出一个 ReferenceError

typeof newConstVariable; // ReferenceError
const newConstVariable = 'hello';

instanceof

  • instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
  • 语法:object instanceof constructor
  • object:某个实例对象 constructor:某个构造函数
  • 用来检测 constructor.prototype 是否存在于参数 object 的原型链上。

举例

// 定义构造函数
function C () {}
function D () {}
var o = new C()
console.log(o instanceof C, o.__proto__ === C.prototype) // true,true C.prototype 在 o 的原型链上
console.log(o instanceof D, o.__proto__ === D.prototype) // false,false D.prototype 不在 o 的原型链上
console.log(o instanceof Object, o.__proto__.__proto__ === Object.prototype) // true true
C.prototype = {}
var o2 = new C()
console.log(o2 instanceof C) // true
console.log(o instanceof C) // false,C.prototype 指向了一个空对象,这个空对象不在 o 的原型链上.
D.prototype = new C() // 继承
var o3 = new D()
console.log(o3 instanceof D) // true
console.log(o3 instanceof C) // true 因为 C.prototype 现在在 o3 的原型链上
一些容易出错的点
var simpleStr = "This is a simple string"; 
var myString  = new String();
var newStr    = new String("String created with constructor");
var myDate    = new Date();
var myObj     = {};
var myNonObj  = Object.create(null);
 
simpleStr instanceof String; // 返回 false, simpleStr并不是对象
myString  instanceof String; // 返回 true
newStr    instanceof String; // 返回 true
myString  instanceof Object; // 返回 true
 
myObj instanceof Object;    // 返回 true, 尽管原型没有定义
({})  instanceof Object;    // 返回 true, 同上
myNonObj instanceof Object; // 返回 false, 一种创建非 Object 实例的对象的方法
 
myString instanceof Date; // 返回 false
 
myDate instanceof Date;     // 返回 true
myDate instanceof Object;   // 返回 true
myDate instanceof String;   // 返回 false

instanceof 能否判断基本数据类型?

// 能。比如下面这种方式:
class PrimitiveNumber {
  static [Symbol.hasInstance](x) {
    return typeof x === 'number'
  }
}
console.log(111 instanceof PrimitiveNumber) // true

其实就是自定义instanceof行为的一种方式,这里将原有的instanceof方法重定义,换成了typeof,因此能够判断基本数据类型。

手动实现一下instanceof的功能

// 核心: 原型链的向上查找。
function myInstanceof (left, right) {
    // 基本数据类型直接返回false
    if (typeof left !== 'object' || left === null) return false
    // getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
    let proto = Object.getPrototypeOf(left)
    while (true) {
        // 查找到尽头,还没找到
        if (proto == null) return false
        // 找到相同的原型对象
        if (proto == right.prototype) return true
        proto = Object.getPrototypeOf(proto)
    }
}

测试:

console.log(myInstanceof("111", String)); // false
console.log(myInstanceof(new String("111"), String)); // true

null 与 undefined 的不同点

null:代表空值,代表一个空对象指针,使用 typeof 运算得到 “object”,所以你可以认为它是一个特殊的对象值。

  • (1) 作为函数的参数,表示该函数的参数不是对象。
  • (2) 作为对象原型链的终点。
  • (3) 当使用完一个比较大的对象时,需要对其进行释放内存时,设置为 null
  • (4) 主动释放一个变量引用的对象,表示一个变量不再指向任何对象地址

undefined: 它是所有未赋值变量默认值。

  • (1)变量被声明了,但没有赋值时,就等于 undefined。
  • (2) 调用函数时,应该提供的参数没有提供,该参数等于 undefined。
  • (3)对象没有赋值的属性,该属性的值为 undefined。
  • (4)函数没有返回值时,默认返回 undefined。
typeof null        // "object" 
typeof undefined   // "undefined"
null === undefined // false
null == undefined // true
null === null // true
null == null // true
!null // true
isNaN(1 + null) // false
isNaN(1 + undefined) // true

循环

普通 for 循环

var arr = ['fx', 18, 'smart', 'good'];
for(var i = 0, len = arr.length; i < len; i++){
    console.log(i + '. ' + arr[i]);
}

forEach

DOM

DOM是什么?

  • DOM(Document Object Model——文档对象模型)是用来呈现以及与任意 HTMLXML 文档交互的APIDOM 是载入到浏览器中的文档模型,以节点树的形式来表现文档,每个节点代表文档的构成部分(例如:页面元素、字符串或注释等等)。在浏览器中主要用于与HTML文档打交道,并且使用DOM API用来访问文档中的数据。最根本对象是documentwindow.document)。
  • 文档对象模型 (DOM) 将 web 页面与到脚本或编程语言连接起来。通常是指 JavaScript,但将 HTMLSVGXML 文档建模为对象并不是 JavaScript 语言的一部分。DOM模型用一个逻辑树来表示一个文档,树的每个分支的终点都是一个节点(node),每个节点都包含着对象(objects)。DOM的方法(methods)让你可以用特定方式操作这个树,用这些方法你可以改变文档的结构、样式或者内容。节点可以关联上事件处理器,一旦某一事件被触发了,那些事件处理器就会被执行。

DOM的作用 DOM 将HTML文档呈现为带有元素、属性和文本的树结构(节点树)。 它允许运行在浏览器中的代码访问文件中的节点并与之交互。节点可以被创建,移动或修改。事件监听器可以被添加到节点上并在给定事件发生时触发。 优点和缺点 DOM的优势主要在于易用性强,使用DOM时,将把所有的XML文档信息都存在于内存中,并且遍历简单,支持XPath,增强了易用性。 DOM的缺点主要表现在效率低,内存占用量过高,对于大文件来说几乎不可能。另外效率低还表现在大量的消耗时间,因为使用DOM进行解析时,将文档的每个Element,Attribute,Processing-instruction和Comment都创建了一个对象,这样在DOM机制中运用了大量对象的创建和销毁无疑影响其效率。 为什么说js操作DOM会影响性能呢? 用我们传统的开发模式,原生JS或JQ操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程。在一次操作中,我需要更新10个DOM节点,浏览器收到第一个DOM请求后并不知道还有9次更新操作,因此会马上执行流程,最终执行10次。例如,第一次计算完,紧接着下一个DOM更新请求,这个节点的坐标值就变了,前一次计算为无用功。计算DOM节点坐标值等都是白白浪费的性能。即使计算机硬件一直在迭代更新,操作DOM的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户体验。

在浏览器中DOM的实现和ECMAScript是分离的。 ES和DOM是两种东西,每次连接都需要消耗性能 操作DOM会导致重排和重绘,重排会占用、消耗CPU; 重绘会占用、消耗GPU

DOM事件流

  • 事件流所描述的就是从页面中接受事件的顺序。
  • DOM事件流(event flow)存在三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。
  • 浏览器在为这个当前页面与用户做交互的过程中,比如我点击了这个鼠标左键,这个左键是怎么传到页面上。还有怎么响应的。事件流分三个阶段,第一阶段是捕获,第二阶段是目标阶段,比如点击的这个按钮,这个按钮就是目标阶段,事件通过捕获到达目标元素,就到达了目标阶段,第三个阶段是冒泡阶段,从目标元素再上传到window对象,就是冒泡的过程。

事件传播——冒泡与捕获

默认情况下,事件使用冒泡事件流,不使用捕获事件流。然而,在FirefoxSafari里,你可以显式的指定使用捕获事件流,方法是在注册事件时传入useCapture参数,将这个参数设为true

冒泡事件流

  • 事件冒泡即事件开始时,由最具体的元素接收(也就是事件发生所在的节点),然后逐级传播到较为不具体的节点。在遵从W3C标准的浏览器里可以通过调用事件对象上的stopPropagation()方法,在Internet Explorer里可以通过设置事件对象的cancelBubble属性为true。如果不停止事件的传播,事件将一直通过DOM冒泡直至到达文档根。
  • DOM事件冒泡的具体流程:第一个接收的是目标元素——第二个接收的是...(子级--父级,按照html结构一层一层往上传)——然后接收的是body标签——html标签——document对象——最后一个接收的是window对象。

捕获事件流

  • 事件的处理将从DOM层次的根开始,而不是从触发事件的目标元素开始,事件被从目标元素的所有祖先元素依次往下传递。在这个过程中,事件会被从文档根到事件目标元素之间各个继承派生的元素所捕获,如果事件监听器在被注册时设置了useCapture属性为true,那么它们可以被分派给这期间的任何元素以对事件做出处理;否则,事件会被接着传递给派生元素路径上的下一元素,直至目标元素。事件到达目标元素后,它会接着通过DOM节点再进行冒泡。
  • DOM事件捕获的具体流程:捕获是从上到下,具体第一个真正接收的是window(对象)——第二个接收的是document(对象)——第三个接收的是html标签(怎么获取html标签document.documentElement)——第四个接收的是bodydocument.body)——......(父级--子级,剩下的就是按照普通的html结构一层一层往下传)——最后到达目标元素。

NodeList

测试代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div class="test-div">
        div1
        <div class="test-div">
            div2
            <div class="test-div">
                div3
            </div>
        </div>
    </div>
</body>
</html>
document.querySelectorAll('div')

  • NodeList 对象是节点的集合
  • NodeList 不是一个数组,是一个类似数组的对象。虽然 NodeList 不是一个数组,但是可以使用 forEach() 来迭代。你还可以使用 Array.from() 将其转换为数组。
  • 在一些情况下,NodeList 是一个实时集合,也就是说,如果文档中的节点树发生变化,NodeList 也会随之变化。

HTMLCollection

document.getElementsByTagName('div')

HTMLCollection继承于一个HTMLCollection对象,而HTMLCollection又直接继承于Object对象

Node

Node是一个接口,各种类型的 DOM API 对象会从这个接口继承。

节点类型

常用节点类型常量

常量描述
Node.ELEMENT_NODE1一个 元素 节点,例如<p> 和 <div>
Node.TEXT_NODE3Element 或者 Attr 中实际的 文字
Node.COMMENT_NODE8一个 Comment 节点。
Node.DOCUMENT_NODE9一个 Document 节点。
document.nodeType === Node.DOCUMENT_NODE; // true

var p = document.createElement("p");
p.textContent = "很久很久以前...";

p.nodeType === Node.ELEMENT_NODE; // true
p.firstChild.nodeType === Node.TEXT_NODE; // true
var node = document.documentElement.firstChild;
if (node.nodeType != Node.COMMENT_NODE)
  console.log("你应该认真编写代码注释!");

常用属性

示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
</head>
<body>
<p id="p1">1</p>
<div id="test">
	<div id="son_one">son_one</div>
	<div style="display: none"><p>son_two</p></div>
	<div style="color: blue!important;">son_three</div>
	<script>
		var test = '这是测试'
	</script>
	<style>
		*{margin: 0;padding: 0}
	</style>
</div>
<script>
	var byId = document.getElementById('test')
	var p1 = document.getElementById('p1')
	var son_one = document.getElementById('son_one')
	console.log('baseURI:', byId.baseURI) // 返回一个表示base URL的DOMString。
	console.log('childNodes:', byId.childNodes) // 返回一个包含了该节点所有子节点的实时的NodeList。NodeList 是动态变化的。如果该节点的子节点发生了变化,NodeList对象就会自动更新。

	console.log('firstChild:', byId.firstChild) // 返回该节点的第一个子节点Node,如果该节点没有子节点则返回null。
	console.log('firstElementChild:', byId.firstElementChild) // 返回该节点的第一个子节点Node,如果该节点没有子节点则返回null。

	console.log('lastChild:', byId.lastChild) // 返回该节点的最后一个子节点Node,如果该节点没有子节点则返回null。
	console.log('lastElementChild:', byId.lastElementChild) // 返回该节点的最后一个子节点Node,如果该节点没有子节点则返回null。

	console.log('nextSibling:', byId.nextSibling) // 返回与该节点同级的下一个节点 Node,如果没有返回null。
	console.log('nextElementSibling:', byId.nextElementSibling) // 返回与该节点同级的下一个节点 Node,如果没有返回null。

	console.log('previousSibling:', byId.previousSibling) // 返回一个当前节点同辈的前一个节点( Node) ,或者返回null(如果不存在这样的一个节点的话)。
	console.log('previousElementSibling:', byId.previousElementSibling) // 返回一个当前节点同辈的前一个节点( Node) ,或者返回null(如果不存在这样的一个节点的话)。

	console.log('parentNode', byId.parentNode) // 返回一个当前节点 Node的父节点 。如果没有这样的节点,比如说像这个节点是树结构的顶端或者没有插入一棵树中, 这个属性返回null。
	console.log('parentElement', byId.parentElement) // 返回一个当前节点的父节点 Element 。 如果当前节点没有父节点或者说父节点不是一个元素(Element), 这个属性返回null。

	console.log('son_one.parentNode', son_one.parentNode) // 返回一个当前节点 Node的父节点 。如果没有这样的节点,比如说像这个节点是树结构的顶端或者没有插入一棵树中, 这个属性返回null。
	console.log('son_one.parentElement', son_one.parentElement) // 返回一个当前节点的父节点 Element 。 如果当前节点没有父节点或者说父节点不是一个元素(Element), 这个属性返回null。

	console.log('nodeName:', byId.nodeName) // 返回一个包含该节点名字的DOMString。
	console.log('nodeType:', byId.nodeType) // 返回一个与该节点类型对应的无符号短整型的值
	console.log('nodeValue:', byId.nodeValue) // 对于文档节点来说, nodeValue返回null. 对于text, comment, 和 CDATA 节点来说, nodeValue返回该节点的文本内容. 对于 attribute 节点来说, 返回该属性的属性值.

	console.log('p1.nodeName:', p1.nodeName) // 返回一个包含该节点名字的DOMString。
	console.log('p1.nodeType:', p1.nodeType) // 返回一个与该节点类型对应的无符号短整型的值
	console.log('p1.nodeValue:', p1.nodeValue) // 对于文档节点来说, nodeValue返回null. 对于text, comment, 和 CDATA 节点来说, nodeValue返回该节点的文本内容. 对于 attribute 节点来说, 返回该属性的属性值.

	console.log('textContent:', byId.textContent)
	console.log('innerText:', byId.innerText)
	console.log('innerHTML:', byId.innerHTML)
	//  textContent 属性表示一个节点及其后代的文本内容
	byId.textContent = '1221' // 在节点上设置 textContent 属性的话,会删除它的所有子节点,并替换为一个具有给定值的文本节点。
	console.log(byId.textContent)
	*/
</script>
</body>
</html>

运行结果如下:

注意点:

  • childNodes、firstChild、lastChild、nextSibling、previousSibling会获取到textNode,这是因为在DOM中实际上有一个叫做textNode的元素,相应的还有document.createTextNodeJS方法,而在IEChrome浏览器中会将源代码中的换行符渲染成一个textNode,只是视觉上不可见。然而,通过childNodes来获取子元素的时候,结果会包含这些textNode

Node.textContent: 表示一个节点及其后代的文本内容。

与 innerText 的区别

  • textContent 会获取所有元素的内容,包括 <script><style> 元素,然而 innerText 只展示给人看的元素。
  • textContent 会返回节点中的每一个元素。相反,innerTextCSS 样式的影响,并且不会返回隐藏元素的文本,此外,由于 innerTextCSS 样式的影响,它会触发回流( reflow )去确保是最新的计算样式。(回流在计算上可能会非常昂贵,因此应尽可能避免。)
  • textContent 不同的是, 在 Internet Explorer (小于和等于 11 的版本) 中对 innerText 进行修改, 不仅会移除当前元素的子节点,而且还会永久性地破坏所有后代文本节点。在之后不可能再次将节点再次插入到任何其他元素或同一元素中。

与 innerHTML 的区别

  • 正如其名称,Element.innerHTML 返回 HTML。通常,为了在元素中检索或写入文本,人们使用 innerHTML。但是,textContent 通常具有更好的性能,因为文本不会被解析为HTML

常用方法

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
</head>
<body>
<p id="p1">1</p>
<div id="test">
	<div id="son_one">son_one</div>
	<div style="display: block" id="son_two"><p id="son_two_p">son_two</p></div>
	<div style="color: blue!important;">son_three</div>
	<div style="display: none" id="son_four"><p id="son_four_p">son_four</p></div>
	<script>
		var test = '这是测试'
	</script>
	<style>
		*{margin: 0;padding: 0}
	</style>
</div>
<script>
	var byId = document.getElementById('test').normalize()
	var p1 = document.getElementById('p1')
	var son_one = document.getElementById('son_one')
	var son_two_p = document.getElementById('son_two_p')
	var son_two = son_two_p.parentNode

	/*克隆一个元素节点会拷贝它所有的属性以及属性值,当然也就包括了属性上绑定的事件(比如onclick="alert(1)"),但不会拷贝那些使用addEventListener()方法或者node.onclick = fn这种用JavaScript动态绑定的事件.
	在使用Node.appendChild()或其他类似的方法将拷贝的节点添加到文档中之前,那个拷贝节点并不属于当前文档树的一部分,也就是说,它没有父节点.
	如果deep参数设为false,则不克隆它的任何子节点.该节点所包含的所有文本也不会被克隆,因为文本本身也是一个或多个的Text节点.*/
	var clonePone = p1.cloneNode(true)
	console.log(clonePone)
	p1.appendChild(clonePone) // 将指定的 childNode 参数作为最后一个子节点添加到当前节点。

	// 返回一个 Boolean 布尔值,来表示传入的节点是否为该节点的后代节点。
	console.log('document.body.contains(clonePone):', document.body.contains(clonePone))
	console.log('clonePone.contains(p1):', clonePone.contains(p1))
	console.log('p1.contains(clonePone):', p1.contains(clonePone))

	console.log('p1.hasChildNodes():', p1.hasChildNodes()) // 返回一个Boolean 布尔值,来表示该元素是否包含有子节点。
	console.log('clonePone.hasChildNodes():', clonePone.hasChildNodes())

	console.log(clonePone.isEqualNode(clonePone)) // 返回一个 Boolean 类型值。当两个 node 节点为相同类型的节点且定义的数据点匹配时(即属性和属性值相同,节点值相同)返回 true,否则返回 false。

	// son_two.removeChild(son_two_p) // 移除当前节点的一个子节点。这个子节点必须存在于当前节点中。

	// replaceChild() 方法用指定的节点替换当前节点的一个子节点,并返回被替换掉的节点。
	var sp1 = document.createElement("p");
	sp1.setAttribute("id", "newSpan") // 添加一个id属性,值为'newSpan'
	var sp1_content = document.createTextNode("新的span元素的内容.") // 创建一个文本节点
	sp1.appendChild(sp1_content) // 将文本节点插入到span元素中
	son_two.replaceChild(sp1, son_two_p) // 用新的span元素sp1来替换掉sp2

	// insertBefore 在参考节点之前插入一个拥有指定父节点的子节点。如果给定的子节点是对文档中现有节点的引用,
	// insertBefore() 会将其从当前位置移动到新位置(在将节点附加到其他节点之前,不需要从其父节点删除该节点)。
	// var insertedNode = parentNode.insertBefore(newNode, referenceNode);
	// insertedNode 被插入节点(newNode) parentNode 新插入节点的父节点 newNode 用于插入的节点 referenceNode newNode 将要插在这个节点之前
	// 如果 referenceNode 为 null 则 newNode 将被插入到子节点的末尾。
	// referenceNode 引用节点不是可选参数——你必须显式传入一个 Node 或者 null。如果不提供节点或者传入无效值,在不同的浏览器中会有不同的表现。
	var newChild = document.createElement('span')
	son_two.insertBefore(newChild, son_two_p)
</script>
</body>
</html>

移除某个节点的所有子节点

function removeAllChildren(element){
  while(element.firstChild){
    element.removeChild(element.firstChild);
  }
}
removeAllChildren(document.body);

遍历所有子节点(包括这个根节点自身)

function eachNode(rootNode, callback){
	if(!callback){
		var nodes = [];
		eachNode(rootNode, function(node){
			nodes.push(node);
		});
		return nodes;
	}

	if(false === callback(rootNode))
		return false;

	if(rootNode.hasChildNodes()){
		var nodes = rootNode.childNodes;
		for(var i = 0, l = nodes.length; i < l; ++i)
			if(false === eachNode(nodes[i], callback))
				return;
	}
}

使用示例

<div id="box">
	<span>Foo</span>
	<span>Bar</span>
	<span>Baz</span>
</div>
var box = document.getElementById("box");
eachNode(box, function(node){
	if(null != node.textContent){
		console.log(node.textContent);
	}
});
// 用户终端上会显示如下字符:
"\n\t", "Foo", "\n\t", "Bar", "\n\t", "Baz"
// 空格是构成 Text节点的一部分,意味着缩进和换行在 Element 节点之间形成单独的 Text 。

Document

Document 接口表示任何在浏览器中载入的网页,并作为网页内容的入口,也就是DOM 树。DOM 树包含了像 <body><table> 这样的元素,以及大量其他元素。它向网页文档本身提供了全局操作功能,能解决如何获取页面的 URL ,如何在文档中创建一个新的元素这样的问题。

常用属性

示例代码

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Title</title>
	<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
	<script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/jquery/1.10.0/jquery.min.js"></script>
</head>
<body>
<div>
	<div name="oneName" class="cOne" id="testOne">testOne</div>
	<div name="twoName" class="cTwo" id="testTwo">testTwo</div>
	<div name="oneName" class="cOne">cOne</div>
	<div name="twoName" class="cTwo">cTwo</div>
	<div name="threeName" class="cThree">cThree</div>
	<p>测试</p>
</div>
<p>测试1</p>
<p>测试2</p>
<script>
	console.log(document.all) // 返回一个以文档节点为根节点的 HTMLAllCollection 集合。换句话说,它能返回页面的完整内容。
	console.log('body:', document.body) // 返回当前文档中的<body>元素或者<frameset>元素.
	console.log('head:', document.head) // 返回当前文档中的 <head> 元素。如果有多个 <head> 元素,则返回第一个。
	console.log('title:', document.title) // 获取或设置文档的标题。
	console.log('characterSet:', document.characterSet) // 文档正在使用的字符集
	console.log('contentType:', document.contentType) // 根据当前文档的 MIME Header,返回它的 Content-Type。
	console.log('documentURI:', document.documentURI) // 以字符串的类型,返回当前文档的路径。
	console.log('images:', document.images) // 返回当前文档中所包含的图片的列表。 HTMLCollection
	console.log('links:', document.links) // 返回一个包含文档中所有超链接的列表。 HTMLCollection
	console.log('scripts:', document.scripts) // 返回文档中所有的 <script> 元素。 HTMLCollection
	console.log('styleSheets:', document.styleSheets) // 返回文档上可用样式表的列表。 StyleSheetList
	console.log('cookie:', document.cookie) // 返回一个使用分号分隔的 cookie 列表,或设置(写入)一个 cookie。
	console.log('domain:', document.domain) // 获取或设置当前文档的域名。
	console.log('location:', document.location) // 返回当前文档的 URI。
	console.log('document.children:', document.children) // 返回 一个Node的子elements ,是一个动态更新的 HTMLCollection
	console.log('document.body.children:', document.body.children) // 返回 一个Node的子elements ,是一个动态更新的 HTMLCollection
	console.log(document.body.childElementCount) // 表示给定元素的子元素数。
	console.log('document.firstChild:', document.firstChild)
	console.log('document.firstElementChild:', document.firstElementChild)
	console.log('document.lastChild:', document.lastChild)
	console.log('document.lastElementChild:', document.lastElementChild)
</script>

常用方法

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Title</title>
	<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
	<script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/jquery/1.10.0/jquery.min.js"></script>
</head>
<body>
<div>
	<div name="oneName" class="cOne" id="testOne">testOne</div>
	<div name="twoName" class="cTwo" id="testTwo">testTwo</div>
	<div name="oneName" class="cOne">cOne</div>
	<div name="twoName" class="cTwo">cTwo</div>
	<div name="threeName" class="cThree">cThree</div>
	<p>测试</p>
</div>
<p>测试1</p>
<p>测试2</p>
<script>
	// Document.createAttribute()方法创建并返回一个新的属性节点。这个对象创建一个实现了 Attr 接口的节点。这个方式下DOM不限制节点能够添加的属性种类。
	var node = document.getElementById("testOne");
	var a = document.createAttribute("my_attrib");
	// a.value = "newVal";
	a.nodeValue = "newValTwo";
	node.setAttributeNode(a);
	console.log(node.getAttribute("my_attrib")); // "newVal"
	// Document.createTextNode() 创建一个新的文本节点。这个方法可以用来转义 HTML 字符。
	var newTextNode = document.createTextNode('郁闷');
	var newElement = document.createElement('p')
	newElement.appendChild(newTextNode)
	document.body.appendChild(newElement)

	// 返回一个包含了所有指定类名的子元素的类数组对象。HTMLCollection
	var byClassName = document.getElementsByClassName('cOne')
	// 返回一个包括所有给定标签名称的元素的HTML集合HTMLCollection
	var byTagName = document.getElementsByTagName('div')
	// 返回一个匹配到 ID 的 DOM Element 对象
	var byId = document.getElementById('testOne')
	// 返回文档中与指定选择器或选择器组匹配的第一个 HTMLElement对象。 如果找不到匹配项,则返回null
	var querySelector = document.querySelector('div')
	// 返回与指定的选择器组匹配的文档中的元素列表 (使用深度优先的先序遍历文档的节点)。返回的对象是 NodeList 。
	var querySelectorAll = document.querySelectorAll('div')
	// 返回一个live的 NodeList   集合,这个集合包含 name 属性为指定值的所有元素,例如<meta> 、<object>,
	// 甚至那些不支持 name 属性但是添加了 name 自定义属性的元素也包含其中。
	// getElementsByName  在不同的浏览器其中工作方式不同。在IE和Opera中, getElementsByName()  方法还会返回那些 id 为指定值的元素。所以你要小心使用该方法,最好不要为元素的 name 和 id 赋予相同的值。
	// IE 和 Edge 都返回一个 HTMLCollection, 而不是NodeList 。
	var getElementsByName = document.getElementsByName('twoName')
</script>
</body>
</html>