JavaScript 高级

Posted by 松一老贼 on January 17, 2018

keywords:变量提升,作用域,this,面向对象

作用域

当浏览器加载页面时,会形成一个全局作用域。

作用域:代码运行的环境,栈内存。

全局变量:运行在全局作用域中定义的变量。

(全局/私有作用域)不写 var 定义变量,会变成 window 的属性:

a = 100; 相当于 window.a = 100;

console.log(a); // a is not defined 原因:浏览器认为 a 是一个值,必须知道 a 的数据类型

私有作用域:函数执行时,首先会形成私有作用域,供函数体中的代码执行,栈内存。函数每执行一次,会形成一个新的私有作用域。

局部/私有变量:在私有作用域定义的变量,一般是函数体中的变量和函数的形参。

函数体中可以访问外层作用域中的变量,但是外层作用域不能访问函数体中的变量。

标记查找从内往外寻找,如果找到停止查找,如果在最外层作用域都没有找到,报错。

私有作用域作用:

  • 闭包:函数执行会形成一个私有作用域,保护函数体内的私有变量不受外部干扰。
  • 存储值

全局变量与私有变量的案例:

var a,
  b,
  fn = function() {
    var a = (b = 10); // 相当于  var a = 10;b=10
  };
fn();
console.log(a, b); // undefined  10
// 以上代码相当于
var a; //默认undefined
var b; //默认undefined
var fn = function() {
  var a = 10; // 重新定义了一个名为a的私有变量
  b = 10; // 给全局变量 b 赋值 10
};
fn();
console.log(a, b);

变量提升

在当前作用域下,代码执行之前,把带 var 的和 function 的变量或者函数,放到当前作用域执行栈最前边。

var 的变量只声明不定义,只有当代码执行到赋值语句时才会定义(赋值)。

function 进行变量提升时,不仅进行声明,还要定义(赋值,因为在这里已经赋值, 在函数执行时会跳过代码中的函数)。所以函数的定义发生在变量提升阶段,而不是函数执行阶段。

函数表达式 var fn = function(){} 变量提升时看做变量而不是函数 function

函数的执行流程:

  1. 形成一个私有作用域
  2. 形参赋值
  3. 变量提升
  4. 代码从上到下执行
  5. 作用域是否销毁
function sum(num) {
  console.log(num);
  function num() {}
  console.log(num);
}
//1.形成私有作用域
//2.形参赋值:num=11
//3.变量提升:num = function sum(){}
//4.运行:

特殊情况:

  1. 不管条件是否成立,都要进行变量提升。

    console.log(a); // undefined
    if (false) {
      var a = 10; //在全居中会提升
    }
    

    在新版本浏览器中,if 块中函数只声明不定义,在 IE10 及以下,if 块中函数不仅声明而且定义。

    console.log(a); // undefined
    console.log(fn); // 在标准浏览器中输出 undefined,IE10及以下输出函数本身
    if (false) {
      var a = 100;
      function fn() {}
    }
    // 一道关于变量提升的面试题
    f = function() {
      return true;
    };
    g = function() {
      return false;
    };
    ~(function() {
      if (g() && [] == ![]) {
        // 标准浏览器输出 g is not a function;IE10及其以下浏览器继续执行
        f = function() {
          return false;
        };
    
        function g() {
          return true;
        }
      }
    })();
    console.log(f()); // IE10及其以下浏览器 false
    console.log(g()); // IE10及其以下浏览器 false
    
  2. 变量提升只发生在等号 = 左边。

  3. return 返回的内容不进行提升,但是 return 语句之后的变量或者函数会提升。

    function sum() {
      // 提升的内容 a c b
      var a = 10;
      console.log(b); // 整个函数 b
      console.log(c); //undefined
      return function() {
        console.log("hello");
      };
      console.log(b); // 此处代码不执行
      var c = 199;
    
      function b() {
        var d = 10;
      }
    }
    
    var f = sum();
    f(); // hello
    

    return function(){};// 这里 return 出去的是function(){}的堆内存地址。

    return 返回的永远是一个值,如果 return 的是对象,返回的是对象的地址,若是基本数据类型的变量,则是原始值。

  4. 如果变量名重复,不再进行提升,会重新赋值

    console.log(a); // undefind
    var a = 100;
    console.log(a); // 100
    var a = function(){};//函数表达式 
    

    如果函数和var变量重复,只提升函数。

    console.log(a);//函数
    function a (){}
    var a = 100;
    

    如果重复的时两个函数,提升并定义最后的一个

    console.log(foo)//function foo(a){console.log(a)}
    function foo(){}
    function foo(a){
      console.log(a)
    }
    
  5. 匿名函数不进行变量提升,自执行函数定义和执行一起完成


es6 中定义变量,通过 let 不进行变量提升。不容许重复声明。

const 定义常量,不容许修改值。

堆栈内存

var ary = [1, 2, 3, 4];
function f(ary) {
  ary[0] = 0;
  arguments[0] = []; // 相当于ary=[],形参换了地址
  ary[0] = 100;
  console.log(ary); //[100]
}
f(ary);
console.log(ary); //[0,2,3,4]

栈:存储基本数据类型 提供运行环境(作用域)

全局作用域销毁:关掉页面或者浏览器

私有作用域的销毁:一般情况下,函数执行完成后立即销毁,另外:

  • 不销毁作用域:
  1. 函数执行后 return 一个引用数据类型
  2. return 出去的内容被外界的变量或对象接收
  • 不立即销毁作用域:
  • 作用域销毁与否不仅与函数内部有关,还有函数执行有关:

堆:存储引用类型

this 关键字

在作用域中有特殊意义:类似与指针,谁调用指向谁

  1. 全局下的 this永远指向 window
  2. 函数中的this要看函数执行前有没有.,. 前边是谁,this 就是谁;如果没有 .this 指向 window
  3. 给元素的事件行为绑定方法,那么方法中的 this 指向当前被绑定的元素
  4. 自执行函数中的 this 永远指向 window
  5. 回调函数中的 this 指向 window
  6. 构造函数(类)中的this指向当前类的实例
  7. apply,call,bind 中的this指向第一个参数

面向对象

面向对象 :类的继承、封装、多态

对象:万物皆对象。

类:一类事物的高度概括。

js 中的类:

  • 内置类:数据类型类,元素类

    • 数据类型类:

      Number String Boolean Null Undefined Object Array RegExp Function Date

    • 元素类

      #div —> HTMLDivElement–>HTMLElement–>Element –>Node–>EventTarget–>Object

  • 自定义类

实例:类的具象化,都是对象类型。

HTMLCollection 元素集合

NodeList 节点集合

创建对象

数组的创建:如果只有一个参数,代表生成数组的 length,大于一个参数,代表目标数组的元素。

var arr1 = new Arrary(3); //[empty,empty,empty]
var arr2 = new Arrary(2, 3); // [2,3]

对象的创建:

var obj = new Object();
obj.a = 12;
console.log(obj); //{a:12}

单例模式

把描述同一件事物的不同属性放进同一个命名空间下,避免了全局变量的干扰和冲突,那么这种模式:单例模式

var obj = {
  name: "kk",
  age: 12
};

工厂模式

把实现同一功能的代码写在函数体中,当想实现类似功能,直接执行当前函数:

减少了代码的冗余,实现了代码的高内聚,低耦合——函数的封装,实现了批量化的生产。

function createObj(name, age) {
  var obj = {};
  obj.name = name;
  obj.age = age;
  return obj;
}

构造函数模式

命名规范:构造函数(类)一般首字母大写

构造函数执行详细步骤:

  1. 形成私有作用域
  2. 形参赋值(如果不传参,可以省略小括号)
  3. 变量提升
  4. 创建出一个空对象,把 this 指向这个空对象
  5. 代码从上往下运行(属性赋值…),返回实例(this
  6. 作用域是否销毁

如果给构造函数手动返回一个引用类型数据,会返回这个引用类型数据

浏览器禁止调用内置类 NullUndefined

原型模式

  1. 每一个函数数据类型(普通函数、类(内置类、自定义类))都天生自带一个prototype属性,这个prototype的属性值是一个对象数据类型的;

  2. prototype这个对象中天生自带constructor属性,constructor的属性值指向了prototype对应的那个类;

  3. 每一个对象数据类型的(对象、数组、实例、prototype)都天生自带一个__proto__属性,属性的属性值指向当前实例所属类的原型;

当获取对象属性名对应的属性值,首先会判断是否为私有属性,如果私有属性中不存在,那么对象会默认通过__proto__继续向上查找,如果上级也没有,那么会继续通过__proto__向上查找,直到找到Object的原型为止,如果Object中也不存在,那么会输出undefined.这样一级一级向上查找过程会形成原型链.

__proto__ 不是 一个私有属性,用 Fn.prototype.hasOwnProperty(“__proto__”);返回false.

for-in 循环只遍历私有属性。

原型深入

函数的三种角色

  1. 普通函数
  2. 类-构造函数
  3. 对象

call apply bind

call 改变原函数this指向。

不传参数,严格模式下指向undefined,非严格指向window

参数为null,严格模式下指向null,非严格模式指向window

apply 改变原函数this指向,参数为数组

bind IE8 及以下不兼容

异常

try {
  //执行代码
} catch (e) {
  console.log(e); //e 异常
}

JSON

  1. 复合类型的值只能是数组或对象,不能是函数、正则表达式对象、日期对象。
  2. 简单类型的值只有四种:字符串、数值(必须以十进制表示)、布尔值和null(不能使用NaN, Infinity, -Infinityundefined)。
  3. 字符串必须使用双引号表示,不能使用单引号。
  4. 对象的键名必须放在双引号里面。
  5. 数组或对象最后一个成员的后面,不能加逗号。

在 IE7 及以下不兼容

数据绑定

  1. 创建元素对象

    document.creatElement("div");
    appendChild();
    
  2. 文档碎片

    document.createDocumentFragment();
    
  3. 字符串拼接

  4. es6 模板字符串

继承

  1. 把 Parent 的实例的空间赋值给类 Child 的原型,那么类 Child 的实例不仅可以调用类 Parent 的公有属性,还可以调用 Parent 的实例的私有属性;这种继承方式就是原型继承

    function Parent() {}
    function Child() {}
    Child.prototype = new Parent();
    

    缺点:

    • Parent 的所有引用类属性会被所有 Child 类实例共享:

      function Parent(){
        this.arr=[];
      }
      function Child(){}
      Child.prototype = new Parent();
           
      var child1 = new Child();
      child1.arr.push('b1');
           
      var child2 = new Child();
      console.log(child2.arr)//['b1']
      //造成引用类属性 arr 被所有 Child 类实例滥用
      
    • 在创建 Child 实例时,不能向 Parent 传参

  2. call : 改变 Parent 中的 this 指向,让其指向 Child 中的 this( Child 的实例); Child 创建的实例就具有Parent 的私有属性;

    function Parent () {
        this.names = ['kevin', 'daisy'];
    }
       
    function Child () {
        Parent.call(this);
    }
       
    var child1 = new Child();
       
    child1.names.push('yayu');
       
    console.log(child1.names); // ["kevin", "daisy", "yayu"]
       
    var child2 = new Child();
       
    console.log(child2.names); // ["kevin", "daisy"]
    

    缺点:

    方法都在构造函数中定义,每次创建实例都会创建一边方法,只能继承私有属性。

  3. 中间类继承

    __proto__: 改变了对象数据类型中__proto__的指向;

    function a() {
      // 在IE10 及以下是不兼容的;
      arguments.__proto__ = Array.prototype;
      arguments.sort();
    }
    
  4. 寄生组合式继承

    function Parent() {
      this.z = 100;
    }
    function Child() {
      Parent.call(this); // 解决私有问题
    }
    Child.prototype = Object.create(Parent.prototype); // 公有属性
    var b = new Child();
    console.log(b);
    
  5. ES6 继承

    class Fn {
      constructor() {}
    }
    class Foo extends Fn {
      constructor() {
        super();
      }
    }
    // es6 继承 继承私有和公有