本文是本人阅读学习深入理解JavaScript原型和闭包时所作的总结和笔记,当然也引用了很多原文,感兴趣的朋友也可以直接去看原文。
1、一切都是对象
先说结论,一切引用类型都是对象,对象是属性的集合。
首先我们对不同变量使用typeof()
看看都有哪些输出的类型。
console.log(typeof(x)); // undefined
console.log(typeof(10)); // number
console.log(typeof('abc')); // string
console.log(typeof(true)); // boolean
console.log(typeof(function () {})); // function
console.log(typeof([1, 'a', true])); // object
console.log(typeof({ a: 10, b: 20 })); // object
console.log(typeof(null)); // object
console.log(typeof(new Number(10))); // object
在以上代码中,undefined
, number
, string
, boolean
属于值类型,不是对象。
而其他的几种类型 - 包括函数、数组、对象、null
、new Number(10)
都是对象,它们属于引用类型。
在JavaScript中,数组是对象,函数是对象,对象还是对象。对象里面的一切都是属性,只有属性,没有方法,或者说方法也是一种属性。属性表示为键值对的形式。
JavaScript中的对象可以任意的扩展属性,定义属性的方法通常有两种。
var obj = {
a = 10,
b: function(x) {
console.log(this.a + x)
},
c: {
name: "Steven",
year: 1988
}
}
上面这段代码中,obj
是一个自定义的对象,其中a
、b
、c
是它的属性,而属性c
的本身又是一个对象,它又有name
、year
两个属性。
函数和数组不能用上面的方法定义属性,下面以函数为例:
var fn = function () {
alert(100);
};
fn.a = 10;
fn.b = function () {
alert(123);
};
fn.c = {
name: "Steven",
year: 1988
};
在jQuery源码中,变量jQuery
或者$
其实是一个函数,我们可以用typeof()
验证一下:
console.log(typeof ($)); // function
console.log($.trim(" ABC "));
很明显,这就是在$
或者jQuery
函数上加了一个trim
属性,属性值是函数,作用是截取前后空格。
2、函数和对象的关系
上文已经说到,函数也是一种对象,我们可以用instanceof
验证一下:
var fn = function () { };
console.log(fn instanceof Object); // true
但是函数和对象的关系却有一点复杂,请看下面这个例子:
function Fn() {
this.name = '严新晨';
this.year = 1990;
}
var fn_1 = new Fn();
由上面这个例子可以得出,对象是可以通过函数创建的。
但其实,对象都是通过函数创建的。
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;
而其中的Object
和Array
都是函数:
console.log(typeof (Object)); // function
console.log(typeof (Array)); // function
由此可以得出,对象都是通过函数创建的。
3、prototype原型
每个函数都有一个默认属性 - prototype
。
这个prototype
的属性值是一个对象,这个对象有一个默认属性 - constructor
,这个属性指向这个函数本身。
而原型作为一个对象,除了constructor
之外,当然可以有其他属性,以函数Object
为例,在浏览器调试窗口输入Object.prototype
会得到以下返回值:
Object
...
constructor
hasOwnProperty
isPrototypeOfs
toLocalString
toString
valueOf
...
同时,我们还可以为这个原型增添自定义方法或属性
function Fn(){}
Fn.prototype.name = "Steven"
Fn.prototype.getYear = function(){
return 1988;
}
var fn = new Fn();
console.log(fn.name);
console.log(fn.getYear());
在上例中,Fn
是一个函数,fn
对象是从Fn
函数中new
出来的,这样fn
对象就可以调用Fn.prototype
中的属性。
每个对象都有一个隐藏的属性 - __proto__
,这个属性引用了创建这个对象的函数的prototype
。即:fn.__proto__ === Fn.prototype
这里的__proto__
称为“隐式原型”。
4、隐式原型
每个函数function都有一个prototype
,即原型。
每个对象都有一个__proto__
,可称为隐式原型。
var obj = {}
console.log(obj.__proto__ === Object.prototype) // true
每个对象都有一个__proto__
属性,指向创建该对象的函数的prototype
function Foo(){}
Foo.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true
Function.__proto__ === Function.prototype // true
Function.prototype.__proto__ === Object.prototype // true
注意,Object.prototype.__proto__
是一个特例,它指向的是null
5、instanceof
由于typeof
在判断引用类型时,返回值只有object
或function
,这时我们可以用到instanceof
。
function Foo(){}
var f = new Foo()
console.log(f instanceof Foo) // true
console.log(f instanceof Object) // true
用法:A instanceof B
,变量A
是一个待判断的对象,变量B
通常是一个函数。
判断规则:沿着A.__proto__
和B.prototype
查找,如果能找到同一个引用,即同一个对象,则返回true
。
由以上判定规则,我们可以解释许多奇怪的判定结果,例如:
Object instanceof Function // true
Function instanceof Object // true
Function instanceof Function // true
instanceof
表示的是一种继承关系 - 原型链
6、继承
JavaScript中的继承通过原型链来体现。
function Foo(){}
var f = new Foo()
f.a = 10
Foo.prototype.a = 100
Foo.prototype.b = 200
console.log(f.a) // 10
console.log(f.b) // 200
上例中,f是Foo函数new出来的对象,f.a是对象f的基本属性,因为f.__proto__ === Foo.prototype
,所以f.b是从Foo.prototype
中继承而来的。
在JavaScript中,访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着__proto__
这条链向上找,这就是原型链
通过hasOwnProperty
,我们可以判断出一个属性到底是基本属性,还是从原型中继承而来的。
function Foo(){}
var f_1 = new Foo()
f.a = 10
Foo.prototype.a = 100
Foo.prototype.b = 200
var item
for(item in f){
console.log(item) // a b
}
for(item in f){
if(f.hasOwnProperty(item){
console.log(item) // a
})
}
hasOwnProperty
方法是从Object.prototype
中继承而来的
每个函数都有apply
、call
方法,都有length
、arguments
等属性,这些都是从Function.prototype
中继承而来的
由于Function.prototype.__proto__
指向Object.prototype
,所以函数也会有hasOwnProperty
方法
7、原型的灵活性
首先,对象属性可以随时改动
其次,如果继承的方法不合适,可以随时修改
var obj = { a: 10, b: 20 }
console.log(obj.toString()) // [object Object]
var arr = [1, 2, true]
console.log(arr.toString()) // 1, 2, true
从上例中可以看出,Object
和Array
的toString()
方法是不一样的,肯定是Array.prototype.toString()
作了修改。
同理,我们也可以自己定义一个函数并修改toString()
方法。
function Foo(){}
var f = new Foo()
Foo.prototype.toString = function(){
return "严新晨"
}
console.log(f.toString) // 严新晨
最后,如果缺少需要的方法,也可以自己创建
如果要添加内置方法的原型属性,最好做一步判断,如果该属性不存在,则添加。如果本来就存在,就没必要再添加了。
8、简述 - 执行上下文 - 上
执行上下文,也叫执行上下文环境
console.log(a) // 报错,a is not undefined
console.log(a) // undefined
var a;
console.log(a) // undefined
var a = 10;
console.log(this) // Window {...}
console.log(f_1) // function f_1({})
function f_1(){} // 函数声明
console.log(f_2) // undefined
var f_2 = function(){} // 函数表达式
在js代码执行前,浏览器会先进行一些准备工作:
变量、函数表达式 - 变量声明,默认赋值为
undefined
;this
- 赋值;函数声明 - 赋值;
这三种数据的准备工作我们称之为“执行上下文”或者“执行上下文环境”。
JavaScript在执行一个代码段之前,都会进行这些“准备工作”来生成执行上下文。这个“代码段”其实分三种情况 - 全局代码,函数体,eval代码。
首先,全局代码,写在<script>
标签里的代码段
其次,eval接收的是一段文本形式的代码(不推荐使用)
最后,函数体代码段是函数在创建时,本质上是new Function(…)
得来的,其中需要传入一个文本形式的参数作为函数体
var fn = new Function("x", "console.log(x+5)")
9、简述 - 执行上下文 - 下
function fn(x){
console.log(arguments)
console.log(x)
}
fn(10)
以上代码展示了在函数体的语句执行之前,arguments变量和函数的参数都已经被赋值。从这里可以看出,函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。
函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。
var a = 10
function fn(){
console.log(a)
}
function bar(fn){
var a = 20
fn() // 10
}
bar(fn)
总结一下上下文环境的数据内容
普通变量 - 声明
函数表达式 - 声明
函数声明 - 赋值
this - 赋值
如果代码段是函数体,则需加上参数 - 赋值
arguments - 赋值
自由变量的取值作用域 - 赋值
所以通俗来讲,执行上下文环境就是在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用undefined占个空。
10、this
this的取值,通常分4种情况
先强调一点,在函数中this到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了,因为this的取值是执行上下文环境的一部分,每次调用函数,都会产生一个新的执行上下文环境。
情况1:构造函数
所谓构造函数就是用来new对象的函数。
注意,构造函数的函数名第一个字母大写(规则约定)。例如:Object、Array、Function等。
function Foo(){
this.name = "Steven"
this.year = 1988
console.log(this) // Foo {name: "Steven", year: 1988}
}
var f = new Foo();
console.log(f.name) // Steven
console.log(f.year) // 1988
如果函数作为构造函数调用,那么其中的this就代表它即将new出来的对象。
如果直接调用Foo函数,而不是new Foo(),情况就完全不同。
function Foo(){
this.name = "Steven"
this.year = 1988
console.log(this) // Window {...}
}
Foo()
情况2:函数作为对象的一个属性
如果函数作为对象的一个属性,并且作为对象的一个属性被调用时,函数中的this指向该对象。
var obj = {
x: 10,
fn: function(){
console.log(this) // Object {x: 10, fn: function}
console.log(this.x) // 10
}
}
obj.fn()
如果函数fn是对象obj的一个属性,但是不作为obj的一个属性被调用
var obj = {
x: 10,
fn: function(){
console.log(this) // Window {...}
console.log(this.x) // undefined
}
}
var f = obj.fn
f()
情况3:函数用call或者apply调用
当一个函数被call和apply调用时,this的值就取传入的对象的值。
var obj = { x: 10 }
var fn = function(){
console.log(this) // Object {x:10}
console.log(this.x) // 10
}
fn.call(obj)
情况4:全局&调用普通函数
在全局环境下,this永远是Window
普通函数在调用时,其中的this也都是Window
var x = 10
var fn = function(){
console.log(this) // Window
console.log(this.x) // 10
}
fn()
下面情况需要注意一下
var obj = {
x: 10,
fn: function(){
function foo(){
console.log(this) // Window
console.log(this.x) // undefined
}
foo()
}
}
obj.fn()
函数foo
虽然是在obj.fn
内部定义的,但是它仍然是一个普通的函数,this
仍然指向window
。
补充:情况5 - 在构造函数的prototype
中
function Fn() {
this.name = 'Steven';
this.year = 1988;
}
Fn.prototype.getName = function () {
console.log(this.name); // 这里的this指向f1对象
}
var f1 = new Fn();
f1.getName(); // Steven
11、执行上下文栈
上文说过,执行全局代码时,会产生一个全局上下文环境,每次调用函数都又会产生函数上下文环境。当函数调用完成时,函数上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境始终只有一个。其实这是一个压栈出栈的过程 - 执行上下文栈。
以下面代码为例:
var a = 10, // 1、进入全局上下文环境
fn,
bar = function(x){
var x = 5
fn(x+b) // 3、进入fn()函数上下文环境
}
fn = function(y){
var c = 5
console.log(y+c)
}
bar() // 2、进入bar()函数上下文环境
执行代码前,首次创建全局上下文环境
a === undefined
fn === undefined
bar === undefined
this === window
代码执行时,全局上下文环境中的各个变量被赋值
a === 10
fn === function
bar === function
this === window
调用bar()
函数时,会创建一个新的函数上下文环境
b === undefined
x === 5
arguments === [5]
this === window
以上是一段简短代码的执行上下文环境的变化过程,一个完整的闭环。
但实际上,上述情况是一种理想的情况。而有一种很常见的情况,无法做到这样干净利落的说销毁就销毁,那就是闭包。
12、简述 - 作用域
JavaScript没有块级作用域。所谓的“块”就是“{}”中的语句,比如:if(){}
或者for(){}
之类的。
所以,编写代码时不要在“块”里声明变量。
重点来了:JavaScript除了全局作用域之外,只有函数可以创建的作用域
所以,在声明变量时,全局代码要在代码前端声明,函数中要在函数体一开始就声明好。除了这两个地方,其他地方都不要出现变量声明。而且建议用“单var”形式。
作用域有上下级的关系,上下级关系的确定就看函数是在哪个作用域下创建的
作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突
13、作用域和上下文环境
在上文中已经说过,除了全局作用域之外,每个函数都会创建自己的作用域。作用域在函数定义时就已经确定了,而不是在函数调用时确定。
var a = 10,
b = 20;
// 全局作用域:a=10,b=20
function fn(x){
var a = 100,
c = 300;
// fn(10):a=100,c=300,x=10
function bar(x){
var a = 1000,
d = 4000;
// bar(100):a=1000,d=4000,x=100
// bar(100):a=1000,d=4000,x=200
}
bar(100);
bar(200);
}
fn(10)
作用域只是一个“地盘”,一个抽象的概念,其中没有变量。
要通过作用域对应的执行上下文环境来获取变量的值。
**同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。
所以,作用域中变量的值是在执行过程中产生的确定的,而作用域是在函数创建时就确定了。
所以,如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值。
14、从自由变量到作用域链
上文中有一种常见情况我们并没有讨论,那就是跨作用域取值的情况。
先说明一个概念 - 自由变量
var a = 10
function fn(){
var b = 20
return a + b // 这里的a就是一个自由变量
}
很多人对此解释为a是从父作用域取值的,这种说法基本正确,但有些时候会产生歧义。
var x = 10;
function fn(){
console.log(x) // 10
}
function show(f){
var x = 20;
(function(){
f() // 10,而不是20
})()
}
show(fn);
所以,更准确的说法是,我们要到创建这个函数的作用域中去取值。
var a = 10;
function fn() {
var b = 20;
function bar() {
console.log(a + b);
// 创建函数bar()时,b=20
// 通过作用域链查找到a=10
}
return bar;
}
var x = fn(),
b = 200;
x(); // 30
15、闭包
先回顾下前面章节讲到的两个重点:
自由变量跨作用域取值时,要去创建函数的作用域取值,而不是调用函数的作用域;
当一个函数被调用完成之后,其执行上下文环境将被销毁,其中的变量也会被同时销毁。
“闭包”这个词的概念很不好解释,但我们只需记住两种情况即可:函数作为返回值和函数作为参数传递。
首先,函数作为返回值,先看个例子
// 1、全局作用域,max=100,其他变量undefined
function fn() {
// 2、fn()作用域,max=10(调用结束后销毁),其他变量undefined
var max = 10;
return function bar(x) {
// 3、bar()作用域,max=10,x=15(调用结束后销毁)
if (x > max) {
console.log(x);
}
}
}
var f1 = fn(), // bar()作为返回值赋值给f1
max = 100;
f1(15); // 15
然后,函数作为参数传递,再看个例子
// 全局作用域,max=10
var max = 10,
fn = function (x) {
if (x > max) {
console.log(x) // 15
}
};
(function (f) {
var max = 100;
f(15); // max=10而不是100
})(fn);
先回顾下前面章节讲到的两个重点:
自由变量跨作用域取值时,要去创建函数的作用域取值,而不是调用函数的作用域;
当一个函数被调用完成之后,其执行上下文环境将被销毁,其中的变量也会被同时销毁。
16、完结 - 这章没干货我也就不写了o(╯□╰)o
17、补充 - this - 我直接写在10、this那一篇里了就不赘述了
18、补充 - 上下文环境和作用域的关系
本篇主要是解释一下上下文环境和作用域并不是一回事。
上下文环境 - 可以理解为一个看不见摸不着的对象(有若干个属性),在调用函数时创建,用来保存调用函数时的各个变量。
作用域 - 除了全局作用域,只有创建函数才会创建作用域,无论你是否调用函数,函数只要创建了就有一个独立的作用域。
两者 - 一个作用域可能包含若干个上下文环境,也可能从来没有过上下文环境(函数从未被调用),还可能函数调用完毕后上下文环境被销毁了等多种情况。
以下面代码为例:
// 全局作用域中x=100
var x = 100;
function fn(x) {
// fn(x)作用域
// 调用f1()时的上下文环境中,x=5
// 调用f2()时的上下文环境中,x=10
return function () {
// 匿名function作用域
console.log(x);
}
}
var f1 = fn(5),
f2 = fn(10);
f1(); // 5
f2(); // 10
所谓上下文环境就是调用函数时创建的一个临时作用域,根据调用情况不同里面的变量会发生变化;而作用域是随着函数的创建而创建,里面的变量可能会在调用时被上下文环境中相同变量覆盖。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。