主题
原型(Day9 1/3)
本页对应 Day09_JavaScript,共 7 段视频,约 1 小时 14 分。
学习目标
- 能够根据调用位置判断函数中
this的指向,并独立分析含全局变量、构造函数实例化、对象方法调用及嵌套函数的this题目输出结果。 - 能够使用递归函数遍历任意层级数组,将多维数组扁平化为一维数组,并说明递归终止条件与执行过程。
- 能够说明原型的概念与作用,解释对象为何可以使用原型上的属性,并区分“继承”与“借用原型属性”的表述。
- 能够使用
对象.__proto__和对象.constructor.prototype获取对象原型,并验证同一构造函数创建的不同对象共享同一原型。 - 能够梳理对象、构造函数、原型三者之间的关系,准确说明对象与构造函数、对象与原型、构造函数与原型之间的对应关系和数量关系。
- 能够解释数组方法等属性为何设计在原型上,并结合实例说明将公共属性和方法放在原型上的意义。
知识点
一、对象回顾:把 object 当成“最基础的对象”来记
为什么需要:后面讲构造函数、this、原型,都会反复回到对象。先把 object 对象的创建、读写、遍历、判断属性梳理清楚,后面才不容易乱。
怎么想:object 对象只具备对象最基本的特征;数组、函数都是在它基础上增加了自己的特点。所以记 object 时可以和数组对照着记:怎么创建、怎么读写属性、怎么遍历、怎么判断属性是否存在。
怎么做:创建可用字面量、Object 函数、Object 构造函数;读写属性用点语法或中括号;遍历属性用 for...in;判断属性是否存在用 in 运算符;删除属性用 delete。
易错点:for...in 遍历的是可枚举属性;in 判断的是“对象中是否存在某个属性”;delete 删除的是属性,不是变量。不要把“对象属性”和“数组元素”完全混为一谈,但方法、属性本质上都可以统一理解。
二、构造函数与实例:函数还是那个函数,看你怎么用
为什么需要:当我们要批量创建“结构相似”的对象时,一次次写字面量很麻烦。构造函数就是对象的描述或设计,用它可以产生对象。
怎么想:构造函数本质还是函数,和普通函数没有根本差别;用 new 调用时,它才是构造函数。对象是构造函数的实例;一个构造函数可以产生无数个对象,一个对象只能有一个构造函数。系统内置的数据类型也可以理解为系统内置的构造函数,比如数组的构造函数是 Array。
怎么做:在构造函数里用 this 给新对象添加属性;用 new 实例化。要记住:new 一下,构造函数里的代码会执行。返回值要分情况:没有 return,或 return 的是原始类型,实例化结果仍是新创建的对象;如果 return 的是对象类型(数组、函数、对象等),实例化结果就变成这个返回的对象。
易错点:不要以为构造函数里写了 return 就一定会覆盖新对象;只有返回对象类型才会覆盖。判断实例可用 instanceof;获取对象的构造函数可用 constructor。看到 new 就想“现在把它当构造函数用”,看到普通调用就想“现在它是函数”。
三、this 的取值:看调用点前面的“点”
为什么需要:同一个函数可能被不同对象调用,函数内部需要知道“当前是谁在调用我”,所以要有 this。
怎么想:this 是系统内置的指针变量,不同位置取值不同。函数外(全局下)this 是 window;构造函数中 this 是本次 new 出来的实例;普通函数中 this 指向调用它的对象。判断规则不要看函数定义在哪里,而要看调用那一刻:前面有点,点前面是谁,this 就是谁;没有点,就是 window。如果函数套函数,要看最近包裹 this 的那个函数。
怎么做:分析题目时先注释后面的代码,逐条执行。比如 func("tom", 19) 是普通调用,前面没有点,this 是 window,所以 this.name = "tom"、this.age = 19 实际改的是全局变量;后面再输出 name、age 就能看到变化。new func(...) 时 this 是新实例;o.getInfo() 时 this 是 o;fn() 时 this 又是 window。转写稿中还演示了 new O.getInfo():新对象没有添加过 name、age,所以输出 undefined。
易错点:不要凭“函数写在谁里面”判断 this;要看调用点。A.B.method() 中 this 是 B,不是 A。没有点就是 window。构造函数每次 new 都会产生新对象,本次 this 只对应本次实例,不会和上一次冲突。
四、递归实现数组扁平化:适合“不整齐的分叉”
为什么需要:数组嵌套层数不固定。写两层 for 循环只能解决两层,数据一变就不知道写几层,所以要用递归。
怎么想:把嵌套数组想成一棵树,分叉不整齐。遇到数组就继续递归遍历,遇到非数组就放进结果数组。递归要有终止条件:当元素不再是数组时,就不继续往下递归。
怎么做:转写稿中的思路是先准备结果数组,再写递归函数:
js
let newNames = [];
function flatArray(arr) {
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
flatArray(arr[i]);
} else {
newNames.push(arr[i]);
}
}
}
flatArray(names);
console.log(newNames);调试时先发现少写 s 报错,补上后仍发现只遍历一层不够;这说明固定层数不行。后来又发现函数创建了却没调用,补上 flatArray(names) 才得到扁平化结果。
易错点:递归函数写完必须有一次“启动调用”;结果数组如果放在函数外,要先想清楚共享问题。转写稿也说明当前写法不是最优,最好在函数最后 return 扁平化数组,但先理解这个版本。
五、原型:对象可以使用原型上的属性
为什么需要:数组有很多方法,如 push、pop、shift、sort,如果每个数组对象都复制一份,浪费内存。JS 用原型来共享这些属性和方法,也用原型模拟继承。
怎么想:每个对象都有原型,原型本身也是对象,所以原型也有原型,形成原型链。对象可以使用原型上的属性,但属性仍属于原型,不是复制给了对象。数组本身通常只放元素和 length,push、pop 等方法放在 Array.prototype 上。
怎么做:在调试工具中展开数组,可以看到 [[Prototype]],继续展开能看到 push、pop、shift、sort 等。获取对象原型有两种常见方式:arr.__proto__ 和 arr.constructor.prototype。转写稿中验证了二者相等:arr.__proto__ === arr.constructor.prototype 为 true。相同构造函数的对象,原型相同;一个原型可以被多个对象共享。
易错点:constructor.prototype 获取的是“对象的原型”,不是构造函数自己的原型;构造函数本身也是对象,也有自己的原型,所以这里最容易绕。__proto__ 常被称为“隐式原型”,constructor.prototype 常被称为“显式原型”,但名字是历史交流习惯;真正代表对象原型的是 __proto__ 指向的那个对象。记住:原型就是原型,不分显隐;所谓显隐只是获取方式的历史说法。
js
1. Object 对象
1.1 创建方式
1.2 读写对象的属性
1.3 遍历对象的属性
1.4 in 运算符
1.5 delete 运算符
2. 构造函数
2.1 构造函数的概念 数据类型
instanceof
.constructor
2.2 构造函数和对象的关系
2.3 自定义构造函数
2.4 实例化
构造函数中的返回对实例化结果的影响
2.5 构造函数和函数
2.6 原始类型数据的对象特性
3. this
3.1 this 是系统自动创建的只读变量,不同地方得到的值不同
3.2 this 的取值
① 在函数外,全局下
② 在构造函数中 this的值是该构造函数实例化产生的对象
③ 在函数(方法)中
谁掉用该函数,函数中的this就指向谁
3.3 windowObject 对象:先按“最原始的对象”记
为什么需要:数组、函数等都有自己的特点,而 Object 对象只具备对象的基本特征,所以先把 Object 的创建、读写、遍历、判断、删除记牢,后面学其他对象才不会乱。怎么想:和数组对着记——数组能遍历,Object 也能遍历;数组有属性,Object 也有属性。怎么做:创建可用直接量、Object()、new Object();读写用点或中括号;遍历用 for...in;in 判断对象中是否存在某个属性;delete 删除属性。易错点:转写稿里的“印运算符”应纠正为 in,不要把它和 instanceof 混用;for...in 遍历的是属性,不要按数组下标思维硬套。
构造函数:构造函数是对象的“描述”
为什么需要:对象是具体的数据,构造函数是对这一类对象的规划和设计。怎么想:听到“对象是构造函数的实例”,就要知道二者构成“描述—落地”的关系;一个构造函数可以产生无数个对象,一个对象只有一个构造函数。怎么做:系统内置数据类型可理解为系统内置构造函数;用 instanceof 判断对象是不是某个构造函数的实例;用 .constructor 得到对象的构造函数。易错点:转写稿里的“ten of”“coin.Strture”分别对应 instanceof、constructor;本节只要求会读、会判断,不要把构造函数神秘化。
自定义构造函数与实例化:new 会执行函数体
为什么需要:自定义构造函数本质上还是函数,只有用 new 调用时才当构造函数用。怎么想:看到 new,就想“正在把函数当构造函数用”,它会创建新对象,并让构造函数里的 this 指向这个新对象。怎么做:写普通函数,在函数里给 this 加属性;用 new 实例化。返回值的规则要分清:没写 return,或 return 的是原始类型数据,实例化结果仍是新对象;如果 return 的是对象类型数据(数组、函数、对象等),实例化结果就是该返回值。易错点:转写稿反复强调“new 一下,构造函数里的代码也会执行”,不要以为只创建对象不执行代码;return 原始类型和对象类型的结果不同。
构造函数和函数:同一个东西,看怎么用
为什么需要:构造函数和普通函数在代码形态上没有差别,差别在调用方式。怎么想:不要看它“长得像不像构造函数”,要看有没有用 new。怎么做:同一个函数,直接调用时按函数规则分析 this;用 new 调用时按构造函数规则分析 this。易错点:不要因为函数名大写就认定它一定是构造函数;转写稿明确说“东西是一个东西,完全看我们怎么用”。
原始类型数据的对象特性:临时包装,用完即销毁
为什么需要:JS 里 Number、String、Boolean 等原始类型在需要时也能表现对象特性。怎么想:它们不是永久对象,而是需要时临时包装成对象。怎么做:例如给字符串用点语法加属性,本质上先临时包装成 String 对象,属性加到临时对象上;语句执行完临时对象销毁。易错点:下一条语句再读该属性时,会再次包装成新的临时对象,所以读不到上一次加的属性;每次 new 出来的对象不是同一个。
this:先背三种取值,再找调用点
为什么需要:this 是系统自动创建的只读变量,不同地方值不同,判断错就会把对象属性写错。怎么想:函数外全局下,this 是 window;构造函数中,this 是该构造函数实例化产生的对象;函数(方法)中,谁调用该函数,this 就指向谁。怎么做:判断时先找“调用那一刻”,看函数前面有没有点:有点,点前面最近的对象就是调用者;没有点,就是 window。多层嵌套看最近包裹 this 的函数。易错点:转写稿里的“raase”应纠正为 this;不要看函数定义在哪里,要看在哪里被调用。以转写稿题目为例:全局 age=10,直接调用 func("tom",19) 时 this 是 window,所以全局 age 被改成 19;new func("tom",19) 时 this 是新对象;o.getInfo() 中 this 是 o;而 new o.getInfo() 时 this 是另一个新对象,里面没有加过 name/age,所以输出 undefined。
window:全局对象与全局变量
为什么需要:全局下的 this 是 window,很多全局变量其实就是 window 的属性。怎么想:自己创建的全局变量、系统提供的 parseInt、Number、String、Array、Object、Function 等,都可按“全局变量”理解;它们的值可能是函数。怎么做:使用全局变量时可省略 window.;判断无点调用的函数时,直接把调用者视为 window。易错点:不要把 window 当成需要自己创建的对象,浏览器打开时就存在;转写稿中的“死针”应纠正为 String,“耳力/Array”对应 Array。
原型为什么存在:用原型补上“继承”
为什么需要:面向对象通常讲封装、继承、多态,但早期 JavaScript 的对象体系并不完整,所以它长期被称为“基于对象”的语言。作者引入原型,核心目的就是模拟继承:让一个对象可以使用另一个对象上的属性。
怎么想:不要把原型理解成“复制一份属性给对象”。更准确地说,原型是对象旁边的一个“公共属性仓库”,对象自己身上没有的属性,会去原型上找。转写稿里反复强调:对象可以使用原型上的属性,这更像“借用”,不是把属性真的变成自己的。
怎么做:先记住一句话——每个对象都有原型,原型本身也是对象。数组、函数、普通 object 都有原型;字符串、数字、布尔值在需要当对象用时,也会临时包装成对象再访问原型。
易错点:不要把“继承”说成“属性归我了”。它只是能访问;另外原型也有原型,会形成原型链,本节先理解一层,不展开终结位置。
每个对象都有原型,原型本身也是对象
为什么需要:只有先承认“每个对象都有原型”,后面才能解释为什么数组能调用 push、字符串能调用 slice,而这些东西又不在对象自己身上。
怎么想:在控制台输出一个数组,展开调试工具里的 [[Prototype]],会看到 push、pop、shift、slice 等方法。它们不在数组自身,而在数组的原型上。转写稿中的调试过程就是:先输出数组,再展开原型,观察方法列表。
怎么做:可以先用调试工具看,不要急着背。比如:
js
const arr = [1, 2, 3];
console.log(arr);展开后看 [[Prototype]],再找 push、pop、shift、slice。
易错点:调试工具里的 [[Prototype]] 是一种“指向表示”,不是让你写 arr.[[Prototype]] 的属性名。转写稿里专门纠正过:有了中括号还写点,语法不对。
获取原型:proto 与 constructor.prototype
为什么需要:光在控制台看不够,代码里也需要拿到原型对象,才能比较、验证关系。
怎么想:对象自己带有一个 __proto__ 属性,前后各两根下划线,它的值就是该对象的原型。另一个途径是通过构造函数:对象.constructor.prototype。注意,这里获取的是“对象的原型”,不是构造函数自己的原型。
怎么做:
js
const arr = [1, 2, 3];
console.log(arr.__proto__);
console.log(arr.constructor.prototype);
console.log(arr.__proto__ === Array.prototype); // true转写稿中的调试结果是:arr.__proto__ 和 arr.constructor.prototype 指向同一个东西,比较为 true。
易错点:__proto__ 是两根下划线开头、两根下划线结尾;constructor.prototype 只是借构造函数这个途径拿对象原型,不要说成“构造函数的原型”。
隐式原型与显式原型:别被名字绕晕
为什么需要:很多资料会说“隐式原型”和“显式原型”,不说清楚很容易把 __proto__ 和 prototype 混掉。
怎么想:真正代表“这个对象自己的原型”的,是 __proto__ 指向的对象;prototype 是构造函数身上的属性,用来给它的实例提供原型。转写稿里说得很直白:原型就是原型,不分隐式显式;所谓隐式、显式,本质上是获取方式的历史叫法。
怎么做:交流时听到“隐式原型”,你就盯住 __proto__;听到“显式原型”,知道对方多指 构造函数.prototype。判断谁才是对象原型时,用 arr.__proto__ === Array.prototype 验证。
易错点:不要把 Array.prototype 说成“数组自己的原型”而混淆主体。数组实例的原型是 arr.__proto__,它等于 Array.prototype;但 Array.prototype 是构造函数提供给实例的原型对象。
对象、构造函数、原型三者的关系
为什么需要:原型和构造函数一掺和,关系就变多,必须用数量关系和相等关系固定下来。
怎么想:构造函数描述对象,对象是构造函数的实例;一个构造函数可以产生无数对象,一个对象只有一个构造函数。每个对象都有原型,一个对象只有一个原型;反过来,一个原型可以作为多个对象的原型。
怎么做:
js
const a1 = [];
const a2 = [];
console.log(a1.__proto__ === a2.__proto__); // true
console.log(a1.__proto__ === Array.prototype); // true转写稿的推导是:相同构造函数的对象,原型相同;相同数据类型的对象,原型也相同。两个数组内容不同、不是同一个对象,但原型相同。
易错点:不要把“内容相同”当成“同一个对象”;也不要把“原型相同”当成“对象相同”。
数组方法为什么放在原型上
为什么需要:如果每个数组对象都各自保存一份 push、pop、slice,会浪费内存,也不利于统一维护。
怎么想:数组自身主要保存元素、length 等自身属性;公共方法放到 Array.prototype,所有数组实例共享。转写稿中用调试工具展开数组原型,看到大量方法,说明这些方法是给数组实例“使用”的。
怎么做:
js
const arr = [1, 2, 3];
console.log(arr.hasOwnProperty('push')); // false
console.log(typeof arr.push); // functionhasOwnProperty 为 false,说明 push 不是数组自身属性;但 arr.push 能调用,说明它来自原型。
易错点:不要因为 arr.push 能用,就说 push 是 arr 自己的属性;判断自身属性要用 hasOwnProperty,判断可用性直接访问即可。
课堂画面






代码示例
示例 1:this 题目 01(构造函数 / 普通函数调用 / window)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>this 题目 01</title>
</head>
<body>
<script>
var age = 10; // 全局变量,本质就是 window.age
function Func(name, age) {
this.name = name;
this.age = age;
this.getInfo = function () {
console.log(this.name, this.age);
};
console.log(this.name); // 看看 this 是谁
}
// ① 普通函数调用:前面没有点,this 是 window
Func('tom', 19); // 输出 tom
// 此时 window.name = 'tom',window.age = 19(把全局 age 从 10 改成了 19)
// ② 实例化:this 是新建的对象
new Func(); // 输出 undefined(没传参)
// ③ 接收实例
var o = new Func('tom', 19);// 输出 tom
o.getInfo(); // 输出 tom 19
// ④ 全局 age 已经被第 ① 步通过 window.age 改成了 19
console.log(age); // 19
// ⑤ 把实例的方法拿来 new:this 是全新的对象,没有 name / age
new o.getInfo(); // 输出 undefined undefined
</script>
</body>
</html>要点解释
Func('tom', 19)前面没有任何点,按"谁调用指向谁"的规则,this就是window;因此this.name = name等价于window.name = 'tom',this.age = age等价于window.age = 19。- 全局变量本质上是
window的属性,所以第 ① 步会把全局的age从10改成19,最后console.log(age)输出19。 new Func()是实例化,this指向新创建的对象;没传参,this.name就是undefined。- 实例化同样会执行构造函数里的所有语句,不只是"创建对象"。
new o.getInfo()是合法的:getInfo也是函数,可以被new;此时this是另一个全新对象,从未添加过name/age,所以两个都是undefined。- 易错点:不要看函数"写在哪里"来判断
this,要看"调用那一刻,点前面是谁"。
示例 2:this 题目 02(点语法决定 this)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>this 题目 02</title>
</head>
<body>
<script>
var age = 20; // 全局变量,即 window.age
var obj = {
age: 10,
getAge: function () {
console.log(this.age);
}
};
var obj1 = {
age: 20
};
// 把 obj 挂到 obj1 的 prop 属性上
obj1.prop = obj;
// obj1.prop 就是 obj,obj.getAge 就是那个函数
var fn = obj1.prop.getAge;
// ① obj 调用:this 是 obj
obj.getAge(); // 10
// ② 点前面是 prop,prop 的值是 obj,this 还是 obj
obj1.prop.getAge(); // 10
// ③ 直接调用:前面没有点,this 是 window
fn(); // 20
</script>
</body>
</html>要点解释
- 判断
this只看调用时"点前面是谁":obj.getAge()点前面是obj,this就是obj,输出10。 obj1.prop.getAge()点前面是prop,而prop的值是obj,所以this仍然是obj,输出10。转写稿里的比喻是"外婆、妈妈和你,找离得最近的那个"。fn()前面什么都没有,this就是window,输出全局的20。- 易错点:
obj1.prop.getAge()不是"谁点出来就是谁",而是"最后一个点前面的那个对象";把函数赋值给变量后,this会跟着"调用方式"变,不会跟着"原来的对象"走。
示例 3:获取对象的原型(__proto__ 与 prototype)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>获取对象的原型</title>
</head>
<body>
<script>
var arr1 = [1, 2, 3];
var arr2 = [4, 5];
// ① 通过对象自带的 __proto__ 拿到它的原型
console.log(arr1.__proto__); // 数组原型对象,上面挂着 push / pop / shift / forEach ...
// ② 通过构造函数拿 prototype(注意:拿到的是"对象的原型",不是构造函数自己的原型)
console.log(Array.prototype);
// ③ 两者是同一个东西
console.log(arr1.__proto__ === Array.prototype); // true
// ④ 构造函数相同 → 原型相同
console.log(arr1.constructor === arr2.constructor); // true
console.log(arr1.__proto__ === arr2.__proto__); // true
</script>
</body>
</html>要点解释
- 每个对象都有原型,
arr1.__proto__拿到的就是数组的原型对象;push、pop、shift、forEach这些方法都挂在原型上,不在数组本身。 Array.prototype也能拿到这个原型,但注意:它拿到的是"数组实例的原型",不是"构造函数自己的原型"。构造函数只是取原型的"途径"。arr1.__proto__ === Array.prototype为true,说明__proto__和prototype指向的是同一个对象。- 两个不同数组的构造函数相同,原型也相同,所以
arr1.__proto__ === arr2.__proto__为true;反过来,一个原型可以作为无数个对象的原型。 - 易错点:调试工具里看到的
[[Prototype]]只是一种"指向"的表示,不能写arr1.[[Prototype]];真正能用的是arr1.__proto__。 - 易错点:所谓"隐式原型"(
__proto__)才是对象真正的原型,"显式原型"(prototype)只是构造函数上的一个属性,是通过构造函数获取对象原型的途径,不要被这两个名字绕晕。
常见错误
1. 调用构造函数时漏写 new,this 落到 window 上
- 现象:写
var o = Func('tom', 19);后o是undefined,而全局变量(如age)被悄悄改掉了。 - 原因:不带
new时它只是普通函数调用,函数内this按"谁调用指向谁"的规则变成window,所有this.xxx = ...都写到了全局对象上;同时没有返回值,所以接收变量是undefined。 - 修法:看到"要给某个变量拿到对象"就必须写
new;排查时先看调用处前面有没有new,再看函数体里是否大量出现this.xxx = ...。
2. 以为 实例.prototype 能拿到原型
- 现象:
arr.prototype输出undefined;arr["__proto__"]写法能出结果,但arr[__proto__](不加引号)报错或拿到undefined。 - 原因:
prototype是构造函数身上的属性,普通实例身上没有;__proto__是属性名,必须写成字符串或点语法,不加引号时会被当成变量去求值。 - 修法:取实例原型统一用
obj.__proto__或obj.constructor.prototype;取"某类对象的原型"用Array.prototype/String.prototype,不要在实例上找prototype。
3. 把"显示原型"当成对象自己的原型
- 现象:比较
arr.__proto__ === Array.prototype得到true,但一到Func.prototype就混乱,认为Func.prototype是Func自己的原型。 - 原因:
Func.prototype是"给它的实例用的原型",不是Func这个函数对象自己的原型(Func.__proto__才是);被"隐式/显示"这套叫法带偏。 - 修法:记住"原型就是原型,不分隐显,一个对象只有一个原型";写代码时只问一句——我现在拿的是谁的原型?是实例的(
__proto__)还是构造函数准备给实例用的(prototype)。
4. 构造函数里 return 了对象,实例化结果被换掉
- 现象:
new Foo()得到的不是构造函数里this挂满属性的那个对象,而是别的东西。 - 原因:构造函数返回对象类型(数组、函数、对象)时,
new的结果就是该返回值;返回原始类型(数字、字符串、布尔)时被忽略,仍返回新建对象。 - 修法:构造函数里不要写
return(除非刻意要替换实例);调试时先看函数体末尾有没有return {...}/return [...]。
5. 给原始类型"加属性"后读不到
- 现象:
var s = 'abc'; s.x = 1; console.log(s.x);输出undefined。 - 原因:赋值时临时包装成
String对象,属性加在那个临时对象上,语句一结束临时对象即销毁;下一句读取又重新包装了一个新对象,自然没有该属性。 - 修法:不要给字符串/数字/布尔挂自定义属性;需要带属性的数据就用对象或数组承载。
6. 递归扁平化"写了函数但没调用",或依赖外层变量
- 现象:函数写完了,打印结果还是空数组/原数组;或函数内部
push到外面定义的newArr,复用一次就串数据。 - 原因:只声明了递归函数,没有第一次"起头"的调用;结果数组放在函数外部,多次调用会互相污染。
- 修法:写完立即用一个真实数组调用一次;把结果数组作为函数内部局部变量,并
return出去,让递归的每一层都返回自己的结果再拼接。
7. 用固定层数 for 循环做扁平化
- 现象:数据两层能跑通,换成三层就漏元素,改代码加一层后数据再变又崩。
- 原因:把"分叉不整齐"的树形结构当成固定层数处理,层数写死。
- 修法:判断元素是否
Array.isArray(),是就递归调用自身并把返回值合并,不是就直接push;由数据本身决定递归深度。
练习
练习 1(this 判定) 不运行代码,写出输出并说明每一步 this 是谁:
js
var age = 10;
function F(name, age) {
this.name = name;
this.age = age;
this.getInfo = function () {
console.log(this.name, this.age);
};
}
F('tom', 19);
console.log(age);
var o = new F('jerry', 20);
o.getInfo();
var g = o.getInfo;
g();练习 2(找错并修正) 下面代码希望得到两个独立的学生对象,指出至少两处问题并改正:
js
function Student(name) {
this.name = name;
this.say = function () { console.log(this.name); };
return { name: 'override' };
}
var s1 = Student('小明');
var s2 = new Student('小红');
s1.say();练习 3(原型获取) 用两种方式分别取出数组 [1,2,3] 的原型,并验证二者严格相等;再验证两个不同数组的原型是否相等,并写出结论。
练习 4(递归扁平化) 写一个 flat(arr),要求不使用外部变量,直接把扁平化后的新数组 return 出来,并处理 [[1,[2,[3,4]]],5] 这种多层嵌套。
练习 5(判断 + 说理) 判断正误并说明理由: ① arr.constructor.prototype === arr.__proto__ ② Func.prototype === Func.__proto__ ③ 一个原型只能对应一个对象 ④ 构造函数返回原始类型时,new 的结果是那个原始值
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 回顾 | 17:19 |
| 2 | this题目01 | 09:00 |
| 3 | this题目02 | 04:35 |
| 4 | 递归实现数组扁平化 | 06:07 |
| 5 | 原型的概念 | 15:04 |
| 6 | 如何获取对象的原型 | 18:00 |
| 7 | 对象、原型、构造函数之间的关系 | 04:39 |
小结
new是"函数"和"构造函数"身份的唯一分界线,看到new就要立刻检查函数体里的this指的是新建对象。- 函数内
this的判定只有一条规则:看调用处前面有没有点、点前面是谁;没有点就是window。不要去猜函数"写在哪里"。 - 构造函数返回对象类型会替换实例化结果,返回原始类型会被忽略——这是
new结果异常的常见根源。 prototype属于构造函数(给实例用),__proto__属于实例(就是它自己的原型);"隐式/显示原型"只是历史叫法,本质只有一个原型。- 同构造函数 ⇒ 同原型;同数据类型 ⇒ 同构造函数 ⇒ 同原型。这条推理链能解释绝大多数"原型是不是同一个"的判断。
- 原型存在的意义是让方法被大量实例共享,实例本身只保留各自的数据;这也是为什么数组的
push、pop不在数组自身上。 - 原始类型被当对象用时是"临时包装、用完即销毁",所以给它加属性必然读不回来。
- 处理层数不确定的嵌套结构只能用递归,且递归函数必须有一次"起头"的调用,结果通过
return层层返回而不是靠外部变量累积。