ES5进阶小结

199 阅读31分钟

ES5进阶.png

一、基础深入

1.变量与常量

1.变量理解:

  • 作用: 用来存放数据,保存的数据可以改变

  • 本质: 变量本身也是数据,也需要在内存中占用空间,保存在内存的栈结构分区中

2.常量理解:

  • 用来保存数据,保存的数据不可以被修改

  • 本质:常量本身也是数据,也需要在内存中占用空间,保存在内存的栈结构分区中

//变量定义
var a = 123;
a = 234;

//常量定义一般大写
var PERSON = 'kobe';

2. (重点!) 基本数据类型 和 引用数据类型的 特点 与 判断

js数据类型分为: 基本数据类型 和 复合数据类型(引用数据类型 / 对象)

1.基本数据类型:

  • 定义: string, number, boolean, null, undefined

  • 特征: 基本数据类型数据赋值给某一个变量之后值本身就不会再发生改变, 再进行赋值就是新开辟的空间存放新数值。

2.引用数据类型:

  • 定义: object, array, function

  • 特征:引用数据类型的值可以通过其赋值的变量修改

3.判断数据类型: typeof

  • 问题: typeof返回值有几种???

  • 答案: string, number, boolean, undefined, function, object

  • 特殊: null和array的返回值也为Object

4.面试题: 如何用原生js判断Array/Function/Object

  • 答案: 使用instanceOf
// 基本数据类型特点: 一旦定义了值是不会发生改变的, 再进行赋值就是新开辟的空间存放新数值。
// 不能说a保存的数据修改为了234!!!
var a = 123;
a = 234;


var b = {name:'kobe'};
b.name = 'wade';       // 操作对象的属性不会导致变量的指针指向发生改变
b = {name: 'duncan'};  // 操作对象本身,会导致指针指向发生改变


// 在堆内存产生了两块内存
var c = {};
var d = {};


// typeof 检测数据类型, 返回值: string, number, boolean, undefined, function, object
console.log(typeof null);  // object
console.log(typeof [1,2,3]);  // object


// 如何用原生js判断 Array/Function/Object 
// instanceof 用来判断A是否是B的实例   语法: A instanceof B,返回值是布尔值
var obj = {};
console.log([1,2,3] instanceof Array); // true
console.log(obj instanceof Array); // false

二、函数基础回顾

1.函数

1.理解:

  • 函数也是对象
  • 函数是特殊的对象,因为函数具备行为,通过调用可以执行内部语句

2.定义函数方式:

  • 函数声明式: function fun(){}
  • 函数表达式: var fun2 = function(){}

3.调用函数方式:

  • test()
  • new test()
  • obj.test()
  • test.call/apply(obj)

4.为什么要设计函数(函数的作用,什么时候使用)

  • 代码结构分明
  • 简化代码的编写,减少重复代码
  • 功能点清晰,封装函数思想: 一个函数通常只有一个指定的功能
  • 复用,提高代码性能
  • 隔离变量,减少命名污染
//普通对象
var obj = {};
obj.name = 'wade';

//隔离变量
var a = 123;
//函数具备行为
function fun() {
    var a = 'abc'
    console.log(a);
}

//函数声明式
function fun2(){
    var a = 123;
}
//函数表达式
var fun3 = function(){}

/*
  * 为什么要设计函数
  *   1. 简化代码的编写,减少重复代码
  *   2. 功能点明确,复用
  *   3. 隔离变量,减少命名污染
  * */
fun();

//修改函数this
var obj2 = {
    name: 'kobe'
}
function test(num, num2) {
    console.log(this, num, num2); // window
}
// call|| apply 用于修改this指向的(强制绑定this), 自动调用修改完this指向的函数
// test.call(指定的this指向对象,参数);
test.call(obj2, 123, 234);
// test.apply(指定的this指向对象,[参数]);
test.apply(obj2, [123, 234]);

2.回调函数

1.什么函数才是回调函数?

  • 你定义的
  • 你没有直接调用
  • 但最终它执行了(在特定条件或时刻)

2.常见的回调函数?

  • DOM事件函数
  • 定时器函数
  • ajax回调函数
  • 生命周期回调函数
document.getElementById('btn').onclick = function () {
    console.log('btn onclick()');
}

setTimeout(function () {
    console.log('setTimeout');
}, 2000)

3.IIFE(立即执行函数)

1.理解

  • 全称: Immediately-Invoked Function Expression 立即调用函数表达式
  • 别名: 匿名函数自调用/立即执行函数

2.作用

  • 隐藏内部实现
  • 不污染外部命名空间

3.特点

  • 立即执行
  • 只会执行一次
//匿名函数自调用
(function () {
    var a = 123;
    console.log('立即执行函数调用');
})()
或
(function () {
    var a = 123;
    console.log('立即执行函数调用');
}())

//普通函数
function fun() {
    console.log('fun()');
}

4.函数中的this

1.理解this:

  • 关键字
  • 变量

2.this的指向问题

  • 函数this不是函数定义的时候决定的
  • 函数this指向谁看如何调用当前的函数

3.this指向分类

  • 函数自调用: window
  • 构造函数(new function): 当前构造函数的实例对象
  • 对象.方法(): 对象本身
  • fun.call/apply(指定的对象): 指定的对象
console.log(this); // window
console.log(window);

// 函数this的指向问题
function fun() {
    console.log(this);
}
fun(); // window

new fun(); // 当前构造函数的实例对象
var obj = {
    fun: fun
}
obj.fun(); // obj

5.new关键字的作用(new干了哪些事情)( 重点!)

1)创建一个空对象(开辟内存空间)

2)将this指向该空对象(this指向该内存空间)

3)执行函数

4)创建实例对象并返回(返回函数执行的结果)

三、函数进阶

3-1、原型

1.原型对象(重点!)

1.每个函数都有一个prototype属性,该属性指向的显示原型对象

2.每一个实例对象身上都有一个__ proto __ 属性,该属性指向的是当前实例的隐式原型对象

3.构造函数的显示原型和当前构造函数的实例的隐式原型对象是同一个对象

4.原型对象本身就是一个普通的Object实例对象,并且初始化的时候是空的对象,后期添加的constructor属性,该属性指向当前的构造函数本身。

5.实例身上的 __ proto __ 为什么叫隐式原型?

当初设计的时候规定不允许操作 __ proto __ ,所以叫隐式原型

注: 程序员能直接操作显式原型, 但不能直接操作隐式原型(ES6之前)

6.作用:

  • 虽然构造函数的方法很好用,但是存在浪费内存的问题。

  • 构造函数通过原型对象分配的函数是所有实例对象所共享的。

  • 一般情况下,我们的公共属性定义到构造函数里面, 公共的方法我们放到原型对象身上!!!

//举例
function Person(name, age) {
    this.name = name;
    this.age = age;
    // this.showName = function () {
    //   console.log(this.name);
    // }
}

console.log(Person.prototype);
Person.prototype.showName = function () {
    console.log(this.name);
};

var person1 = new Person('kobe', 43);
var person2 = new Person('kobe2', 44);
var person3 = new Person('kobe3', 45);
console.log(person1.name);
person1.showName();
person2.showName();
person3.showName();

2.原型链(重点!)

1.查找对象的属性的时候先在自身找,如果自身没有沿着 __ proto __ 找原型对象

2.如果原型对象上还没有,继续沿着 __ proto __ ,直到找到Object的原型对象

3.如果还没有找到返回undefined

4.原型链: 沿着 __ proto __ 查找的这条链就是原型链

//例1:
console.log(Object.prototype);
function Fn() {
    this.test1 = function () {
        console.log('test1()')
    }
}
Fn.prototype.test2 = function () {
    console.log('test2()')
};

var fn = new Fn();
fn.test1();
fn.test2();
console.log(Object.prototype);
console.log(fn.toString())
console.log(fn.test3)  //undefined
fn.test3()  // test3 is not a function
// 即undefined();

注: 对象.属性不会报错!

//例2:
function Foo() {}
var f1 = new Foo();
var f2 = new Foo();
var o1 = new Object();
var o2 = {};
console.log(Foo.toString());

图解原型链1 - 构造函数/原型/实体对象的关系

原型链图解1.jpg

图解原型链2 - 终极原型链

终极原型链.jpg

手画终极原型链

手画终极原型链.jpg

1)所有函数都是new Function()出来的,包括自己(仅仅理论上为了支持原型链)。

2)所有对象都是new Object()出来的。

注:

  • Function = new Function()

  • Object = new Function()

3)这个2个关系式只是为了支持原型链出现的,但是并非真实存在的。

4)实际上 Function 和 Object 是 js引擎内置的。

3.原型链-属性问题

1.读取对象的属性值时: 会自动到原型链中查找

2.设置对象的属性值时: 不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值

3.方法一般定义在原型中, 属性一般通过构造函数定义在对象本身上

4.instanceof原理

1.instanceof是如何判断的?

  • 表达式: A instanceof B
  • 如果B函数的显式原型对象在A对象的原型链上, 返回true, 否则返回false

2.Function是通过new自己产生的实例

//面试题
console.log(Object instanceof Function);   // true
console.log(Object instanceof Object);     // true
console.log(Function instanceof Object);   // true
console.log(Function instanceof Function); // true
function Foo() {}
console.log(Object instanceof Foo); // false

5.面试题

//面试题1
var A = function() {
};
A.prototype.n = 1;
//已经实例化的对象不会受prototype改变的影响,因为__proto__指向没变
var b = new A(); // {n: 1}
A.prototype = {
    n: 2,
    m: 3
};
var c = new A();
console.log(b.n, b.m, c.n, c.m);  // 1 undefined 2 3


//面试题2
var F = function(){};  // 函数对象
Object.prototype.a = function(){
    console.log('a()')
};
Function.prototype.b = function(){
    console.log('b()')
};
var f = new F(); // 构造函数
// f就是一个普通的实例对象
f.a()  //a()   // f的隐式原型对象 == F的显示原型对象 === {constructor}
f.b()  //报错
F.a()  //a()   // 函数对象, 当把函数当做对象看待的时候,这个时候其构造函数应该是Function
F.b()  //b()

3-2、执行上下文

1.变量提升 & 函数提升

1.js引擎在js代码正式执行之前会做一些预解析的工作

2.找关键字: var, function

3.找到var以后将var后边的变量提前声明,但是不赋值,就是var a;

4.找到function以后定义对应的函数,也就是说函数在预解析的时候已经定义完毕

5.预解析: 全局预解析,局部预解析

6.注:

  • 全局预解析在定义函数的时候不关心函数是否被使用
  • 函数局部预解析的时候如果内部函数没有被使用就不会提前定义

注: 断点调试发现预解析。

console.log(a);
console.log(b);
var a = 123;
fun();
function fun() {
    var a = 234;
    console.log('fun()');
}
fun2();
var fun2 = function(){}

//面试题
var a = 4
function fn () {
    console.log(a)  // undefined
    var a = 5
}
fn()

2.调试练习(浏览器控制台)

console.log('程序开始执行');
console.log('程序继续执行');
console.log(a);
console.log(b);
var a = 123;
fun();
function fun() {
    var a = 234;
    console.log('fun()');
}
fun2();
var fun2 = function(){}
console.log('程序执行完毕');

3.执行上下文(底层知识)

1.理解:

  • 执行上下文抽象的概念,代表了代码执行的环境,包含: 执行环境,变量对象,this,作用域链
  1. 流程:
  • js引擎在js代码正式执行之前会先创建一个执行环境(开发商批的地,工程队施工的环境)
  • 进入该环境以后创建一个变量对象(打地基),该对象用于收集当前环境下的: 变量,函数,函数的参数,this
  • 找关键字var ,function
  • 确认this的指向
  • 创建作用域链
  1. 重点:
  • 执行上下文是动态创建的
  • 尤其是针对函数,每调用一次函数都会创建一次执行上下文

4.执行上下文分类

  • 全局执行上下文
  • 函数执行上下文

注: 断点调试发现执行上下文。

console.log(a1)
console.log(a2)
console.log(a3)
// console.log(a4)
console.log(this) // window
var a1 = 3
var a2 = function () {
    console.log('a2()')
}
function a3() {
    console.log('a3()')
}
a4 = 4
a3();

4.执行上下文栈

用于保存执行上下文(变量对象)

1.在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文

2.在全局执行上下文(window)确定后, 将其添加到栈中(压栈)

3.在函数执行上下文创建后, 将其添加到栈中(压栈)

4.在当前函数执行完后,将栈顶的对象移除(出栈)

5.当所有的代码执行完后, 栈中只剩下window

var a = 10;                  // 全局执行上下文
var bar = function (x) {
    var b = 5;
    foo(x + b);              // 函数执行上下文
}
var foo = function (y) {
    var c = 5;
    console.log(a + c + y);
}
bar(10);                     // 函数执行上下文

//面试题
console.log('global begin: '+ i);    // 全局执行上下文
var i = 1;
foo(1);                            // 函数执行上下文
function foo(i) {
    if (i == 4) {
        return;
    }
    console.log('foo() begin:' + i);
    foo(i + 1);                      // 函数执行上下文
    console.log('foo() end:' + i);
}
console.log('global end: ' + i);

1)依次输出什么? 
undefined 1 2 3 3 2 1 1
2)整个过程中产生了几个执行上下文? 
5

5.面试题

//测试题1: 先预处理函数, 后预处理变量, 如果已经存在就会被忽略
console.log(typeof a);  // function
var a;
function a() {}
//1.预解析时,函数优先级更好(通常说法)
//2.a开始undefined,后来里面放函数


//测试题2: 变量预处理, in操作符
if (!(b in window)) {
    var b = 1; //此处有预解析,没有块级作用域
}
console.log(b);  // undefined


//测试题3: 预处理, 顺序执行
//预解析结果 c = function(){}
var c = 1;
function c(c) {
    console.log(c);
    var c = 3;
}
c(2);  //报错, c is not function, c为1

3-3、作用域与作用域链(重点!)

1.作用域

1.抽象的概念

2.用来决定代码执行的范围,变量所属的范围

3.它是静态的(相对于执行上下文对象),作用域是代码定义的时候决定的

4.作用域作用:

  • 隔离变量
  • 规定其之后的作用域链是什么样的,体现: [[scopes]]: 上一级作用域链

5.作用域分类

  • 全局作用域
  • 函数作用域
  • eval作用域

6.作用域与执行上下文的区别和联系

区别1:

  • 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了,而不是在函数调用时。
  • 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
  • 函数执行上下文环境是在调用函数时, 函数体代码执行之前创建

区别2:

  • 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
  • 上下文环境是动态的, 调用函数时创建, 函数调用结束时上下文环境就会被释放

7.联系

  • 上下文环境(对象)是从属于所在的作用域
  • 全局上下文环境 ==> 全局作用域
  • 函数上下文环境 ==> 对应的函数作用域
var a = 10,
    b = 20;
function fn(x) { // 隐式变量
    var a = 100,
        c = 300;
    console.log('fn()', a, b, c, x);  // 100, 20, 300, 10
    function bar(x) {
        var a = 1000,
            d = 400;
        console.log('bar()', a, b, c, d, x);  // 1000, 20, 300, 400, 100 || 200
    }
    bar(100);
    bar(200);
}
fn(10);

/*
   问题:
   1. 有几个作用域?   3个
   2. 产生过几个上下文环境对象?   4个
*/

2.作用域链

1.作用域链是一个数组结构

2.该结构内保存的是一个个的变量对象, 即保存着当前作用域的变量对象及其上级作用域的变量对象,直到全局的变量对象

3.作用域链什么时候创建的:在js代码正式执行之前创建的

4.查找变量时就是沿着作用域链来查找的

5.作用域链如何产生

  • 函数在定义的时候自动添加一个属性 ‘[[Scopes]]’, 该属性保存的是其上级作用域链
  • 当函数执行的时候,进入执行上下文环境,将创建的变量对象添加到‘[[Scopes]]’数组的第一个位置,形成新的数组,该数组就是当前函数拥有的作用域链

6.查找一个变量的查找规则

  • 先在当前作用域的变量对象中查找,如果有就使用
  • 如果没有就会沿着作用域链的数组去上级作用域中的变量对象中查找
  • 找到就返回对应的值,如果没有继续向上查找,直到找到最后一个变量对象(全局的变量对象),如果还没有就会报错

注: 浏览器断点调试处显示作用域链与发现‘[[Scopes]]’属性

var a = 2;
function fn1() {
    var b = 3;
    function fn2() {
        var c = 4;
        console.log(c);  //4
        console.log(b);  //3
        console.log(a);  //2
        console.log(d);  //报错
    }
    fn2();
}
fn1();


//详解
// 创建全局作用域 ---> 预解析工作 ---> 创建全局的执行上下文 ---> 执行上下文环境 ---> 全局变量对象{a: undefined, this: window, fun:function} ---> 作用域链[全局变量对象]
var a = 123;
var b = 'abc'
// 先定义---> 创建局部作用域---> 函数自身有一个[[scopes]]: 上一级作用域链(global)
function fun() {
    // 创建局部执行上下文 ---> 局部变量对象{a: undefined, fun2: function, this: window} ---> 创建作用域链[局部变量对象, Global]
    var a = 234;
    var c = 345;
    console.log(a);
    console.log(b);
    // fun2函数已经定义了 ---> 创建局部作用域(fun2)---> 函数自身有一个[[scopes]]: 上一级作用域链[fun的变量对象, Global]
    function fun2() {
        // 创建局部执行上下文 ---> 局部变量对象{this: window} ---> 创建作用域链: [fun2的局部变量对象,fun的变量对象, Global]
        console.log(c);
        console.log(d);
    }
    fun2();
}
fun();

3.面试题

//1
//问题: 结果输出多少?
var x = 10;
function fn() {
    console.log(x);  //10
}
function show(f) {
    var x = 20;
    f();
}
show(fn);

//2
console.log(fn); // var fn;
var fn = function () { // fn = function
    console.log(fn);
}
fn();

//3
var obj = {
    fn2: function () {
        console.log(fn2); // 报错
    }
}
obj.fn2();

3-4、闭包(重难点!)

1.引入

//需求: 点击某个按钮, 提示"点击的是第n个按钮"
<button>测试1</button>
<button>测试2</button>
<button>测试3</button>

var btns = document.getElementsByTagName('button');
//有问题-循环早已结束,始终i=3
for(var i = 0; i < btns.length; i++) {
    var btn = btns[i];
    btn.onclick = function () {
        alert('第' + (i + 1) + '个');
    }
}

//解决一: 保存下标
for(var i = 0; i < btns.length; i++) {
    var btn = btns[i];
    btn.index = i;
    btn.onclick = function () {
        alert('第' + (this.index + 1) + '个');
    }
}

//解决二: 利用闭包
for(var i=0,length=btns.length;i<length;i++) {
    (function (i) {
        var btn = btns[i];
        btn.onclick = function () {
            alert('第'+ i +'个');
        }
    })(i);
}

2.闭包形成的条件

  • 函数嵌套
  • 内部函数引用外部函数的局部变量
  • 内部函数被使用

注: 函数变量提升的时候如果内部函数没有被使用,在预解析的过程中不会定义内部函数

3.闭包的理解

理解一: 闭包就是能够读取其他函数内部变量的函数(绝大部分人)

理解二: 包含被引用变量(函数)的对象(极少数人)

理解三: (深入理解):

  • 闭包是一个存在内部函数的引用关系
  • 该引用指向的是外部函数的局部变量对象(前提是内部函数使用了外部函数的局部变量)
function fun() {
    var a = 123;
    function fun2() {  //函数预解析有性能优化,不调用是不会预解析
        console.log(a);
    }
    //直接调用,或者返回函数
    // fun2();
    return fun2;
}
var fun2 = fun();
fun2();

4.常见的闭包

1.将函数作为另一个函数的返回值 - 手写闭包

2.将函数作为实参传递给另一个函数调用

// 1. 将函数作为另一个函数的返回值 - 手写闭包(必须会)!
function fn1() {
    var a = 2;
    function fn2() {
        a++;
        console.log(a);
    }
    return fn2;
}
var f = fn1();  //由于f引用着内部的函数-->内部函数以及闭包都没有成为垃圾对象
// f.xxx = 外部函数的变量对象 = {a: 2}
f(); // 3   //间接操作了函数内部的局部变量{a: 2}
f(); // 4
f = fn1();
f(); // 3

// 2. 将函数作为实参传递给另一个函数调用
function showMsgDelay(msg, time) {
    setTimeout(function () {
        console.log(msg);
    }, time);
}
showMsgDelay('hello', 1000);

5.闭包的作用

1.延长外部函数变量对象的生命周期

2.使用闭包能够间接的从函数外部访问函数内部的私有变量

注意: 浏览器为了性能,后期将外部函数中不被内部函数使用的变量清除了。

问题:

1)函数执行完后, 函数内部声明的局部变量是否还存在?

存在,因为闭包存在

2)在函数外部能直接访问函数内部的局部变量吗?

不能

6.闭包的生命周期

  1. 产生: 在嵌套内部函数定义执行完时就产生了(不是在调用) - 跟作用域有关
  2. 死亡: 在嵌套的内部函数成为垃圾对象时
function fun1() {
    //此处闭包已经产生
    var a = 3;
    function fun2() {
        a++;
        console.log(a);
    }
    return fun2;
}
var f = fun1();
f();
f();
f = null //此时闭包对象死亡,注意及时清除闭包,让f变为null

7.闭包的优缺点

  • 优点: 延长外部函数变量对象的生命周期

  • 缺点: 延长外部函数变量对象的生命周期(占内存,如果不及时清除容易造成内存溢出,泄漏)

8.使用闭包的注意事项:

  • 及时清除闭包
  • 让内部的函数成为垃圾对象 ---> 内部函数身上没有指针指向

9.闭包的应用 - 模块化 - 自定义JS模块

1.定义JS模块

  • 具有特定功能的js文件
  • 将所有的数据和功能都封装在一个函数内部(私有的)
  • 只向外暴露一个包含n个方法的对象 或 函数
  • 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
//方式1-使用闭包,返回对象
<script type="text/javascript" src="coolModule.js"></script>
var myModuleObj = myModule();
console.log(myModuleObj);
// 函数内部的数据(变量,方法)是私有的
myModuleObj.doSomething();
console.log(myModuleObj.doOtherthing());

//coolModule.js文件
function myModule() { // Module模块
    var msg = 'mymodule1 msg';
    var num = 123;
    function doSomething() {
        console.log(msg);
    }
    function doOtherthing() {
        return msg;
    }
    return {doSomething: doSomething, doOtherthing: doOtherthing};
}


//方式2-使用匿名函数自调用,并在window对象上挂载对象来暴露方法
<script type="text/javascript" src="coolModule2.js"></script>
console.log(window);
myModuleObj2.doSomething();
console.log(myModuleObj2.doOtherthing());

//coolModule2.js文件
(function myModule(w) {// Module模块
    var msg = 'mymodule1 msg';
    var num = 123;
    function doSomething() {
        console.log(msg);
    }
    function doOtherthing() {
        return msg;
    }
    w.myModuleObj2 = {
        doSomething: doSomething,
        doOtherthing: doOtherthing
    };
})(window);

10.面试题

//1-1
var name = "The Window";
var object = {
    name: "My Object",
    getNameFunc: function () {
        return function () {
            return this.name;
        };
    }
};
//函数自调用this永远指向window
console.log(object.getNameFunc()());  //The Window

//1-2
var name2 = "The Window";
var object2 = {
    name2: "My Object",
    getNameFunc: function () {
        var that = this;  // 缓存this
        return function () {
            return that.name2;
        };
    }
};
console.log(object2.getNameFunc()()); //My Object


//2.大厂最坑闭包题目-滴滴
function fun(n, o) {
  console.log(o);
  return {
    fun: function (m) {  // 满足产生闭包的条件, 产生了一个闭包引用 ---> 外部函数fun的变量对象 === {n: 0, o:undefined }
      return fun(m, n);  // m = 1, n = 0
    }
  }
}

var a = fun(0);  // a = {fun: function(){}}
a.fun(1);  // m = 1 n = 0;
a.fun(2);
a.fun(3);
//输出:undefined,0,0,0

var c = fun(0).fun(1);  // {n: 1, o: 0}
c.fun(2);  // 1
c.fun(3);
//输出:undefined,0,1,1

var b = fun(0).fun(1).fun(2).fun(3).fun(30).fun(55);
//输出:undefined,0,1,2,3,30

//总结:看内部函数的引用的变量对象(闭包)


//3-综合题
function Foo() {
    getName = function () { alert (1); };
    return this;
}
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
var getName = function () { alert (4);};
function getName() { alert (5);}

//请写出以下输出结果
Foo.getName();  // 2
getName();  // 4
Foo().getName();  // 1
getName();  // 1
new Foo.getName();  // 2  new先找最近的()变成可执行语句
new Foo().getName();  // 3  new Foo()先执行,再.getName()
new new Foo().getName();  // 3  先执行new Foo()再.getName,最后结合new()


//扩展
console.log(1, console.log(2, console.log(3)));
//输出:3 2 undefined 1 undefined

//注:
console.log(123);  //函数返回结果为undefined
alert(123);  //函数返回结果为undefined
console.log(console.log());  //undefined
console.log(alert(123));  //undefined

四、词法作用域(了解!)

1.词法作用域 与 动态作用域

1.作用域分类:

1)静态作用域(词法作用域): JavaScript

2)动态作用域: bash

2.特征对比:

1)词法作用域规定作用域在代码定义的时候就决定了,而不是看调用的时候

2)动态作用域是在代码执行的时候决定的

2.词法作用域面试题

// 考点: 词法作用域规则 / 闭包
// 重点: 词法作用域中的作用域在代码定义的时候决定而不是调用的时候决定
// 面试题1
var scope1 = "global scope";
function checkScope1() {
    var scope1 = "local scope";
    function fn() {
        return scope1;
    }
    return fn();
}
console.log(checkScope1());  //"local scope"


// 面试题2
var scope2 = "global scope";
function checkScope2() {
    var scope2 = "local scope";
    function fn() {
        return scope2;
    }
    return fn;
}
console.log(checkScope2()());  //"local scope"


//扩展
var a = 10;
function fun() {   // [[scopes]]: [Global]
    console.log(a);  //10
}
function fun2() {  // [[scopes]]: [Global]
    var a = 20;
    fun();
}
fun2();

五、对象基础深入

1.对象基础回顾

1.什么是对象?

  • 代表现实中的某个事物, 是该事物在编程中的抽象

  • 多个数据的集合体(封装体)

  • 用于保存多个数据的容器

2.为什么要用对象?

  • 便于对多个数据进行统一管理

3.对象的组成

  • 属性

    • 代表现实事物的状态数据

    • 由属性名和属性值组成

    • 属性名都是字符串类型, 属性值是任意类型

  • 方法

    • 代表现实事物的行为数据

    • 是特别的属性==>属性值是函数

4.如何访问对象内部数据?

  • .属性名: 编码简单, 但有时不能用

  • ['属性名']: 编码麻烦, 但通用

5.难点: 对象最终保存的属性名一定是字符串,如果设置的时候不是字符串,会调用toString()方法将其转换成字符串

// 创建对象
var p = {
    name: 'Tom',
    age: 12,
    setName: function (name) {
        this.name = name
    },
    setAge: function (age) {
        this.age = age
    }
};
// 访问对象内部数据
console.log(p.name, p['age']);
p.setName('Jack');
p['age'](23);
console.log(p['name'], p.age);

2.基础深入

1-问题: 什么时候必须使用['属性名']的方式?

  • 属性名不是合法的标识名

  • 属性名不确定

var obj = {};
obj.name = 'kobe';
obj['content-type'] = 'application';
console.log(obj['content-type']);

var msg = 'age';
obj[msg] = 42;
console.log(obj);

2-创建的对象key为变量的情况

// 创建对象
var age = 'age';
var p = {
    name: 'Tom',
    [age]: 12,
    setName: function (name) {
        this.name = name
    },
    setAge: function (age) {
        this.age = age
    }
};

3.对象面试题

1.对象中所有的key都是字符串

2.对象.toString() = '[object Object]'

var a = {}
var obj1 = {n: 3};  // obj1.toString() = '[object Object]'
var obj2 = {h: 6};  // obj2.toString() = '[object Object]'
a[obj1] = 4;
console.log(a);  // {[object Object]: 4}
a[obj2] = 5;
console.log(a);
console.log(a[obj1]); //5
console.log(a['[object Object]']) // 5

4.in运算符与hasOwnProperty()的区别

  • in 运算符:检测属性是否存在于某个对象中,自有属性和继承属性都返回true
  • hasOwnProperty() 方法用于检测属性是否是自有属性,是则返回true,否则返回false
function Init() {}
Init.prototype.name = 'xyz'; //原型对象
var init = new Init();
init.age = 18;

console.log("name" in init); // true
console.log("age" in init); // true
console.log(init.hasOwnProperty("name")); //false
console.log(init.hasOwnProperty("age")); //true

5.for...in注意事项与替代方案

  • for in 枚举对象的时候除了能够枚举自身的属性之外还会枚举原型的属性
  • 替代方案:
    • 使用Object.keys() + forEach()
    • 使用Object.getOwnPropertyNames(obj) + forEach()
var obj = {a: 0, b: 1, c: 3};
for(var i in obj) {
     console.log(i, ":", obj[i]);
}


var obj = {a: 0, b: 1, c: 3};
Object.keys(obj).forEach(function(key){
     console.log(key, obj[key]);
});


var obj = {a: 0, b: 1, c: 3};
Object.getOwnPropertyNames(obj).forEach(function(key){
    console.log(key, obj[key]);
});

六、对象进阶

6-1、对象创建

1.Object构造函数

方式一: Object构造函数

  • 套路: 先创建空Object对象, 再动态添加属性/方法

  • 适用场景: 起始时不确定对象内部数据

  • 问题: 语句太多

var obj = new Object();
obj.name = 'kobe';
obj.age = 43;

2.对象字面量

方式二: 对象字面量

  • 套路: 使用{}创建对象, 同时指定属性/方法

  • 适用场景: 起始时对象内部数据是确定的

  • 问题: 如果创建多个对象, 有重复代码

var obj = {name: 'kobe', age: 40};
var obj2 = {name: 'wade', age: 42};
var obj2 = {name: 'wade1', age: 12};
var obj2 = {name: 'wade2', age: 22};
var obj2 = {name: 'wade3', age: 32};

3.工厂模式

方式三: 工厂模式

  • 套路: 通过工厂函数动态创建对象并返回

  • 适用场景: 需要创建多个对象

  • 问题: 对象没有一个具体的类型, 都是Object类型

// 工厂函数: 返回一个需要的数据的函数
function Person(name, age) {
    return {
        name: name,
        age: age
    }
}
var person1 = Person('kobe', 43);
var person2 = Person('wade', 38);
person2.sex = '男';
console.log(person1 instanceof Person); // false

4.自定义构造函数

方式四: 自定义构造函数模式

  • 套路: 自定义构造函数, 通过new创建对象

  • 适用场景: 需要创建多个类型确定的对象

  • 问题: 每个对象都有相同的数据, 浪费内存

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.showName = function () {
        console.log(this.name);
    };
}
var person1 = new Person('kobe', 43);
var person2 = new Person('wade', 38);
console.log(person1 instanceof Person); // true
person1.showName();
person2.showName();

5.构造函数 + 原型的组合

方式五: 构造函数 + 原型的组合模式

  • 套路: 自定义构造函数, 属性在函数中初始化, 方法添加到原型上

  • 适用场景: 需要创建多个类型确定的对象

function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.showName = function () {
    console.log(this.name);
};
var person1 = new Person('kobe', 43);
var person2 = new Person('wade', 38);
console.log(person1 instanceof Person); // true
person1.showName();
person2.showName();

6-2、继承(重难点!)

1.原型继承

1.核心思想

  • 子类的原型 成为 父类的实例

  • Child.prototype = new Parent();

2.注意问题

  • 以上的写法会导致子类的构造器属性丢失

3.解决问题

  • Child.prototype.constructor = Child

4.原型链继承存在的问题:

  • 问题1:原型中包含的引用类型属性将被所有实例共享;

  • 问题2:子类在实例化的时候不能给父类构造函数传参;

// 定义一个Person类
function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.showName = function () {
    console.log(this.name);
};
var person1 = new Person('kobe', 43);

// 定义一个Child类
function Child(name, age) {
    this.name = name;
    this.age = age;
}

// 原型继承关键:子类的原型 成为 父类的实例
// 默认原型实现代码:Child.prototype = {constructor: Child}
Child.prototype = new Person();
//注意: 构造器属性丢了,手动将constructor指回Child
Child.prototype.constructor = Child;

var child1 = new Child('xiaoming', 18);
console.log(child1);
child1.showName();
// child1 ---> Child.prototype --> Person.prototype  --> Object.prototype

2.构造函数继承 (不是真的继承)

1.核心思想

  • 让父类的构造函数在子类构造函数中执行

2.注意问题

  • 如果父类的构造函数在子类构造函数中直接调用,会导致在window对象身上添加了不必要的属性

3.解决问题

  • 使用call || apply

  • Parent.call(子类的实例对象, 参数)

  • Parent.call(this, 参数)

4.构造函数继承存在的问题:

  • 但是由于方法必须定义在构造函数中,所以会导致每次创建子类实例都会创建一遍方法。
function Person(name, age) {
    this.name = name;
    this.age = age;
}
var person1 = new Person('kobe', 43);

function Child(name, age, sex) {
    // 重复代码
    // this.name = name;
    // this.age = age;
    
    // 借用Person中的代码
    // Person(name, age); // this ---> window
    // 思考: this应该是 Child的实例对象
    // 难点: Child的实例对象 = this;
    //构造函数继承关键:让父类的构造函数在子类构造函数中执行,并将this指向子类的实例
    Person.call(this, name, age);
    this.sex = sex;
}

var child2 = new Child('xiaoming', 18, '男');

3.原型 + 构造函数的组合继承

1.核心思想

  • 原型继承 + 构造函数继承

    1. 利用原型继承实现对父类型对象的方法继承

    2. 利用父类构造函数来初始化相同属性

2.组合继承存在的问题:

  • 调用了 2 次父类构造函数,第一次是在 new Animal(),第二次是在 Animal.call()
function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.showName = function () {
    console.log(this.name);
};

function Child(name, age, sex) {
    // 重复代码
    // this.name = name;
    // this.age = age;
    
    // 借用Person中的代码
    // Person(name, age); // this ---> window
    // 思考: this应该是 Child的实例对象
    // 难点: Child的实例对象 = this;
    Person.call(this, name, age);
    this.sex = sex;
}

// 子类的原型 成为 父类的实例
Child.prototype = new Person();
// 构造器属性丢了,手动将constructor指回Child
Child.prototype.constructor = Child;

var person1 = new Person('kobe', 43);
var child2 = new Child('xiaoming', 18, '男');

4.优化版 - 原型 + 构造函数的组合继承

1.优化调用2次父类构造函数的问题

2.解决 - 使用 Object.create() 方法创建父类实例

  • Child.prototype = Object.create(Person.prototype)
function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.showName = function () {
    console.log(this.name);
};

function Child(name, age, sex) {
    // 重复代码
    // this.name = name;
    // this.age = age;
    
    // 借用Person中的代码
    // Person(name, age); // this ---> window
    // 思考: this应该是 Child的实例对象
    // 难点: Child的实例对象 = this;
    Person.call(this, name, age);
    this.sex = sex;
}

// 子类的原型 成为 父类的实例
// 使用 Object.create()方法创建父类实例
Child.prototype =  Object.create(Person.prototype);
// 构造器属性丢了,手动将constructor指回Child
Child.prototype.constructor = Child;

var person1 = new Person('kobe', 43);
var child2 = new Child('xiaoming', 18, '男');

七、事件循环机制

1.进程与线程(了解!)

1.进程: 程序的一次执行, 是系统进行资源分配的基本单位, 它占有一片独有的内存空间

2.线程: CPU的基本调度单位, 是程序执行的一个完整流程

3.进程与线程

  • 一个进程中一般至少有一个运行的线程: 主线程
  • 一个进程中也可以同时运行多个线程, 我们会说程序是多线程运行的
  • 一个进程内的数据可以供其中的多个线程直接共享
  • 多个进程之间的数据是不能直接共享的

4.浏览器运行是单进程还是多进程?

  • 有的是单进程
    • firefox老版
    • 老版IE
  • 有的是多进程
    • chrome
    • 新版IE

5.如何查看浏览器是否是多进程运行的呢?

  • 任务管理器 ==> 进程

6.浏览器运行是单线程还是多线程?

  • 都是多线程运行的

2.JS是单线程的

JS是单线程的, 也就是说,同一个时间只能做一件事。

1.为什么js要用单线程模式, 而不用多线程模式?

  • JavaScript的单线程,与它的用途有关。
  • 作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。
  • 这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

2.js代码分两种: 同步任务 || 异步任务

  • 同步任务: 1. 阻塞的 2. 同步是没有回调的
  • 异步任务: 1. 非阻塞 2. 异步有回调(用来通知当前异步任务执行的结果)

3.如何证明js执行是单线程的?

  • 使用setTimeout和alert
console.log('程序开始执行');
setTimeout(function () {
    console.log('回调');
}, 2000);
//证明js是单线程的,可以使用alert
alert('---');
console.log('-----------');
console.log('程序执行结束');

3.JS事件循环(轮询)机制(重点!)

事件轮询机制:

1.js任务都会在js的主线程执行

2.当开启一个异步任务的时候会交给对应的管理模块去管理

3.主线程继续执行后续的任务

4.管理模块接管的对应的回调,它会在恰当的时机将对应的回调放入callback queue中

5.当主线程上的所有同步任务执行完毕会通过‘轮询’的方式询问callback queue是否有可执行的回调

6.假如没有会反复询问

7.假如有可执行的回调将对应的回调钩到主线程执行

注: 回调函数也叫钩子函数!

function fn1() {
    console.log('fn1()')
}
fn1();

document.getElementById('btn').onclick = function () {
    console.log('处理点击事件')
};

setTimeout(function () {
    console.log('到点了')
}, 2000);

function fn2() {
    console.log('fn2()')
}
fn2()

执行结果: 
fn1()
fn2()
onclick与定时器谁先触发放入回调队列,就先执行谁。

图解:

事件循环机制图.png

4.定时器问题

1.定时器真的是定时执行的吗?

  • 不一定,一般情况是,特例: 定时器任务的后边有运算量大的代码段时,会延迟很久(即定时器不准时)。因为JS是单线程的。

注:

  • 不要在定时器任务之后放置运算量大的代码段。
  • 定时器设置后就开始计时了。(可用alert验证!)
console.log('程序开始执行');
var time = Date.now();
setTimeout(function () {
    console.log('定时器花费的时间: ', Date.now() - time);
}, 2000);
//用alert验证定时器设置后就开始计时了
alert('---');
//验证定时器任务的后边有运算量大的代码段时,会延迟很久(即定时器不准时)。
for (var i = 0; i < 1000000; i++) {
    console.log(i);
}

5.Web Workers(了解!)

1.H5规范提供了js分线程的实现, 取名为: Web Workers

2.相关API

  • Worker: 构造函数, 加载分线程执行的js文件
  • Worker.prototype.onmessage: 用于接收另一个线程的回调函数
  • Worker.prototype.postMessage: 向另一个线程发送消息

3.不足

  • worker内代码不能操作DOM(更新UI)
  • 不能跨域加载JS
  • 不是每个浏览器都支持这个新特性
//  js主线程
var worker = new Worker('./work.js');
// 监听消息: 接收消息
worker.onmessage = function (msg) {// 当接收到分线程发送过来的消息自动调用
    console.log('分线程发送过来的数据: ', msg.data);
};
// 发送消息
worker.postMessage('Nba');

//work.js文件
// 模拟js分线程
// 分线程直接写onmessage即可,不用new Worker()来实例化
onmessage = function (msg) {
    console.log('主线程发送过来的数据:', msg.data);
    var result = msg.data.toLowerCase();
    // 将结果发送主线程
    postMessage(result);
};

6.Web Workers应用-使用分线程计算斐波那契 (fibonacci) 数列

//  js主线程
var worker = new Worker('./work2.js');
// 监听消息: 接收消息
worker.onmessage = function (msg) {// 当接收到分线程发送过来的消息自动调用
    console.log('斐波那契数列计算结果: ', msg.data);
};
// 发送消息
worker.postMessage(8);


//work2.js文件
// 模拟js分线程
// 分线程直接写onmessage即可,不用new Worker()来实例化
onmessage = function (msg) {
    console.log('要计算的斐波那契数列:', msg.data);

    function fibonacci(n) {
        return n >= 3 ? fibonacci(n - 1) + fibonacci(n - 2) : 1;
    }
    var result = fibonacci(msg.data);
    // 将结果发送主线程
    postMessage(result);
};

八、Object扩展

1.Object.create()与hasOwnProperty()方法

1.Object.create(prototype, [descriptors])

  • 作用: 以指定对象为原型创建新的对象,返回该对象

  • 为新的对象指定新的属性, 并对属性进行描述

    • value : 指定值
    • writable : 标识当前属性值是否是可修改的,默认为false
    • configurable: 标识当前属性是否可以被删除,默认为false
    • enumerable: 标识当前属性是否能用for in 枚举,默认为false
    • get :用来获取当前属性值的回调函数
    • set :修改当前属性值的触发的回调函数,并且实参即为修改后的值
  • 存取器属性:setter,getter一个用来监视值,一个用来取值

注: value / writable 与 get / set 不能混用,只能一组一起用。

2.Object.prototype.hasOwnProperty()

  • 返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。
// Object.create(prototype, [descriptors])
var obj = {
    name: 'kobe',
    showName: function () {
        console.log(this.name);
    }
};
var obj2 = Object.create(obj, {
    sex: {
        value: '男', // 修饰符
        writable: true, // 可以被修改的
        configurable: true,
        enumerable: true
    },
    age: {
        value: 43,
        enumerable: true
    }
});
console.log(obj2.sex);
obj2.sex = '女';
console.log(obj2);
console.log(obj2.sex);

// for in 枚举对象的时候除了能够枚举自身的属性之外还会枚举原型的属性
for(var i in obj2){
    if(obj2.hasOwnProperty(i)){ // 验证是否是自身的属性
        console.log(i);
    }
}

delete obj2.sex;
console.log(obj2);

2.Object.defineProperties() / Object.defineProperty()(重难点!)

1.Object.defineProperties(object, descriptors)

  • 作用: 为指定对象定义扩展多个属性,返回该对象
  • 指定新的属性, 并对属性进行描述
    • value: 指定值
    • writable: 标识当前属性值是否是可修改的,默认为false
    • configurable: 标识当前属性是否可以被删除,默认为false
    • enumerable: 标识当前属性是否能用for in 枚举,默认为false
    • get: 用来获取当前属性值得回调函数
    • set: 修改当前属性值得触发的回调函数,并且实参即为修改后的值
  • 存取器属性:setter,getter一个用来监视值,一个用来取值

注: value / writable 与 get / set不能混用,只能一组一起用。

案例: 实现简单数据双向绑定

// Object.defineProperties(object, descriptors)
var obj = {
    name: 'kobe',
    age: 42
};

var sex = '男';
Object.defineProperties(obj, { // 配置对象
    sex: {
        //value: '男', // 修饰符
        //writable: true,
        
        get: function () {  // 获取, get方法的作用: 提供扩展属性的值
          console.log('get()');
          console.log(this);
          //return '男';
          return sex;
        },
        // set方法用来监视扩展属性的,
        // 如果扩展属性一旦被修改set就调用,并且会自动将修改之后的属性值自动作为参数传入set函数内部
        set: function (msg) {
          console.log(this);
          console.log('set()', msg);
        // set是监视当前扩展属性的,所以不能在set方法中直接修改扩展属性值,否则会出现死循环
        // this.sex = msg;
        // 借助第三方变量修改get中return的sex,即修改了sex属性值
          sex = msg;
        }
    }
});

console.log(obj.sex); // 要获取扩展属性的值的时候就会自动调用get方法
console.log(obj.sex); // 要获取扩展属性的值的时候就会自动调用get方法
console.log(obj.sex); // 要获取扩展属性的值的时候就会自动调用get方法
obj.sex = '女';
// console.log(obj);
console.log(obj.sex);



//2个需求案例
var obj2 = {
    name: 'kobe',
    age: 42
};
var obj3 = {};
// 枚举obj2对象
for(var item in obj2){
    if(obj2.hasOwnProperty(item)){
        // 需求1: obj2克隆给obj3
        // console.log(item, ': ', obj2[item]);
        // obj3[item] = obj2[item];

        //需求2: 实现简单数据双向绑定-使用闭包
        (function (item) {
            Object.defineProperties(obj3, {
                // 注意:不能直接写item变量
                [item]: { // 配置对象
                    get: function () {
                        return obj2[item];
                    },
                    set: function (msg) {
                        console.log('set()', msg);
                        obj2[item] = msg;
                    }
                }
            })
        })(item)
    }
}

console.log(obj3.name);
console.log(obj3.age);
obj2.name = 'wade';
console.log(obj3.name);

obj3.name = 'duncan';
console.log(obj3.name);
console.log(obj2.name);

2.Object.defineProperty(obj,prop,descriptor)

  • 作用: 直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
  • 指定新的属性, 并对属性进行描述
    • value: 指定值
    • writable: 标识当前属性值是否是可修改的,默认为false
    • configurable: 标识当前属性是否可以被删除,默认为false
    • enumerable: 标识当前属性是否能用for in 枚举,默认为false
    • get: 用来获取当前属性值得回调函数
    • set: 修改当前属性值得触发的回调函数,并且实参即为修改后的值
  • 存取器属性:setter,getter一个用来监视值,一个用来取值

注: value / writable 与 get / set不能混用,只能一组一起用。

var obj5 = {};
var sex = '男';
Object.defineProperty(obj5, 'sex', {
    get: function () {
        //return '男';
        return sex;
    },
    set: function (msg) {
        console.log(msg);
        sex = msg;
    },
    // value:'男',
    // writable: true
});
console.log(obj5.sex);
obj5.sex = '女';
console.log(obj5.sex);