不知道有没有小伙伴跟我有一样的困扰,很多前端的问题,总是知其然不知其所以然,虽然能解决问题,但是难免根源上遗漏很多细节,不太喜欢这种感觉,重新再学习一下前端基础,在这里做下记录和大家一起探讨,希望大家不吝指教...
IIFE(立即调用函数表达式)
- 是一个在定义时就会立即执行的 JavaScript 函数
这是一个
自执行匿名函数的设计模式,主要包含两部分。
第一部分是包围在圆括号运算符()里的一个匿名函数,这个匿名函数拥有独立的词法作用域,这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。
第二部分再一次使用()创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。
// 常规方式
(function(){
alert('IIFE');
})()
// 这里需要注意的是, IIFE 去掉括号则会报错
function(){}(); // 报错
// 这里需要将语法块转为表达式方式再执行,
// 使用这些运算符 !+ - ~ 都可以
!function(){
alert('IIFE');
}
JavaScript中的作用域
全局作用域
- 函数变量提升优先于变量,函数和变量同名且同时存在,这货没有值就会被直接忽略
- 函数变量提升
(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 作为全局变量被声明
});
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);
- 其实以上这些坑拿着原来的传统模式开发不小心都会被踩中,不过现在的
es6+时代其实很少会遇到这类问题,不过还是会有面试官会拿出来考面试者的,毕竟还是很容易忽略的点......大家还是知坑勿踩为上
块级作用域
其实
es5之前大都知道的是函数级作用域,而忽略了几个特殊的块级作用域
- 函数级作用域:
function block(){} - 逻辑语法块:
if(){}, 不过这个时候需要es6中let、const配合使用
if (false) {
let a = 1;
}
alert(a); // 报错,not defined
try{}catch(e){}语法块中会形成块级作用域
if (true) {
try {
throw 111;
} catch(a) {
// a 只在catch内部生效
alert(a); // 111
}
}
alert(a); // 报错,not defined
with只对对象中存在的属性有用,但是对象中不存在的属性,with会生成全局变量
// with 只对对象中存在的属性有用, 不存在,则生成全局变量
var obj = {
a: 1
};
with(obj) {
b = 2;
};
alert(obj.b); // undefined
alert(b); // 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();
=>绑定当前函数的顶级作用域