基本形状转换那些事,作用域链与闭包

2019-10-04 23:14栏目:人才招聘
TAG:

前端基础进阶(9):详解面向对象、构造函数、原型与原型链

2017/04/02 · JavaScript · 1 评论 · 原型, 原型链, 构造函数, 面向对象

原文出处: 波同学   

图片 1

.

如果要我总结一下学习前端以来我遇到了哪些瓶颈,那么面向对象一定是第一个毫不犹豫想到的。尽管我现在对于面向对象有了一些的了解,但是当初的那种似懂非懂的痛苦,依然历历在目。

为了帮助大家能够更加直观的学习和了解面向对象,我会用尽量简单易懂的描述来展示面向对象的相关知识。并且也准备了一些实用的例子帮助大家更加快速的掌握面向对象的真谛。

  • jQuery的面向对象实现
  • 封装拖拽
  • 简易版运动框架封装

这可能会花一点时间,但是却值得期待。所以如果有兴趣的朋友可以来简书和公众号关注我。

而这篇文章主要来聊一聊关于面向对象的一些重要的基本功。

聊聊 SVG 基本形状转换那些事

2017/01/20 · HTML5 · SVG

原文出处: 凹凸实验室   

图片 2

前端基础进阶(六):在chrome开发者工具中观察函数调用栈、作用域链与闭包

2017/02/26 · CSS, 基础技术 · 1 评论 · Chrome, 作用域链, 函数调用栈, 闭包

原文出处: 波同学   

图片 3

配图与本文无关

在前端开发中,有一个非常重要的技能,叫做断点调试

在chrome的开发者工具中,通过断点调试,我们能够非常方便的一步一步的观察JavaScript的执行过程,直观感知函数调用栈,作用域链,变量对象,闭包,this等关键信息的变化。因此,断点调试对于快速定位代码错误,快速了解代码的执行过程有着非常重要的作用,这也是我们前端开发者必不可少的一个高级技能。

当然如果你对JavaScript的这些基础概念[执行上下文,变量对象,闭包,this等]了解还不够的话,想要透彻掌握断点调试可能会有一些困难。但是好在在前面几篇文章,我都对这些概念进行了详细的概述,因此要掌握这个技能,对大家来说,应该是比较轻松的。

为了帮助大家对于this与闭包有更好的了解,也因为上一篇文章里对闭包的定义有一点偏差,因此这篇文章里我就以闭包有关的例子来进行断点调试的学习,以便大家及时纠正。在这里认个错,误导大家了,求轻喷 ~ ~

一、对象的定义

在ECMAScript-262中,对象被定义为“无序属性的集合,其属性可以包含基本值,对象或者函数”

也就是说,在JavaScript中,对象无非就是由一些列无序的key-value对组成。其中value可以是基本值,对象或者函数。

// 这里的person就是一个对象 var person = { name: 'Tom', age: 18, getName: function() {}, parent: {} }

1
2
3
4
5
6
7
// 这里的person就是一个对象
var person = {
    name: 'Tom',
    age: 18,
    getName: function() {},
    parent: {}
}

创建对象

我们可以通过new的方式创建一个对象。

var obj = new Object();

1
var obj = new Object();

也可以通过对象字面量的形式创建一个简单的对象。

var obj = {};

1
var obj = {};

当我们想要给我们创建的简单对象添加方法时,可以这样表示。

// 可以这样 var person = {}; person.name = "TOM"; person.getName = function() { return this.name; } // 也可以这样 var person = { name: "TOM", getName: function() { return this.name; } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 可以这样
var person = {};
person.name = "TOM";
person.getName = function() {
    return this.name;
}
 
// 也可以这样
var person = {
    name: "TOM",
    getName: function() {
        return this.name;
    }
}

访问对象的属性和方法

假如我们有一个简单的对象如下:

var person = { name: 'TOM', age: '20', getName: function() { return this.name } }

1
2
3
4
5
6
7
var person = {
    name: 'TOM',
    age: '20',
    getName: function() {
        return this.name
    }
}

当我们想要访问他的name属性时,可以用如下两种方式访问。

person.name // 或者 person['name']

1
2
3
4
person.name
 
// 或者
person['name']

如果我们想要访问的属性名是一个变量时,常常会使用第二种方式。例如我们要同时访问person的name与age,可以这样写:

['name', 'age'].forEach(function(item) { console.log(person[item]); })

1
2
3
['name', 'age'].forEach(function(item) {
    console.log(person[item]);
})

这种方式一定要重视,记住它以后在我们处理复杂数据的时候会有很大的帮助。

一、前言

前段时间研究 SVG 压缩优化,发现SVG预定义的 rectcircleellipselinepolylinepolygon 六种基本形状可通过path路径转换实现,这样可以在一定程度上减少代码量。不仅如此,我们常用的 SVG Path 动画(路径动画),是以操作path中两个属性值stroke-dasharraystroke-dashoffset来实现,基本形状转换为path路径,有利于实现路径动画。

一、基础概念回顾

函数在被调用执行时,会创建一个当前函数的执行上下文。在该执行上下文的创建阶段,变量对象、作用域链、闭包、this指向会分别被确定。而一个JavaScript程序中一般来说会有多个函数,JavaScript引擎使用函数调用栈来管理这些函数的调用顺序。函数调用栈的调用顺序与栈数据结构一致。

二、工厂模式

使用上面的方式创建对象很简单,但是在很多时候并不能满足我们的需求。就以person对象为例。假如我们在实际开发中,不仅仅需要一个名字叫做TOM的person对象,同时还需要另外一个名为Jake的person对象,虽然他们有很多相似之处,但是我们不得不重复写两次。

var perTom = { name: 'TOM', age: 20, getName: function() { return this.name } }; var perJake = { name: 'Jake', age: 22, getName: function() { return this.name } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var perTom = {
    name: 'TOM',
    age: 20,
    getName: function() {
        return this.name
    }
};
 
var perJake = {
    name: 'Jake',
    age: 22,
    getName: function() {
        return this.name
    }
}

很显然这并不是合理的方式,当相似对象太多时,大家都会崩溃掉。

我们可以使用工厂模式的方式解决这个问题。顾名思义,工厂模式就是我们提供一个模子,然后通过这个模子复制出我们需要的对象。我们需要多少个,就复制多少个。

var createPerson = function(name, age) { // 声明一个中间对象,该对象就是工厂模式的模子 var o = new Object(); // 依次添加我们需要的属性与方法 o.name = name; o.age = age; o.getName = function() { return this.name; } return o; } // 创建两个实例 var perTom = createPerson('TOM', 20); var PerJake = createPerson('Jake', 22);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var createPerson = function(name, age) {
 
    // 声明一个中间对象,该对象就是工厂模式的模子
    var o = new Object();
 
    // 依次添加我们需要的属性与方法
    o.name = name;
    o.age = age;
    o.getName = function() {
        return this.name;
    }
 
    return o;
}
 
// 创建两个实例
var perTom = createPerson('TOM', 20);
var PerJake = createPerson('Jake', 22);

相信上面的代码并不难理解,也不用把工厂模式看得太过高大上。很显然,工厂模式帮助我们解决了重复代码上的麻烦,让我们可以写很少的代码,就能够创建很多个person对象。但是这里还有两个麻烦,需要我们注意。

第一个麻烦就是这样处理,我们没有办法识别对象实例的类型。使用instanceof可以识别对象的类型,如下例子:

var obj = {}; var foo = function() {} console.log(obj instanceof Object); // true console.log(foo instanceof Function); // true

1
2
3
4
5
var obj = {};
var foo = function() {}
 
console.log(obj instanceof Object);  // true
console.log(foo instanceof Function); // true

因此在工厂模式的基础上,我们需要使用构造函数的方式来解决这个麻烦。

二、SVG基本形状

SVG 提供了rectcircleellipselinepolylinepolygon六种基本形状用于图形绘制,这些形状可以直接用来绘制一些基本的形状,如矩形、椭圆等,而复杂图形的绘制则需要使用 path 路径来实现。

图片 4

二、认识断点调试工具

在尽量新版本的chrome浏览器中(不确定你用的老版本与我的一致),调出chrome浏览器的开发者工具。

浏览器右上角竖着的三点 -> 更多工具 -> 开发者工具 -> Sources

1
浏览器右上角竖着的三点 -> 更多工具 -> 开发者工具 -> Sources

界面如图。

图片 5

断点调试界面

在我的demo中,我把代码放在app.js中,在index.html中引入。我们暂时只需要关注截图中红色箭头的地方。在最左侧上方,有一排图标。我们可以通过使用他们来控制函数的执行顺序。从左到右他们依次是:

  • resume/pause script execution
    恢复/暂停脚本执行
  • step over next function call
    跨过,实际表现是不遇到函数时,执行下一步。遇到函数时,不进入函数直接执行下一步。
  • step into next function call
    跨入,实际表现是不遇到函数时,执行下一步。遇到到函数时,进入函数执行上下文。
  • step out of current function
    跳出当前函数
  • deactivate breakpoints
    停用断点
  • don‘t pause on exceptions
    不暂停异常捕获

其中跨过,跨入,跳出是我使用最多的三个操作。

上图左侧第二个红色箭头指向的是函数调用栈(call Stack),这里会显示代码执行过程中,调用栈的变化。

左侧第三个红色箭头指向的是作用域链(Scope),这里会显示当前函数的作用域链。其中Local表示当前的局部变量对象,Closure表示当前作用域链中的闭包。借助此处的作用域链展示,我们可以很直观的判断出一个例子中,到底谁是闭包,对于闭包的深入了解具有非常重要的帮助作用。

三、构造函数

在JavaScript中,new关键字可以让一个函数变得与众不同。通过下面的例子,我们来一探new关键字的神奇之处。

function demo() { console.log(this); } demo(); // window new demo(); // demo

1
2
3
4
5
6
function demo() {
    console.log(this);
}
 
demo();  // window
new demo();  // demo

为了能够直观的感受他们不同,建议大家动手实践观察一下。很显然,使用new之后,函数内部发生了一些变化,让this指向改变。那么new关键字到底做了什么事情呢。嗯,其实我之前在文章里用文字大概表达了一下new到底干了什么,但是一些同学好奇心很足,总期望用代码实现一下,我就大概以我的理解来表达一下吧。

// 先一本正经的创建一个构造函数,其实该函数与普通函数并无区别 var Person = function(name, age) { this.name = name; this.age = age; this.getName = function() { return this.name; } } // 将构造函数以参数形式传入 function New(func) { // 声明一个中间对象,该对象为最终返回的实例 var res = {}; if (func.prototype !== null) { // 将实例的原型指向构造函数的原型 res.__proto__ = func.prototype; } // ret为构造函数执行的结果,这里通过apply,将构造函数内部的this指向修改为指向res,即为实例对象 var ret = func.apply(res, Array.prototype.slice.call(arguments, 1)); // 当我们在构造函数中明确指定了返回对象时,那么new的执行结果就是该返回对象 if ((typeof ret === "object" || typeof ret === "function") && ret !== null) { return ret; } // 如果没有明确指定返回对象,则默认返回res,这个res就是实例对象 return res; } // 通过new声明创建实例,这里的p1,实际接收的正是new中返回的res var p1 = New(Person, 'tom', 20); console.log(p1.getName()); // 当然,这里也可以判断出实例的类型了 console.log(p1 instanceof Person); // true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 先一本正经的创建一个构造函数,其实该函数与普通函数并无区别
var Person = function(name, age) {
    this.name = name;
    this.age = age;
    this.getName = function() {
        return this.name;
    }
}
 
// 将构造函数以参数形式传入
function New(func) {
 
    // 声明一个中间对象,该对象为最终返回的实例
    var res = {};
    if (func.prototype !== null) {
 
        // 将实例的原型指向构造函数的原型
        res.__proto__ = func.prototype;
    }
 
    // ret为构造函数执行的结果,这里通过apply,将构造函数内部的this指向修改为指向res,即为实例对象
    var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
 
    // 当我们在构造函数中明确指定了返回对象时,那么new的执行结果就是该返回对象
    if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
        return ret;
    }
 
    // 如果没有明确指定返回对象,则默认返回res,这个res就是实例对象
    return res;
}
 
// 通过new声明创建实例,这里的p1,实际接收的正是new中返回的res
var p1 = New(Person, 'tom', 20);
console.log(p1.getName());
 
// 当然,这里也可以判断出实例的类型了
console.log(p1 instanceof Person); // true

JavaScript内部再通过其他的一些特殊处理,将var p1 = New(Person, 'tom', 20); 等效于var p1 = new Person('tom', 20);。就是我们认识的new关键字了。具体怎么处理的,我也不知道,别刨根问底了,一直回答下去太难 – -!

老实讲,你可能很难在其他地方看到有如此明确的告诉你new关键字到底对构造函数干了什么的文章了。理解了这段代码,你对JavaScript的理解又比别人深刻了一分,所以,一本正经厚颜无耻求个赞可好?

当然,很多朋友由于对于前面几篇文章的知识理解不够到位,会对new的实现表示非常困惑。但是老实讲,如果你读了我的前面几篇文章,一定会对这里new的实现有似曾相识的感觉。而且我这里已经尽力做了详细的注解,剩下的只能靠你自己了。

但是只要你花点时间,理解了他的原理,那么困扰了无数人的构造函数中this到底指向谁就变得非常简单了。

所以,为了能够判断实例与对象的关系,我们就使用构造函数来搞定。

var Person = function(name, age) { this.name = name; this.age = age; this.getName = function() { return this.name; } } var p1 = new Person('Ness', 20); console.log(p1.getName()); // Ness console.log(p1 instanceof Person); // true

1
2
3
4
5
6
7
8
9
10
11
12
var Person = function(name, age) {
    this.name = name;
    this.age = age;
    this.getName = function() {
        return this.name;
    }
}
 
var p1 = new Person('Ness', 20);
console.log(p1.getName());  // Ness
 
console.log(p1 instanceof Person); // true

关于构造函数,如果你暂时不能够理解new的具体实现,就先记住下面这几个结论吧。

  • 与普通函数相比,构造函数并没有任何特别的地方,首字母大写只是我们约定的小规定,用于区分普通函数;
  • new关键字让构造函数具有了与普通函数不同的许多特点,而new的过程中,执行了如下过程:
    1. 声明一个中间对象;
    2. 将该中间对象的原型指向构造函数的原型;
    3. 将构造函数的this,指向该中间对象;
    4. 返回该中间对象,即返回实例对象。

1.rect 矩形

XHTML

<rect x="10" y="10" width="30" height="30"/> <rect x="60" y="10" rx="10" ry="10" width="30" height="30"/>

1
2
<rect x="10" y="10" width="30" height="30"/>
<rect x="60" y="10" rx="10" ry="10" width="30" height="30"/>

SVG中rect元素用于绘制矩形、圆角矩形,含有6个基本属性用于控制矩形的形状以及坐标,具体如下:

x 矩形左上角x位置, 默认值为 0 y 矩形左上角y位置, 默认值为 0 width 矩形的宽度, 不能为负值否则报错, 0 值不绘制 height 矩形的高度, 不能为负值否则报错, 0 值不绘制 rx 圆角x方向半径, 不能为负值否则报错 ry 圆角y方向半径, 不能为负值否则报错

1
2
3
4
5
6
x 矩形左上角x位置, 默认值为 0
y 矩形左上角y位置, 默认值为 0
width 矩形的宽度, 不能为负值否则报错, 0 值不绘制
height 矩形的高度, 不能为负值否则报错, 0 值不绘制
rx 圆角x方向半径, 不能为负值否则报错
ry 圆角y方向半径, 不能为负值否则报错

这里需要注意,rxry 的还有如下规则:

  • rxry 都没有设置, 则 rx = 0 ry = 0
  • rxry 有一个值为0, 则相当于 rx = 0 ry = 0,圆角无效
  • rxry 有一个被设置, 则全部取这个被设置的值
  • rx 的最大值为 width 的一半, ry 的最大值为 height 的一半
JavaScript

rx = rx || ry || 0; ry = ry || rx || 0;   rx = rx &gt; width / 2 ?
width / 2 : rx; ry = ry &gt; height / 2 ? height / 2 : ry;   if(0
=== rx || 0 === ry){ rx = 0, ry = 0;
//圆角不生效,等同于,rx,ry都为0 }

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5b8f49eccc27a188181481-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f49eccc27a188181481-2">
2
</div>
<div class="crayon-num" data-line="crayon-5b8f49eccc27a188181481-3">
3
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f49eccc27a188181481-4">
4
</div>
<div class="crayon-num" data-line="crayon-5b8f49eccc27a188181481-5">
5
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f49eccc27a188181481-6">
6
</div>
<div class="crayon-num" data-line="crayon-5b8f49eccc27a188181481-7">
7
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f49eccc27a188181481-8">
8
</div>
<div class="crayon-num" data-line="crayon-5b8f49eccc27a188181481-9">
9
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f49eccc27a188181481-10">
10
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5b8f49eccc27a188181481-1" class="crayon-line">
rx = rx || ry || 0;
</div>
<div id="crayon-5b8f49eccc27a188181481-2" class="crayon-line crayon-striped-line">
ry = ry || rx || 0;
</div>
<div id="crayon-5b8f49eccc27a188181481-3" class="crayon-line">
 
</div>
<div id="crayon-5b8f49eccc27a188181481-4" class="crayon-line crayon-striped-line">
rx = rx &gt; width / 2 ? width / 2 : rx;
</div>
<div id="crayon-5b8f49eccc27a188181481-5" class="crayon-line">
ry = ry &gt; height / 2 ? height / 2 : ry;
</div>
<div id="crayon-5b8f49eccc27a188181481-6" class="crayon-line crayon-striped-line">
 
</div>
<div id="crayon-5b8f49eccc27a188181481-7" class="crayon-line">
if(0 === rx || 0 === ry){
</div>
<div id="crayon-5b8f49eccc27a188181481-8" class="crayon-line crayon-striped-line">
rx = 0,
</div>
<div id="crayon-5b8f49eccc27a188181481-9" class="crayon-line">
ry = 0; //圆角不生效,等同于,rx,ry都为0
</div>
<div id="crayon-5b8f49eccc27a188181481-10" class="crayon-line crayon-striped-line">
}
</div>
</div></td>
</tr>
</tbody>
</table>
三、断点设置

在显示代码行数的地方点击,即可设置一个断点。断点设置有以下几个特点:

  • 在单独的变量声明(如果没有赋值),函数声明的那一行,无法设置断点。
  • 设置断点后刷新页面,JavaScript代码会执行到断点位置处暂停执行,然后我们就可以使用上边介绍过的几个操作开始调试了。
  • 当你设置多个断点时,chrome工具会自动判断从最早执行的那个断点开始执行,因此我一般都是设置一个断点就行了。

四、原型

虽然构造函数解决了判断实例类型的问题,但是,说到底,还是一个对象的复制过程。跟工厂模式颇有相似之处。也就是说,当我们声明了100个person对象,那么就有100个getName方法被重新生成。

这里的每一个getName方法实现的功能其实是一模一样的,但是由于分别属于不同的实例,就不得不一直不停的为getName分配空间。这就是工厂模式存在的第二个麻烦。

显然这是不合理的。我们期望的是,既然都是实现同一个功能,那么能不能就让每一个实例对象都访问同一个方法?

当然能,这就是原型对象要帮我们解决的问题了。

我们创建的每一个函数,都可以有一个prototype属性,该属性指向一个对象。这个对象,就是我们这里说的原型。

当我们在创建对象时,可以根据自己的需求,选择性的将一些属性和方法通过prototype属性,挂载在原型对象上。而每一个new出来的实例,都有一个__proto__属性,该属性指向构造函数的原型对象,通过这个属性,让实例对象也能够访问原型对象上的方法。因此,当所有的实例都能够通过__proto__访问到原型对象时,原型对象的方法与属性就变成了共有方法与属性。

我们通过一个简单的例子与图示,来了解构造函数,实例与原型三者之间的关系。

由于每个函数都可以是构造函数,每个对象都可以是原型对象,因此如果在理解原型之初就想的太多太复杂的话,反而会阻碍你的理解,这里我们要学会先简化它们。就单纯的剖析这三者的关系。

// 声明构造函数 function Person(name, age) { this.name = name; this.age = age; } // 通过prototye属性,将方法挂载到原型对象上 Person.prototype.getName = function() { return this.name; } var p1 = new Person('tim', 10); var p2 = new Person('jak', 22); console.log(p1.getName === p2.getName); // true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 声明构造函数
function Person(name, age) {
    this.name = name;
    this.age = age;
}
 
// 通过prototye属性,将方法挂载到原型对象上
Person.prototype.getName = function() {
    return this.name;
}
 
var p1 = new Person('tim', 10);
var p2 = new Person('jak', 22);
console.log(p1.getName === p2.getName); // true

图片 6

图示

通过图示我们可以看出,构造函数的prototype与所有实例对象的__proto__都指向原型对象。而原型对象的constructor指向构造函数。

除此之外,还可以从图中看出,实例对象实际上对前面我们所说的中间对象的复制,而中间对象中的属性与方法都在构造函数中添加。于是根据构造函数与原型的特性,我们就可以将在构造函数中,通过this声明的属性与方法称为私有变量与方法,它们被当前被某一个实例对象所独有。而通过原型声明的属性与方法,我们可以称之为共有属性与方法,它们可以被所有的实例对象访问。

当我们访问实例对象中的属性或者方法时,会优先访问实例对象自身的属性和方法。

function Person(name, age) { this.name = name; this.age = age; this.getName = function() { console.log('this is constructor.'); } } Person.prototype.getName = function() { return this.name; } var p1 = new Person('tim', 10); p1.getName(); // this is constructor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.getName = function() {
        console.log('this is constructor.');
    }
}
 
Person.prototype.getName = function() {
    return this.name;
}
 
var p1 = new Person('tim', 10);
 
p1.getName(); // this is constructor.

在这个例子中,我们同时在原型与构造函数中都声明了一个getName函数,运行代码的结果表示原型中的访问并没有被访问。

我们还可以通过in来判断,一个对象是否拥有某一个属性/方法,无论是该属性/方法存在与实例对象还是原型对象。

function Person(name, age) { this.name = name; this.age = age; } Person.prototype.getName = function() { return this.name; } var p1 = new Person('tim', 10); console.log('name' in p1); // true

1
2
3
4
5
6
7
8
9
10
11
12
function Person(name, age) {
    this.name = name;
    this.age = age;
}
 
Person.prototype.getName = function() {
    return this.name;
}
 
var p1 = new Person('tim', 10);
 
console.log('name' in p1); // true

in的这种特性最常用的场景之一,就是判断当前页面是否在移动端打开。

isMobile = 'ontouchstart' in document; // 很多人喜欢用浏览器UA的方式来判断,但并不是很好的方式

1
2
3
isMobile = 'ontouchstart' in document;
 
// 很多人喜欢用浏览器UA的方式来判断,但并不是很好的方式

更简单的原型写法

根据前面例子的写法,如果我们要在原型上添加更多的方法,可以这样写:

function Person() {} Person.prototype.getName = function() {} Person.prototype.getAge = function() {} Person.prototype.sayHello = function() {} ... ...

1
2
3
4
5
6
function Person() {}
 
Person.prototype.getName = function() {}
Person.prototype.getAge = function() {}
Person.prototype.sayHello = function() {}
... ...

除此之外,我还可以使用更为简单的写法。

function Person() {} Person.prototype = { constructor: Person, getName: function() {}, getAge: function() {}, sayHello: function() {} }

1
2
3
4
5
6
7
8
function Person() {}
 
Person.prototype = {
    constructor: Person,
    getName: function() {},
    getAge: function() {},
    sayHello: function() {}
}

这种字面量的写法看上去简单很多,但是有一个需要特别注意的地方。Person.prototype = {}实际上是重新创建了一个{}对象并赋值给Person.prototype,这里的{}并不是最初的那个原型对象。因此它里面并不包含constructor属性。为了保证正确性,我们必须在新创建的{}对象中显示的设置constructor的指向。即上面的constructor: Person

2.circle 圆形

XHTML

<circle cx="100" cy="100" r="50" fill="#fff"></circle>

1
<circle cx="100" cy="100" r="50" fill="#fff"></circle>

SVG中circle元素用于绘制圆形,含有3个基本属性用于控制圆形的坐标以及半径,具体如下:

r 半径 cx 圆心x位置, 默认为 0 cy 圆心y位置, 默认为 0

1
2
3
r 半径
cx 圆心x位置, 默认为 0
cy 圆心y位置, 默认为 0
四、实例

接下来,我们借助一些实例,来使用断点调试工具,看一看,我们的demo函数,在执行过程中的具体表现。

JavaScript

// demo01 var fn; function foo() { var a = 2; function baz() { console.log( a ); } fn = baz; } function bar() { fn(); } foo(); bar(); // 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// demo01
 
var fn;
function foo() {
    var a = 2;
    function baz() {
        console.log( a );
    }
    fn = baz;
}
function bar() {
    fn();
}
 
foo();
bar(); // 2

在向下阅读之前,我们可以停下来思考一下,这个例子中,谁是闭包?

这是来自《你不知道的js》中的一个例子。由于在使用断点调试过程中,发现chrome浏览器理解的闭包与该例子中所理解的闭包不太一致,因此专门挑出来,供大家参考。我个人更加倾向于chrome中的理解。

  • 第一步:设置断点,然后刷新页面。

图片 7

设置断点

  • 第二步:点击上图红色箭头指向的按钮(step into),该按钮的作用会根据代码执行顺序,一步一步向下执行。在点击的过程中,我们要注意观察下方call stack 与 scope的变化,以及函数执行位置的变化。

一步一步执行,当函数执行到上例子中

图片 8

baz函数被调用执行,foo形成了闭包

我们可以看到,在chrome工具的理解中,由于在foo内部声明的baz函数在调用时访问了它的变量a,因此foo成为了闭包。这好像和我们学习到的知识不太一样。我们来看看在《你不知道的js》这本书中的例子中的理解。

图片 9

你不知道的js中的例子

书中的注释可以明显的看出,作者认为fn为闭包。即baz,这和chrome工具中明显是不一样的。

而在备受大家推崇的《JavaScript高级编程》一书中,是这样定义闭包。

图片 10

JavaScript高级编程中闭包的定义

图片 11

书中作者将自己理解的闭包与包含函数所区分

这里chrome中理解的闭包,与我所阅读的这几本书中的理解的闭包不一样。具体这里我先不下结论,但是我心中更加偏向于相信chrome浏览器。

我们修改一下demo01中的例子,来看看一个非常有意思的变化。

JavaScript

// demo02 var fn; var m = 20; function foo() { var a = 2; function baz(a) { console.log(a); } fn = baz; } function bar() { fn(m); } foo(); bar(); // 20

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// demo02
var fn;
var m = 20;
function foo() {
    var a = 2;
    function baz(a) {
        console.log(a);
    }
    fn = baz;
}
function bar() {
    fn(m);
}
 
foo();
bar(); // 20

这个例子在demo01的基础上,我在baz函数中传入一个参数,并打印出来。在调用时,我将全局的变量m传入。输出结果变为20。在使用断点调试看看作用域链。

图片 12

闭包没了,作用域链中没有包含foo了。

是不是结果有点意外,闭包没了,作用域链中没有包含foo了。我靠,跟我们理解的好像又有点不一样。所以通过这个对比,我们可以确定闭包的形成需要两个条件。

  • 在函数内部创建新的函数;
  • 新的函数在执行时,访问了函数的变量对象;

还有更有意思的。

我们继续来看看一个例子。

JavaScript

// demo03 function foo() { var a = 2; return function bar() { var b = 9; return function fn() { console.log(a); } } } var bar = foo(); var fn = bar(); fn();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// demo03
 
function foo() {
    var a = 2;
 
    return function bar() {
        var b = 9;
 
        return function fn() {
            console.log(a);
        }
    }
}
 
var bar = foo();
var fn = bar();
fn();

在这个例子中,fn只访问了foo中的a变量,因此它的闭包只有foo。

图片 13

闭包只有foo

修改一下demo03,我们在fn中也访问bar中b变量试试看。

JavaScript

// demo04 function foo() { var a = 2; return function bar() { var b = 9; return function fn() { console.log(a, b); } } } var bar = foo(); var fn = bar(); fn();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// demo04
 
function foo() {
    var a = 2;
 
    return function bar() {
        var b = 9;
 
        return function fn() {
            console.log(a, b);
        }
    }
}
 
var bar = foo();
var fn = bar();
fn();

图片 14

这个时候闭包变成了两个

这个时候,闭包变成了两个。分别是bar,foo。

我们知道,闭包在模块中的应用非常重要。因此,我们来一个模块的例子,也用断点工具来观察一下。

JavaScript

// demo05 (function() { var a = 10; var b = 20; var test = { m: 20, add: function(x) { return a + x; }, sum: function() { return a + b + this.m; }, mark: function(k, j) { return k + j; } } window.test = test; })(); test.add(100); test.sum(); test.mark(); var _mark = test.mark(); _mark();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// demo05
(function() {
 
    var a = 10;
    var b = 20;
 
    var test = {
        m: 20,
        add: function(x) {
            return a + x;
        },
        sum: function() {
            return a + b + this.m;
        },
        mark: function(k, j) {
            return k + j;
        }
    }
 
    window.test = test;
 
})();
 
test.add(100);
test.sum();
test.mark();
 
var _mark = test.mark();
_mark();

图片 15

add执行时,闭包为外层的自执行函数,this指向test

图片 16

sum执行时,同上

图片 17

mark执行时,闭包为外层的自执行函数,this指向test

图片 18

_mark执行时,闭包为外层的自执行函数,this指向window

注意:这里的this指向显示为Object或者Window,大写开头,他们表示的是实例的构造函数,实际上this是指向的具体实例

上面的所有调用,最少都访问了自执行函数中的test变量,因此都能形成闭包。即使mark方法没有访问私有变量a,b。

我们还可以结合点断调试的方式,来理解那些困扰我们很久的this指向。随时观察this的指向,在实际开发调试中非常有用。

JavaScript

// demo06 var a = 10; var obj = { a: 20 } function fn () { console.log(this.a); } fn.call(obj); // 20

1
2
3
4
5
6
7
8
9
10
11
12
// demo06
 
var a = 10;
var obj = {
    a: 20
}
 
function fn () {
    console.log(this.a);
}
 
fn.call(obj); // 20

图片 19

this指向obj

更多的例子,大家可以自行尝试,总之,学会了使用断点调试之后,我们就能够很轻松的了解一段代码的执行过程了。这对快速定位错误,快速了解他人的代码都有非常巨大的帮助。大家一定要动手实践,把它给学会。

最后,根据以上的摸索情况,再次总结一下闭包:

  • 闭包是在函数被调用执行的时候才被确认创建的。
  • 闭包的形成,与作用域链的访问顺序有直接关系。
  • 只有内部函数访问了上层作用域链中的变量对象时,才会形成闭包,因此,我们可以利用闭包来访问函数内部的变量。
  • chrome中理解的闭包,与《你不知道的js》与《JavaScript高级编程》中的闭包理解有很大不同,我个人更加倾向于相信chrome。这里就不妄下结论了,大家可以根据我的思路,探索后自行确认。在之前一篇文中我根据从书中学到的下了定义,应该是错了,目前已经修改,对不起大家了。

大家也可以根据我提供的这个方法,对其他的例子进行更多的测试,如果发现我的结论有不对的地方,欢迎指出,大家相互学习进步,谢谢大家。

1 赞 2 收藏 1 评论

图片 20

四、原型链

原型对象其实也是普通的对象。几乎所有的对象都可能是原型对象,也可能是实例对象,而且还可以同时是原型对象与实例对象。这样的一个对象,正是构成原型链的一个节点。因此理解了原型,那么原型链并不是一个多么复杂的概念。

我们知道所有的函数都有一个叫做toString的方法。那么这个方法到底是在哪里的呢?

先随意声明一个函数:

function foo() {}

1
function foo() {}

那么我们可以用如下的图来表示这个函数的原型链。

图片 21

原型链

其中foo是Function对象的实例。而Function的原型对象同时又是Object的实例。这样就构成了一条原型链。原型链的访问,其实跟作用域链有很大的相似之处,他们都是一次单向的查找过程。因此实例对象能够通过原型链,访问到处于原型链上对象的所有属性与方法。这也是foo最终能够访问到处于Object原型对象上的toString方法的原因。

基于原型链的特性,我们可以很轻松的实现继承

3.ellipse 椭圆

XHTML

<ellipse cx="75" cy="75" rx="20" ry="5"/>

1
<ellipse cx="75" cy="75" rx="20" ry="5"/>

SVG中ellipse元素用于绘制椭圆,是circle元素更通用的形式,含有4个基本属性用于控制椭圆的形状以及坐标,具体如下:

rx 椭圆x半径 ry 椭圆y半径 cx 圆心x位置, 默认为 0 cy 圆心y位置, 默认为 0

1
2
3
4
rx 椭圆x半径
ry 椭圆y半径
cx 圆心x位置, 默认为 0
cy 圆心y位置, 默认为 0

版权声明:本文由ag真人发布于人才招聘,转载请注明出处:基本形状转换那些事,作用域链与闭包