主要是一系列相关的概念介绍,需要有一定的相关的JS基础。
一、一切都是对象
一切都是对象 这句话的重点在于如何去理解“对象”这个概念。
——当然,也不是所有的都是对象,值类型就不是对象。
简单类型与复杂类型
JS分为简单类型和复杂类型,简单类型又叫做基本数据类型或者值类型,复杂类型又叫做引用类型。
值类型:简单数据类型/基本数据类型,在存储时变量中存储的是值本身,因此叫做值类型。 ( Number, String, Boolean, Null、Undefined、Symbol)
引用类型:复杂数据类型,在存储时变量中存储的仅仅是地址(引用),因此叫做引用数据类型,通过 new 关键字创建的对象(系统对象、自定义对象),如 Object、Array、Date等。
所以这句话的理解应该是:一切(引用类型)都是对象,对象是属性的集合
二、函数和对象的关系
对象都是通过函数创建的
举个例子
var obj = { a: 10, b: 20 };
var arr = [5, 'x', true];
虽然看起来跟函数没什么关系,但本质是依靠函数构造的。
//var obj = { a: 10, b: 20 };
//var arr = [5, 'x', true];
var obj = new Object();
obj.a = 10;
obj.b = 20;
var arr = new Array();
arr[0] = 5;
arr[1] = 'x';
arr[2] = true;
console.log(typeof (Object)); // function
console.log(typeof (Array)); // function
三、prototype原型
之前第二点提到,对象都是通过函数创建的。但是第一点又说一切都是对象,包括函数也是对象。他也是属性的集合,可以对函数进行自定义属性。
这是因为函数的一个属性——prototype。每个函数都有一个属性叫做prototype。
这个prototype的属性值是一个对象(属性的集合,再次强调!),默认的只有一个叫做constructor的属性,指向这个函数本身。
如上图,SuperType是是一个函数,右侧的方框就是它的原型。
原型既然作为对象,属性的集合,不可能就只弄个constructor来玩玩,肯定可以自定义的增加许多属性。例如这位Object大哥,人家的prototype里面,就有好几个其他属性。
也可以在自己自定义的方法的prototype中新增自己的属性
function Fn() { }
Fn.prototype.name = 'name';
Fn.prototype.getName = function () {
return this.name;
};
const fn = new Fn();
console.log(fn.name); // name
console.log(fn.getName()); // name
即,Fn是一个函数,fn对象是从Fn函数new出来的,这样fn对象就可以调用Fn.prototype中的属性。
因为每个对象都有一个隐藏的属性——“proto”,这个属性引用了创建这个对象的函数的prototype。即:fn.proto === Fn.prototype
这里的"proto"成为“隐式原型”
四、隐式原型
每个函数function都有一个prototype,即原型。这里再加一句话——每个对象都有一个__proto__,指向创建该对象的函数的prototype。
obj这个对象本质上是被Object函数创建的,因此obj.proto=== Object.prototype。我们可以用一个图来表示。
即,每个对象都有一个__proto__属性,指向创建该对象的函数的prototype。
那么上图中的“Object prototype”也是一个对象,它的__proto__指向哪里?
Object.prototype 是一个特例——它的__proto__指向的是null。
原型和原形链在这里讲的比较简单,可以去看之前一篇比较详细的文章 JS -原型与原形链
五、instanceof
对于值类型,你可以通过typeof判断,string/number/boolean都很清楚,但是typeof在判断到引用类型的时候,返回值只有object/function,你不知道它到底是一个object对象,还是数组。这个时候就需要用到instanceof。
function Foo() {};
var f1 = new Foo();
console.log(f1 instanceof Foo)// true
console.log(f1 instanceof Object) // true
上图中,f1这个对象是被Foo创建,但是“f1 instanceof Object”为什么是true呢?
nstanceof运算符的第一个变量是一个对象,暂时称为A;第二个变量一般是一个函数,暂时称为B。
Instanceof的判断队则是:沿着A的__proto__这条线来找,同时沿着B的prototype这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回true。如果找到终点还未重合,则返回false。
所以,上面代码 f1 instanceof Object ”为 true。
六、原型链
javascript中的继承是通过原型链来体现的。
function Foo() {};
var f1 = new Foo();
f1.a = 10;
Foo.prototype.a = 100;
Foo.prototype.b = 200;
console.log(f1.a) // 10
console.log(f1.b) // 100
以上代码中,f1是Foo函数new出来的对象,f1.a是f1对象的基本属性,f1.b是怎么来的呢?——从Foo.prototype得来,因为f1.__proto__指向的是Foo.prototype
访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着__proto__这条链向上找,这就是原型链。
七、执行上下文
浏览器在执行代码前需要一些“准备工作”:
- 变量、函数表达式——变量声明,默认赋值为undefined;
- this——赋值;
- 函数声明——赋值;
这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。
如果在函数中,除了以上数据之外,还会有其他数据。
function fn(x) {
console.log(arguments) // [10]
console.log(x) // 10
}
fn(10)
以上代码展示了在函数体的语句执行之前,arguments变量和函数的参数都已经被赋值。从这里可以看出,函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。
另外一点不同在于,函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。
行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。
其实这是一个压栈出栈的过程——执行上下文栈。
上下文栈的压栈、出栈过程:
- 在执行代码之前,首先将创建全局上下文环境
- 代码执行,上下文环境中的变量被赋值
- 调用函数,执行函数体语句之前,会创建一个新的执行上下文环境,并将这个执行上下文环境压栈,设置为活动状态
- 函数执行完毕后,调用函数所生成的上下文环境出栈,并且被销毁(已经用完了,就要及时销毁,释放内存)
八、作用域
作用域是一个很抽象的概念,类似于一个“地盘”。
如上图,全局代码和fn、bar两个函数都会形成一个作用域。而且,作用域有上下级的关系,上下级关系的确定就看函数是在哪个作用域下创建的。例如,fn作用域下创建了bar函数,那么“fn作用域”就是“bar作用域”的上级。
作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
九、作用域 和 执行上下文
首先明确一下,作用域在函数定义时确定, 执行上下文是在函数调用时确定。
作用域只是一个“地盘”,一个抽象的概念,其中没有变量。要通过作用域对应的执行上下文环境来获取变量的值。
同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。
所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。
可以看之前比较详细的文章 JS - 作用域和执行上下文的区别
十、自由变量和作用域链
自由变量,就是在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的)。对于A作用域来说,x就是一个自由变量。
var x = 10;
function fn() {
console.log(x + 20); // x 就是一个自由变量
}
那自由变量x 应该到哪个作用域取呢?
var x = 10;
function fn() {
console.log(x); // 10
}
function show(f) {
var x = 20;
(function() {
var x = 30
f()
})();
}
show(fn);
要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”,切记切记——其实这就是所谓的“静态作用域”。
上面描述的只是跨一步作用域去寻找。
如果跨了一步,还没找到呢?——接着跨!——一直跨到全局作用域为止。要是在全局作用域中都没有找到,那就是真的没有了。
这个一步一步“跨”的路线,我们称之为——作用域链。
十一、this
this的取值,分四种情况。
- 构造函数
function Foo() {
this.name = "name";
console.log(this);
}
var f1 = new Foo(); // Foo {name: 'name'}
console.log(f1.name); // name
- 函数作为对象的一个属性, this 由调用对象决定
var obj = {
name: "name",
fn: function () {
console.log(this); // {name: 'name', fn: ƒ}
console.log(this.name); // name
}
}
obj.fn();
var name = "window";
var obj = {
name: "name",
fn: function () {
console.log(this); // window
console.log(this.name); // window
}
}
obj.fn();
var foo = obj.fn;
foo()
- 函数用call或者apply调用,this的值就取传入的对象的值。
var x = 10;
var obj = {
x: 20
};
var fn = function() {
console.log(this.x);
}
fn(); // 10
fn.call(obj) //20
- 全局 & 调用普通函数
var x = 10;
var fn = function() {
console.log(this.x);
}
fn(); // 10
this 的调用还要区分 普通函数和普通箭头函数 JS-普通函数和箭头函数的this区别
十二、闭包
你只需要知道应用的两种情况即可——函数作为返回值,函数作为参数传递。
- 函数作为返回值
function fn() {
var max = 10;
return function bar(x) {
if(x>max) {
console.log(x);
}
}
}
var f1 = fn();
f1(15)
// 15
如上代码,bar函数作为返回值,赋值给f1变量。执行f1(15)时,用到了fn作用域下的max变量的值。
- 函数作为参数被传递
var max = 10;
var fn = function (x) {
if(x>max) {
console.log(x);
}
};
(function (f) {
var max = 100;
f(15);
})(fn);
// 15
fn函数作为一个参数被传递进入另一个函数,赋值给f参数。执行f(15)时,max变量的取值是10,而不是100。