关于 js 中的几个简单核心点

211 阅读7分钟

不知道有没有小伙伴跟我有一样的困扰,很多前端的问题,总是知其然不知其所以然,虽然能解决问题,但是难免根源上遗漏很多细节,不太喜欢这种感觉,重新再学习一下前端基础,在这里做下记录和大家一起探讨,希望大家不吝指教...

IIFE(立即调用函数表达式)

  • 是一个在定义时就会立即执行的 JavaScript 函数

这是一个自执行匿名函数的设计模式,主要包含两部分。
第一部分是包围在圆括号运算符()里的一个匿名函数,这个匿名函数拥有独立的词法作用域,这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。
第二部分再一次使用()创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。

// 常规方式
(function(){
    alert('IIFE');
})()

// 这里需要注意的是, IIFE 去掉括号则会报错
function(){}(); // 报错

// 这里需要将语法块转为表达式方式再执行,
// 使用这些运算符 !+ - ~ 都可以
!function(){
    alert('IIFE');
}

JavaScript中的作用域

全局作用域

  1. 函数变量提升优先于变量,函数和变量同名且同时存在,这货没有值就会被直接忽略
  • 函数变量提升
(function(){
    alert(a);
    var a = 1;
    function a(){}
})(); // function a(){}

// ==> 变量提升后
(function(){
    function a(){}
    var a; // 函数和变量同名且同事存在,这货没有值就会被直接忽略
    alert(a);
    a = 1; // a的赋值保留当前的词法作用域
})();
  • 声明变量提升
(function(){
    // 这里之前面试被问过,有个变量提升的暗坑
    var a = b = 1;
})();
alert(a); // 报错,not defined
alert(b); // 1

// ==> 变量提升后
(function(){
    var a = 1; // a 作为局部变量被声明
    b = 1; // b 作为全局变量被声明
});
  1. es5 中只有大家多注意到的函数级作用域,其实还有特殊的块级作用域(下面会讲到)
  • 无函数时的变量提升
if (false){
    // 没有函数,则提升到全局
    var a = 1;
}
alert(a); // undefined

// ===> 变量提升后
var a;
if (false) {
    a = 1;
}
alert(a); // undefined
  • 有函数时的变量提升
function test() {
    if (false) {
        // 有函数,则提升到函数顶端
        var a = 1;
    }
    alert('inner ' + a); // undefined
}
test(); // inner undefined
alert(a); // a is not defined(报错)

// ===> 变量提升后
function test() {
    var a;
    if (false) {
        a = 1;
    }
    alert('inner ' + a);
}
test();
alert(a);
  1. 其实以上这些坑拿着原来的传统模式开发不小心都会被踩中,不过现在的es6+时代其实很少会遇到这类问题,不过还是会有面试官会拿出来考面试者的,毕竟还是很容易忽略的点......大家还是知坑勿踩为上

块级作用域

其实es5之前大都知道的是函数级作用域,而忽略了几个特殊的块级作用域

  1. 函数级作用域:function block(){}
  2. 逻辑语法块:if(){}, 不过这个时候需要 es6letconst 配合使用
if (false) {
    let a = 1;
}
alert(a); // 报错,not defined
  1. try{}catch(e){} 语法块中会形成块级作用域
if (true) {
    try {
        throw 111;
    } catch(a) {
        // a 只在catch内部生效
        alert(a); // 111
    }
}
alert(a); // 报错,not defined
  1. with 只对对象中存在的属性有用,但是对象中不存在的属性,with 会生成全局变量
// with 只对对象中存在的属性有用, 不存在,则生成全局变量
var obj = {
    a: 1
};
with(obj) {
    b = 2;
};
alert(obj.b); // undefined
alert(b); // 2

闭包

  1. 概念这里就不用说了,这里借鉴下大神的整理 @闭包,通俗易懂
  2. 需要注意的有两个点:
    • 闭包中的变量都保存在内存中,无法及时清理,容易造成内存泄漏;
    function func(){
        var name = 'hello world';
        return (function(){
            // 这里的 name 会一直被占用,无法释放
            return name;
        })()
    }
    
    • 在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是,每个对象的创建)。
    function MyObject(name, message) {
      this.name = name.toString();
    
      // 这里的方法其实每次new 实例的时候都会赋值一次
      this.getName = function() {
        return this.name;
      };
    }
    
    // 以上这种情况应当避免使用闭包
    // 使用原型继承的方式进行改造
    function MyObject(name, message) {
      this.name = name.toString();
    }
    // 这里直接在原型链式进行赋值
    MyObject.prototype.getName = function() {
      return this.name;
    };
    

原型链

这里引用网上找的一个图,很形象,描述了原型链的整个完整过程

  • JavaScript 在处理面向对象编程的过程中, es6 之前并没有 class,而是采用 Function 去代替
var Animal = function(name){
    // constructor == Animal 构造函数和 Animal 方法本身相等
    this.color = 'orange';
    this.name = name;
    this.getName = function(){
        return this.color + '的动物是' + this.name;
    }
}
// 实例化,执行构造函数,即 Animal 本身,这个时候 this 则指向 fox
var fox = new Animal('fox'); 

// 该实例的构造函数与对象 Animal 本身相等
foo.constructor === Animal;

// 由此类推, 即可推测 Animal 其实就是一个 Function 的实例
Animal.constructor === Function;
  • 通过以上 Animal 去实例化的话,可以看到构造函数其实每次实例化都会去执行,为了避免这种性能浪费,则需要通过 原型链 去处理
var Animal = function(name) {
    this.name = name;
}
// 这里在 Animal 的原型链上去声明了 getName
Animal.prototype.getName = function(){
    return this.name;
}

// 这个时候实例化的对象 fox 原型链中则会共享 Animal 原型链上的方法
var fox = new Animal('fox');

// 这里其实 fox 实例的 getName 方法和 Animal 原型链上的方法是同一个
// 就不需要再通过构造函数去重新声明了
fox.getName === Animal.prototype.getName
  • 以上对象创建完成,则再深入一些,就需要考虑对象的继承了
// 继承对象 Animal 需要注意以下几点
// 1. 拿到父类原型上的方法
// 2. 构造函数不能执行2次
// 3. 原型链上的方法不能直接按址引用,否则改写当前原型链则会影响父级的原型链
// 4. 子类的 constructor 必须指向当前类

var Fox = function(name){
    this.name = name
};

// 复制一个原型链的副本,包括其方法、属性、constructor 
var proto = Object.create(Animal.prototype);
// 继承复制的原型链
Fox.prototype = proto;

// 这个时候如果 new Fox() 则这个实例的构造函数还是 Animal 的
// 所以这个时候需要修复原型链的 constructor 指向
proto.constructor = Fox;

// 到这里就完成了Fox 继承 Animal
var fox = new Fox('Bob');
// 继承了 父类 Animal 的方法
fox.getName()
  • 这里有一个点需要注意下,构造函数的属性方法优先级要由于原型链上的属性方法
var Fox = function(name){
    this.name = name;
}
Fox.prototype.name = 'Bob';

var fox = new Fox('Sally');
// 这里其实构造函数的 name 的优先级要优于原型链上的 name
fox.name ==> Sally
// 这里直接访问原型链上的 name
fox.__proto__.name ==> Bob
  • __proto__ 属性可以向上找到父级, Object 是所有对象的父级,即最顶层对象,所有对象都可以通过 __proto__ 向上找到它
fox.__proto__ === Fox.prototype
// Animal ==> Fox ==> fox 实例
fox.__proto__.__proto__ === Fox.prototype.__proto__ === Animal.prototype;
// 这里需要注意一点:对象实例没有 prototype 原型链属性,而函数有 prototype
fox.__proto__.__proto__.__proto__ === Object.prototype

Object.prototype.__proto__ === null
  • Function 是所有函数的父级,所有函数都可以通过 __proto__ 找到它
Animal.__proto__ === Function.prototype;
Fox.__proto__ === Function.prototype;

// 这里其实可以发现 Object 也是一个函数
Object.__proto__ === Function.prototype;
  • Function 的父级则是它自己,这个是比较特殊的
Function.prototype === Function.__proto__;

this 指针

  • 谁引用,则当前 this 指向谁
  • 需要注意的是 apply / call / bind 都可以改变 this 指向,其中 bind 则会返回一个新的对象
function Animal(){
}
function Fox(){
  console.log(this.name)   
}

Fox.apply(Animal) ==> Animal
Fox.call(Animal) ==> Animal
var newFox = Fox.bind(Animal);
newFox();
  • => 绑定当前函数的顶级作用域