JavaScript中你所不知道的Object(二)--Function篇

时间:2021-08-05 08:37:45

  上一篇(JavaScript中你所不知道的Object(一))说到,Object对象有大量的内部属性,而其中多数和外部属性的操作有关。最后留了个悬念,就是Boolean、Date、Number、String、Function等有更多的内部属性,而它们分别是什么呢?

  这些内部属性不能像Object的内部属性一样一言以蔽之,因为它们各有各的用处和特点。其中核心的部分自然是最特殊的对象,Function对象。我们先从简单的开始:

  1. [[PrimitiveValue]]: 值的类型是基础数据类型。所以所有的包装类比如Boolean、Number、String都有此内部属性,其中Date也有,用来存储时间戳。
  2. [[Construct]]: 值的类型是方法。传入参数列表,返回Object。Function对象特有,当使用new func()调用的就是这个内部属性。所以拥有这个属性的方法也可以叫做构造器。注意区分构造器的prototype对象下的constructor属性。
  3. [[Call]]: 值的类型是方法。传入参数列表,返回任意数据类型。Function对象特有,当使用func()调用的就是这个内部属性。
  4. [[HasInstance]]: 值的类型是方法。传入任意值,返回Boolean。检测这个参数的原型链上是否有此函数的prototype,即检测参数及其原型中是否有对象是此函数作为构造器时创建的。
  5. [[Scope]]: Function对象特有,每当函数执行时使用此内部属性新建作用域链加入到新的执行环境中。
  6. [[FormalParameters]]: 形参列表。Function对象特有。
  7. [[Code]]: JS代码。Function对象特有。
  8. [[TargetFunction]]: 如下
  9. [[BoundThis]]: 如下
  10. [[BoundArguments]]: 如下
  11. [[ParameterMap]]: 形参列表。arguments对象特有。
    以上三个为Function.prototype.bind创建的函数特有的内部属性。
    var arr1 = [1, 4, 5],
    arr2 = [2, 3]; var func = Array.prototype.splice.bind(arr1, 1, 0); func.apply(arr1, arr2); console.log(arr1); //[1, 2, 3, 4, 5]

    例子中,func是通过bind创建的函数,其内部属性[[TargetFunction]]对应Array.prototype.splice,[[BoundThis]]对应arr1,[[BoundArguments]]对应1,0组成的列表。

  上面列出的属性乍看一下好像都能理解,但细思恐极,比如标红的作用域和执行环境,都是很抽象的概念,我们现在就来完整的剖析下这些概念。

  首先引入的概念就是可执行代码。ES规定三种可执行代码:全局代码、Eval代码、函数代码。当执行到这三种代码的时候,解释器会创建并进入新的执行环境,当代码运行完毕的时候,解释器会退出并销毁当前执行环境,并回到前一个执行环境。

  到现在为止执行环境还是一个抽象的概念,那执行环境的具体实现是怎样的呢?首先,它有三个要素:词法环境、变量环境、this绑定。this绑定我们都比较熟悉,那词法环境和变量环境分别是干什么用的呢?简单而不太严谨的说,词法环境就是作用域链,用来取变量的值;而变量环境暂时理解为当前作用域吧,用来赋变量的值。我们举个栗子吧:

var a = 0;

function test0() {
console.log(a); //
console.log(window.a); //
} function test1() {
a = 1;
console.log(a); //
console.log(window.a); //
} function test2() {
var a = 2;
console.log(a); //
console.log(window.a); //
} test0();
test1();
test2();

  这里涉及4个执行环境:全局执行环境、test0-2的执行环境。

  进入test0的执行环境以后,要打印出a,当前的作用域中没有定义这个变量,于是沿着作用域链找,找到了全局执行环境中定义的a,于是打印出来0,相信这里没什么问题。

  进入test1的执行环境后,这时已经退出了test0的执行环境。这时候给a赋了一个值,可能就有人立即想到,赋值?ok,变量环境,作用于当前作用域,所以a应该是1,window.a应该还是0,但结果却不是这样的。我们把a = 1拆开来看,首先是取a这个变量,当前作用域是没有这个变量的,所以a这个变量指向了全局执行环境中的a,然后才是赋值,1自然赋给了全局执行环境中的a。那么怎么让当前作用域中有这个变量呢?声明!即test2中的var a = 2,我们同样拆开来看,先是声明了a,所以在当前作用域中绑定了a这个变量,即在变量环境中添加了a,然后取a这个变量,从作用域链中的当前作用域就找到了a,最后把2赋给a。

  所以我们来规范一下上面的定义:词法环境用于查找变量,可以理解为作用域链。变量环境呢,不能再理解为作用域了,它是一个用来存储当前执行环境和变量之间的绑定信息的对象。注意这里隐藏了一个关键点:取变量是以作用域为单位查找的,而声明变量是以执行环境为单位存储的。不理解没关系,继续往下走。

  例子中细心的话可能察觉到一丝不对劲:为什么我在全局执行环境中声明的变量可以通过全局对象访问呢,那么我在test2中声明的a可以通过test2.a访问吗?

  当然是不行的。因为作用域分为两类:一种是声明式的,一种是对象式的。function产生的作用域是声明式的,而全局执行环境对应的作用域是对象式的。对象式作用域中声明的所有变量都可以通过此对象的属性进行访问,而声明式则可以定义不可以被修改的变量,你在严格模式下修改function中的arguments试试。

  相信看到这里基本已经陷入混乱了。我来整理几个问题:作用域和执行环境到底什么关系?声明式和对象式作用域只对应function和全局吗?

  首先,一个执行环境中是可以产生多个作用域的,但都有一个基础作用域。然后其他作用域怎么生成呢?声明式的作用域还有一种方式生成,就是catch语句,我们在catch(e){}的语句中可以通过e访问到错误对象,就是生成了一个声明式的作用域,并在这个作用域里添加了变量e。而对象式的作用域也同样还有一种方式生成,就是with语句。

var a = {
name: 'tarol'
},
name = 'okal'; with(a) {
console.log(name); //tarol
console.log(window.name); //okal
name = 'ctarol';
console.log(name); //ctarol
console.log(window.name); //okal
var age = 18;
} console.log(a.age); //undefined
console.log(window.age); //

  例子中,进入with语句后,生成了一个新的对象式作用域,并添加到了作用域链的头部,所以在语句中对变量name的访问是取a.name的值,对变量name的赋值也是对a.name的赋值。疑难点在var age = 18后,age这个属性没有赋给a,而是赋给了window。就像小伙a看上了姑娘age,说好了也订了婚,最后姑娘嫁给了a的老大window(怎么有种曹操和关羽的既视感)。其实原因就在上面,with语句修改了执行环境的词法环境,所以把访问变量的规则改了,但是没有修改变量环境,所以声明的变量统统都给了全局执行环境中的基础作用域(a:我怎么把这茬给忘了?)。

  另外把var age = 18中的var去掉,结果还是一样的。因为娶变量的时候,媒婆找啊找,找到最后一个作用域了都没找到,于是就停到那里了,突然看到有个值送上门来,也懒得换地方了,当场就在这个作用域把这个值打扮成了个变量。所以前一个例子中去掉var a = 0也不影响test1()的结果。

  注意!注意!注意!对整个作用域链中未定义的变量赋值,这个变量会绑定到作用域链的尾部,而给这个原型链中未定义的属性赋值,这个属性会绑到原型链的头部即当前的对象中。一次给两个栗子:

var a = {};
function test() {
with(a) {
age = 19;
}
}
test();
console.log(window.age); //
var a = {},
b; b = Object.create(a);
b.age = 18; console.log(a.age); //undefined

  好了,说了那么多,回到之前的内部属性[[Scope]]。它是在创建函数生成,值是创建时的作用域链;并在执行函数时取用,生成新执行环境中的作用域链。

  注意这个[[Scope]]是创建函数是生成!

  注意这个[[Scope]]是创建函数是生成!

  注意这个[[Scope]]是创建函数是生成!所以无论在哪里执行这个函数作用域链都是一样的。

  给个栗子:

var a = {
age: 18
},
age = 19; with(a) {
var test0 = function() {
console.log(age);
};
function test1() {
console.log(age);
}
test0(); //
test1(); //
} !function() {
var age = 20;
function test2() {
console.log(age);
}
test0(); //
test1(); //
test2(); //
}() test0(); //
test1(); //

  需要注意的是,在with语句中新建函数,如果此函数的作用域链中想插入a,要用test0的声明方式,而不是test1。至于为什么?那是变量声明和函数声明的时序问题,在进入执行环境后,会先遍历所有的变量声明和函数声明,会生成函数对象,但不会对变量赋值。这也是为什么JS中函数可以先调用后声明,因为你随便写在哪里,一进入执行环境就函数就生成了。而test1函数生成的时候还没有run到with语句,[[Scope]]自然是全局执行环境的基础作用域链。test0开始只声明个undefined的变量,到with语句才进行赋值,所以这个函数的[[Scope]]中加入了a。

  那么,[[Scope]]又是怎么生成作用域的呢,还有之前说到,全局代码和function代码都存在基础作用域,甚至是with语句,它们的作用域都是怎么生成的呢?

  首先,作用域都是在开始执行的时候生成的。

  全局代码和with语句生成作用域的过程是类似的,会绑定原型链上所有属性名和属性值作为变量名和变量值到作用域上。

  而function代码是先通过[[Scope]]生成新的作用域链,并绑定形参名和实参值作为变量名和变量值到作用域链的顶部即当前的作用域上。另外还有个特殊的变量,即arguments。arguments是一个特殊的对象,但不是数组对象,它在原型链上的位置处于倒数第三位,也就是原生Object对象的位置。那么,要把arguments装作用域,拢共分几步?

  四步:

  1. 通过Object构建对象,并用实参数组的长度初始化此对象的length属性
  2. 遍历实参数组,以index为属性名,对应实参为属性值给此对象添加属性
  3. 将形参数组赋给此对象的[[ParameterMap]](arguments特有的内部属性),如果不在严格模式下,给此对象添加callee和caller属性
  4. 绑定arguments到作用域,并指向此对象

  写的比较乱,而且为了便于理解,并没有规范化一些概念,比如Environment Records变成了作用域,Lexical Environments变成了作用域链,以后再行整理。

JavaScript中你所不知道的Object(二)--Function篇的更多相关文章

  1. JavaScript中你所不知道的Object(一)

    Object实在是JavaScript中很基础的东西了,在工作中,它只有那么贫瘠的几个用法,让人感觉不过尔尔,但是我们真的了解它吗? 1. 当我们习惯用 var a = { name: 'tarol' ...

  2. 你所不知道的linq(二)

    上一篇说了from in select的本质,具体参见你所不知道的linq.本篇说下from...in... from... in... select 首先上一段代码,猜猜结果是什么? class P ...

  3. Visual Studio中你所不知道的智能感知

    在Visual Studio中的智能感知,相信大家都用过.summary,param,returns这几个相信很多人都用过的吧.那么field,value等等这些呢. 首先在Visual Studio ...

  4. KVO中你所不知道的"坑"

      一.什么是 KVO 首先让我们了解一下什么KVO,全称为Key-Value Observing,是iOS中的一种设计模式,用于检测对象的某些属性的实时变化情况并作出响应.键值观察Key-Value ...

  5. Go基础之--位操作中你所不知道的用法

    之前一直忽略的就是所有语言中关于位操作,觉得用处并不多,可能用到也非常简单的用法,但是其实一直忽略的是它们的用处还是非常大的,下面先回顾一下位操作符的基础 位操作符 与操作:&1 & ...

  6. 前端开发 CSS中你所不知道的伪类与伪元素的区别--摘抄

    做过前端开发的人都熟悉伪类与伪元素,而真正能够彻底了解这二者的区别的人并不多.伪类与伪元素确实很容易混淆. 伪元素主要是用来创建一些不存在原有dom结构树种的元素,例如:用::before和::aft ...

  7. 闭包----你所不知道的JavaScript系列(4)

    一.闭包是什么? · 闭包就是可以使得函数外部的对象能够获取函数内部的信息. · 闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. · 闭包就 ...

  8. js值----你所不知道的JavaScript系列(6)

    1.数组 在 JavaScript 中,数组可以容纳任何类型的值,可以是字符串.数字.对象(object),甚至是其他数组(多维数组就是通过这种方式来实现的) .----<你所不知道的JavaS ...

  9. js类型----你所不知道的JavaScript系列(5)

    ECMAScirpt 变量有两种不同的数据类型:基本类型,引用类型.也有其他的叫法,比如原始类型和对象类型等. 1.内置类型 JavaScript 有七种内置类型: • 空值(null) • 未定义( ...

随机推荐

  1. Spring Enable annotation – writing a custom Enable annotation

    原文地址:https://www.javacodegeeks.com/2015/04/spring-enable-annotation-writing-a-custom-enable-annotati ...

  2. 前端性能优化--为什么DOM操作慢?

    作为一个前端,不能不考虑性能问题.对于大多数前端来说,性能优化的方法可能包括以下这些: 减少HTTP请求(合并css.js,雪碧图/base64图片) 压缩(css.js.图片皆可压缩) 样式表放头部 ...

  3. springmvc中RequestMapping的解析

    在研究源码的时候,我们应该从最高层来看,所以我们先看这个接口的定义: package org.springframework.web.servlet; import javax.servlet.htt ...

  4. 单点登录的原理与CAS技术的研究

    1.什么是单点登录? 关于单点登录技术的说明参考文章:http://www.cnblogs.com/yupeng/archive/2012/05/24/2517317.html 一般来说,整个原理大家 ...

  5. *收藏

    Make a video using several .png images http://*.com/q/13590976/5624248 Specifying and sa ...

  6. 九度oj题目1207:质因数的个数

    题目描述: 求正整数N(N>1)的质因数的个数. 相同的质因数需要重复计算.如120=2*2*2*3*5,共有5个质因数. 输入: 可能有多组测试数据,每组测试数据的输入是一个正整数N,(1&l ...

  7. gsoap创建webservice服务简单教程

    版权声明:本文为博主原创文章,未经博主允许不得转载. 目录(?)[-] WebServicesoapgsoap 使用gsoap创建webservice服务 下载gsop 准备待导出的服务接口定义文件比 ...

  8. centos6&period;9 改系统语言成中文简体

    1.在root权限下 切换到root下:su root 查看当前语言环境:locale -a  (注意中间有空格) 如果看到 zh_CN.UTF-8(这个是中文简体)说明你的系统支持中文语言 2.编辑 ...

  9. TortoiseGit 安装

    1.TortoiseGit 下载安装 2.语言包下载安装 3.配置语言 一 TortoiseGit 下载安装 官网下载地址:https://download.tortoisegit.org/tgit/ ...

  10. 可见参数和增强for以及自动拆装箱

    可变参数:定义方法的时候不知道该定义多少个参数格式: 修饰符 返回值类型 方法名(数据类型… 变量名){ } 注意: 这里的变量其实是一个数组如果一个方法有可变参数,并且有多个参数,那么,可变参数肯定 ...