本文是本人阅读学习王福朋的博客 - 时所记的笔记。
先说结论,一切引用类型都是对象,对象是属性的集合。
首先我们对不同变量使用
看看都有哪些输出的类型。
- 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
上文已经说到,函数也是一种对象,我们可以用
验证一下:
- 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
由此可以得出, 对象都是通过函数创建的 。
每个函数都有一个默认属性 -
。
- 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__
每个函数 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
由于
在判断引用类型时,返回值只有
- 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
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.b 是从
- f.__proto__ === Foo.prototype
中继承而来的。
- Foo.prototype
这条链向上找,这就是原型链
- __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
首先,对象属性可以随时改动
其次,如果继承的方法不合适,可以随时修改
- 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) // 严新晨
最后,如果缺少需要的方法,也可以自己创建
如果要添加内置方法的原型属性,最好做一步判断,如果该属性不存在,则添加。如果本来就存在,就没必要再添加了。
执行上下文,也叫执行上下文环境
- 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 代码 。
其次,eval 接收的是一段文本形式的代码(不推荐使用)
最后,函数体代码段是函数在创建时,本质上是
得来的,其中需要传入一个文本形式的参数作为函数体
- new Function(…)
- var fn = new Function("x", "console.log(x+5)")
- 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)
总结一下上下文环境的数据内容
所以通俗来讲,执行上下文环境就是在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用 undefined 占个空
this 的取值,通常分 4 种情况
先强调一点, 在函数中 this 到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了 ,因为 this 的取值是执行上下文环境的一部分,每次调用函数,都会产生一个新的执行上下文环境。
情况 1:构造函数
所谓构造函数就是用来 new 对象的函数。
注意,构造函数的函数名第一个字母大写(规则约定)。例如:Object、Array、Function 等。
- function Foo(){
- this.name = "严新晨"
- this.year = 1990
- console.log(this) // Foo {name: "严新晨", year: 1990}
- }
- var f = new Foo();
- console.log(f.name) // 严新晨
- console.log(f.year) // 1990
如果函数作为构造函数调用,那么其中的 this 就代表它即将 new 出来的对象。
如果直接调用 Foo 函数,而不是 new Foo(),情况就完全不同。
- function Foo(){
- this.name = "严新晨"
- this.year = 1990
- 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
- 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
上文说过,执行全局代码时,会产生一个全局上下文环境,每次调用函数都又会产生函数上下文环境。当函数调用完成时,函数上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。 处于活动状态的执行上下文环境始终只有一个。其实这是一个压栈出栈的过程 - 执行上下文栈。
以下面代码为例:
- 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
以上是一段简短代码的执行上下文环境的变化过程,一个完整的闭环。
但实际上,上述情况是一种理想的情况。而有一种很常见的情况,无法做到这样干净利落的说销毁就销毁,那就是闭包。
JavaScript 没有块级作用域。所谓的 "块" 就是 "{}" 中的语句,比如:
或者
- if(){}
之类的。
- for(){}
所以,编写代码时不要在 "块" 里声明变量。
重点来了: JavaScript 除了全局作用域之外,只有函数可以创建的作用域
所以,在声明变量时,全局代码要在代码前端声明,函数中要在函数体一开始就声明好。除了这两个地方,其他地方都不要出现变量声明。而且建议用 "单 var" 形式。
在上文中已经说过,除了全局作用域之外,每个函数都会创建自己的作用域。 作用域在函数定义时就已经确定了,而不是在函数调用时确定 。
- var a = 10,
- b = 20;
- function fn(x){
- var a = 100,
- c = 300;
- function bar(x){
- var a = 1000,
- d = 4000;
- }
- bar(100);
- bar(200);
- }
- fn(10)
来源: