一、原型
原型是顺应人类自然思维的产物。中文中有个成语叫做“照猫画虎”,这里的猫看起来就是虎的原型,所以,由此我们可以看出,用原型来描述对象的方法可以说是古已有之。 我们在上一节讲解面向对象的时候提到了:在不同的编程语言中,设计者也利用各种不同的语言特性来抽象描述对象。 还有一种就是基于原型的编程语言,它们利用原型来描述对象。我们的 JavaScript 就是其中代表。
Function.prototype.__proto__ === Object.prototype //true
__proto__所有对象都有,prototype只有函数才有。
- 应用层面:
每个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型。
每个对象都有 proto 属性,指向了创建该对象的构造函数的原型。 - 原型链和原型对象一一对应,原型链一定指向原型对象。
原型和原型链
- 属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象Object.prototype,如还是没找到,则输出 undefined;
属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用: b.prototype.x = 2;但是这样会造成所有继承于该对象的实例的属性发生改变。
//js引擎部分
<script>
const l = console.log
// 一、 2个顶级原型对象
l(Object.prototype) // ->{constructor:f} 普通对象
l(Function.prototype) // -> ƒ () { [native code] } 函数对象
// 二、他们的构造函数
l(Object.prototype.constructor) // ->ƒ Object() { [native code] }
l(Function.prototype.constructor === Function) // -> ƒ Function() { [native code] }
// 三、他们的原型链关系
l(Object.prototype.__proto__) // ->null
l(Function.prototype.__proto__) // ->{constructor:f} Object.prototype
// 四、利用Function.prototype,内置了很多高阶函数对象(一百多种)
l(Array.__proto__ === Function.prototype) // -> true
l(Boolean.__proto__ === Function.prototype) // -> true
l(Date.__proto__ === Function.prototype) // -> true
l(eval.__proto__ === Function.prototype) // -> true
l(Function.__proto__ === Function.prototype) // -> true 悖论:自己产生了自己?为了逻辑的完整性
</script>
// 以上全是浏览器集成的js引擎提供的机制
//应用层
<script>
// 五、应用层之函数:所有函数都是 new Function生成的。
const date2 = new Date()
l(date2)
/* Date.prototype.eat = function () {
l('吃了苹果')
}
const date1 = new Date()
date1.eat()
l(date1)
l(date1.getDate())
l(date1.getFullYear())
l(date1.getMonth()) */
Date.prototype.eat = new Function()
const date1 = new Date()
date1.eat()
// 六、应用层之对象:所有对象都是 new Ojbect生成的。
let obj1 = {} // let obj1 = new Object()
l(obj1.__proto__ === Object.prototype) //true
</script>
[[prototype]] 和 proto
旧版浏览器有proto,新版浏览器没有proto。
es6为了保证新旧版浏览器的兼容性,无论是否支持proto属性,都纳入了规范。但是不推荐使用。
mdn 资料二、数据类型
不同语言类型对比
参考:qqqq
‘’‘’
000
000
内置类型
JS 中分为七种内置类型,七种内置类型又分为两大类型:基本类型和对象(Object)。
基本类型
六种:null,undefined,Boolean,Number,String,Symbol
内置类型 | 定义 | 值 | 备注 |
---|---|---|---|
Null | 定义了还是空的 | 只有一个值null === null // true 值 null 特指对象的值未设置 |
1. 原型链的终点 |
Undefined | 未定义的属性或变量 | 只有一个值,原始值undefinedundefined === undefined // true |
1. 设计失误:undefined不是关键字,用void 0来代替。 1. 它是一个JavaScript的 原始数据类型 。 |
Boolean | 表示逻辑意义上的真和假 | 有两个值: 关键字true和false |
|
Number | 表示通常意义上的数字。 |
有n个值 | 1. 符合 IEEE 754-2008 规定的双精度浮点数规则 1. 浮点数的精度问题::有些小数以二进制表示位数是无穷的, |
String | 表示文本数据 | 有n个值 | 1. JavaScript 中的字符串是永远无法变更的,一旦字符串构造出来,无法用任何方式改变字符串的内容,所以字符串具有值类型的特征。 1. JavaScript 这个设计继承自 Java,最新标准中是这样解释的,这样设计是为了“性能和尽可能实现起来简单”。因为现实中很少用到 BMP 之外的字符。 |
symbol | 唯一标识 | 有n个唯一的值 | 1. 它是一切非字符串的对象 key 的集合,在 ES6 规范中,整个对象系统被用 Symbol 重塑。 |
undefined
概念区分:
- js中什么是未定义?两种情况
- 没有声明变量,直接使用该变量,报错该变量未定义。
- 声明了变量,没有赋值。
- “变量声明了没有赋值” —-> 变量定义了,浏览器默认赋值为undefined。
object
| object |
- 广义上:表示对象的意思,是一切有形和无形物体的总称。
- 在 js 中,对象的定义是“属性的集合”。属性分为数据属性和访问器属性,二者都是 key-value 结构,key 可以是字符串或者 Symbol 类型。
| 有普通对象、函数对象、原型对象几种 |
1. 关于类和类型:
其他语言(java)每个类都是一个类型,二者几乎等同。可以自定义类型。
javascript只有七种类型,不可以自定义类型;类只是运行时对象的一个私有属性。
| | —- | —- | —- | —- |
- 基本类型在对象类型中的亲戚:Number;String;Boolean;Symbol。
- Number、String 和 Boolean,三个构造器是两用的,当跟 new 搭配时,它们产生对象;symbol不能与new使用
- Number、String 和 Boolean直接调用时,它们表示强制类型转换;Symbol直接调用则产生一个symbol对象。
js模糊了对象与基本类型的关系:基本类型可以直接使用对应的对象方法。
console.log("abc".charAt(0)); //a
甚至我们在原型上添加方法,都可以应用于基本类型。
Symbol.prototype.hello = () => console.log("hello");
var a = Symbol("a");
console.log(typeof a); //symbol,a并非对象
a.hello(); //hello,有效
关于symbol 的背景
es6之前key的类型只能为字符串。
- 所有键名都是字符串,加不加引号都行。
- 键名是数字时,自动转为字符串。
- 键名不符合规范,语法报错。
- symbol 是 ES6 中引入的新类型,它是一切非字符串的对象 key 的集合,在 ES6 规范中,整个对象系统被用 Symbol 重塑。
- 为什么引入?
字符串key容易造成属性名的冲突,引入了symbol作为key。 为什么不new生成?
这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。类型判断
1、typeof
存在哪些问题?
对于基本类型,除了null会错误显示为”object”(最初使用低位存储变量的类型信息造成的),其他正确显示;
- 对于对象除了函数显示为”function”,数组和对象都显示为’object’,不能区分array和object;
为什么有这个问题?
let bool = true;
let num = 1;
let str = 'abc';
let und= undefined;
let nul = null;
let arr = [1,2,3,4];
let obj = {name:'xiaoming',age:22};
let fun = function(){console.log('hello')};
let s1 = Symbol();
console.log(typeof bool); //boolean
console.log(typeof num);//number
console.log(typeof str);//string
console.log(typeof und);//undefined
console.log(typeof nul);//object
console.log(typeof arr);//object
console.log(typeof obj);//object
console.log(typeof fun);//function
console.log(typeof s1); //symbol
2、instanceof
- 优缺点:
- 不能识别出基本的数据类型
- 可以检测出引用类型,如array、object、function
- 同时对于是使用new声明的类型,它还可以检测出多层继承关系。
- 类型判断原理:
js的继承都是采用原型链来继承的。比如objA instanceof A ,其实就是看objA的原型链上是否有A的原型,而A的原型上保留A的constructor属性。所以instanceof一般用来检测对象类型,以及继承关系。 ```javascript let bool = true; let num = 1; let str = ‘abc’; let und= undefined; let nul = null; let arr = [1,2,3,4]; let obj = {name:’xiaoming’,age:22}; let fun = function(){console.log(‘hello’)}; let s1 = Symbol();
console.log(bool instanceof Boolean);// false console.log(num instanceof Number);// false console.log(str instanceof String);// false console.log(und instanceof Object);// false console.log(nul instanceof Object);// false console.log(arr instanceof Array);// true console.log(obj instanceof Object);// true console.log(fun instanceof Function);// true console.log(s1 instanceof Symbol);// false
手写代码实现instanceof
```javascript
// instanceOf([],Array)
function instanceOf(left, right) {
let leftValue = left.__proto__;
let rightValue = right.prototype;
while (true) {
//Object.__proto --> null
if (leftValue === null) {
return false;
}
// 通过原型链找到了原型对象
if (leftValue === rightValue) {
console.log('left2', leftValue)
console.log('right2', rightValue)
return true;
}
// 沿着原型链查找
console.log('left1', leftValue)
console.log('right1', rightValue)
leftValue = leftValue.__proto__;
}
}
// test
function People() { }
var student = new People()
instanceOf(student, Object)
// student.__proto__ -->People.prototype
// student.__proto__.__proto__ --> Object.Prototype
3、constructor
null、undefined没有construstor方法,因此constructor不能判断undefined和null。
但是它是不安全的,因为contructor的指向是可以被改变。例如:Boolean.prototype.constructor = 'aa'
。
注意:bool.constructor
可以直接.constructor是因为继承了原型对象—Object.prototype的construct属性
// obj.__proto__.constructor === obj.constructor //true
let bool = true;
let num = 1;
let str = 'abc';
let und= undefined;
let nul = null;
let arr = [1,2,3,4];
let obj = {name:'xiaoming',age:22};
let fun = function(){console.log('hello')};
let s1 = Symbol();
console.log(bool.constructor === Boolean);// true
console.log(num.constructor === Number);// true
console.log(str.constructor === String);// true
console.log(arr.constructor === Array);// true
console.log(obj.constructor === Object);// true
console.log(fun.constructor === Function);// true
console.log(s1.constructor === Symbol);//true
4、Object.prototype.toString.call
此方法可以相对较全的判断js的数据类型。
let bool = true;
let num = 1;
let str = 'abc';
let und= undefined;
let nul = null;
let arr = [1,2,3,4];
let obj = {name:'xiaoming',age:22};
let fun = function(){console.log('hello')};
let s1 = Symbol();
console.log(Object.prototype.toString.call(bool));//[object Boolean]
console.log(Object.prototype.toString.call(num));//[object Number]
console.log(Object.prototype.toString.call(str));//[object String]
console.log(Object.prototype.toString.call(und));//[object Undefined]
console.log(Object.prototype.toString.call(nul));//[object Null]
console.log(Object.prototype.toString.call(arr));//[object Array]
console.log(Object.prototype.toString.call(obj));//[object Object]
console.log(Object.prototype.toString.call(fun));//[object Function]
console.log(Object.prototype.toString.call(s1)); //[object Symbol]
至于在项目中使用哪个判断,还是要看使用场景,具体的选择,一般基本的类型可以选择typeof,引用类型可以使用instanceof。
- 基本类型(null): 使用 String(null)
- 基本类型(string / number / boolean / undefined) + function: 直接使用 typeof即可
- 其余引用类型(Array / Date / RegExp Error): 调用toString后根据[object XXX]进行判断
function determineType(obj) {
if (obj == null) return String(obj) //'null'
return typeof obj === 'object'
? Object.prototype.toString.call(obj) //'[object xxx]'
: typeof obj //'function' 或基本类型
}
类型转换
转undefined和null
其他类型无法转换为undefined和null。转boolean类型
| 其他类型转boolean(6种) | 哪些值或对象可以转为false | 哪些值或对象可以转为true | | —- | —- | —- | | undefined | undefined | 无 | | null | null | 无 | | number | NaN ,0 ,-0 | 剩下的都可以 | | string | 空字符串’’ | 剩下的都可以 | | object | 无 | 都可以 | | symbol | 无 | 都可以 |
需要注意的是:
- 基本类型 null 和 undefined 都是值类型(有且只有一个值)。
- number类型中特殊的值NaN表示非数值类型,或者不可表示的值。
NaN === NaN // false
- string类型中转换true和false的依据是,字符串长度是否大于0。例如:
Boolean('0') //true
- object类型中所有对象都为true。例如:
Boolean({}) //true
,Boolean([]) //true
转number类型
| 其他类型转number(6种) | 哪些值或对象可以转为正常数字 | 哪些值或对象可以转为NaN | | —- | —- | —- | | undefined | 无 |Number(undefined)//NaN
| | null |Number(null) // 0
| 无 | | boolean |Number(true) // 1
Number(false) //0
| 无 | | string | 空字符串、所有有且只有有效数字字符的字符串
例如:Number('9')//9
| 所有非有效数字字符的非空字符串
例如:Number('9px')//NaN
| | object | 无 | 所有对象 | | symbol | 无法将符号值转换为数字 | |
转string类型
其他类型转string(6种) | 哪些值或对象可以转为string |
---|---|
undefined | 都是所见即所得,转为对应值的字符串形式,例如:String(undefined) // "undefined" String(null) // "null" String(true) // "true" String(NaN) // "NaN" String(Symbol()) // "Symbol()" |
null | |
boolean | |
number | |
symbol | |
object | object比较复杂,有多种情况: 1. 普通对象会转为”[object Object]”字符串形式 1. 数组类型中,空数组转为空字符串 “”,非空数组转为序列字符串,例如: |
String([])//""
String([67,'34','wow','哇哦'])//"67,34,wow,哇哦"
3. 函数对象所见即所得,例如:
String(function(){console.log('98')})
//"function(){console.log('98')}"
|
转object类型
undefined、null、boolean、number、string、symbol这六种基本类型转为亲戚对象,在此过程中基本对象类型经历了隐式转换即装箱操作。原理:基本值 —> 亲戚对象 (装箱转换) —> 可以用任意方法
类型方法
对象方法
方法名 | 语法 | 返回值 | 实现什么功能 |
---|---|---|---|
assign | Object.assign(target, …sources) 参数: target 目标对象 sources 源对象 |
目标对象 | 将所有可枚举属性的值从一个或多个源对象分配到目标对象 |
create | |||
defineProperties | |||
defineProperty | |||
entries | |||
fromEntries |
assign方法
// 1.1 key不冲突时,把资源对象的键值对都补充在目标对象里
const target = { a: 1 }
const result = Object.assign(target, { b: 2 }, { c: 3 })
console.log(target === result) //true
console.log(target) //true
// 1.2 key冲突时,资源对象会覆盖掉目标对象中相同key的值
const target = { a: 1 }
Object.assign(target, { a: 2, b: 2 })
console.log(target) // { a: 2, b: 2 }
// 1.3 数组也可使用这个方法
const target = [1, 2, 7]
Object.assign(target, [3, 4], [5, 6]) //{0:1,1:2}
console.log(target) // [5,6,7]
// 1.4 这个方法是浅拷贝
// 浅拷贝只能拷贝值和对象地址(栈内存中的内容),深拷贝除了他们还可以拷贝对象。(栈和堆内存中的内容)
数组方法
序号 | 遍历方法 | 方法作用 | 入参 | 回调函数参数 | 是否改变原数组 | 返回值 |
---|---|---|---|---|---|---|
1 | for…in… | 循环遍历 | / | / | / | / |
2 | for loop | / | / | / | / | |
3 | forEach() | 回调函数(callback) | via系列(value,index,array) | 数组中每一项是基本类型是不会改变原数组,复杂类型时会改变原数组 | undefined | |
4 | map() | 返回一个由回调函数的返回值组成的新数组 | 返回一个新数组(和原数组等长) | |||
5 | filter() | 返回一个满足回调函数筛选条件的数组(过滤数据) | 返回一个新数组(小于等于原数组长度) |
forEach()
var arr1 = [2, 3, 1, 4]
var arr2 = [{ a: 1 }, { a: 2 }]
// 用函数作为参数叫做高阶函数
// 有什么好处?相当于把逻辑片段作为参数传进去;代码更加简洁,封装了重复代码;
arr1.forEach((value, index, arr) => {
value = '333'
});
// 实现forEach
Array.prototype._forEach = function (callback) {
for (var key = 0; key < this.length; key++) {
callback(this[key], key, this)
}
}
序号 |
增删方法 | 方法作用 | 入参 | 是否改变原数组 | 返回值 |
---|---|---|---|---|---|
1 | push() | 在数组尾部添加元素 | 新增的一个或者多个元素 | 是 | 新数组长度 |
2 | pop() | 在数组尾部删除元素 | 无 | 是 | 被删除的元素 |
3 | shift | 在数组头部删除元素 | 无 | 是 | 被删除的元素 |
4 | unshift | 在数组头部添加元素 | 新增的一个或多个元素 | 是 | 新数组的长度 |
序号 | 查找元素方法 | 用途 | 入参 | 是否改变原数组 | 返回值 |
---|---|---|---|---|---|
1 | indexOf() | 从开头查基本类型的数组 | 要查找的元素和要从哪开始查找的索引 | 否 | 若查到则返回符合条件的第一个元素的索引,否则是 -1 |
2 | lastIndexOf() | 从尾部查基本类型的数组 | 若查到则返回符合条件的最后一个元素的索引,否则是 -1 | ||
3 | includes() | 判断一个数组是否包含一个指定的值 | 若查到则返回true,否则返回false | ||
4 | find() | 用来查找引用类型数组中的满足回调函数条件的元素 | via系列的回调函数 | 若找到返回符合条件的元素,否则返回undefined | |
5 | findIndex() | 若找到返回符合条件的元素的索引,否则返回 -1 |
字符串方法
三、面向对象
js相比较其他语言而言:
- javaScript(直到 ES6)有对象的概念,但是却没有像其他的语言那样,有类的概念;
- 在 JavaScript 对象里可以自由添加属性,而其他的语言却不能。
- 利用各种不同的语言特性来抽象描述对象,最为成功的流派是使用“类”的方式来描述对象,这诞生了诸如 C++、Java 等流行的编程语言。
如何定义面向对象和基于对象?
- 面向对象强调编程的思想
- 面向对象编程也被认为是:更接近人类思维模式的一种编程范式
基于对象:语言和宿主的基础设施由对象来提供,并且 JavaScript 程序即是一系列互相通讯的对象集合
对象的特征
对象具有唯一标识性:即使完全相同的两个对象,也并非同一个对象。
- 对象有状态:对象具有状态,同一对象可能处于不同状态之下。
- 对象具有行为:即对象的状态,可能因为它的行为产生变迁。
| 语言 | 唯一标识性 | 状态 | 行为 |
| —- | —- | —- | —- |
| C++ | 各种语言的对象唯一标识性都是用内存地址来体现的, 对象具有唯一标识的内存地址,所以具有唯一的标识。 | 成员变量 | 成员函数 |
| java | | 属性 | 方法 |
| js | | 属性
(JavaScript 中对象独有的特色是:对象具有高度的动态性,这是因为 JavaScript 赋予了使用者在运行时为对象添改状态和行为的能力) | |
表 - 语言中对象特征对比
语言 | 描述对象方式 |
---|---|
C++ java |
基于类 |
js | 基于原型 |
javascript对象的两种属性描述符
实际上 JavaScript 对象的运行时是一个“属性的集合”,属性以字符串或者 Symbol 为 key,以数据属性特征值或者访问器属性特征值为 value。
对象是一个属性的索引结构(索引结构是一类常见的数据结构,我们可以把它理解为一个能够以比较快的速度用 key 来查找 value 的字典)。{writable:true,value:1,configurable:true,enumerable:true}是 value。
为什么有人说“JavaScript 不是面向对象”这样的说法。这是由于 JavaScript 的对象设计跟目前主流基于类的面向对象差异非常大。
可事实上,这样的对象系统设计虽然特别,但是 JavaScript 提供了完全运行时的对象系统,这使得它可以模仿多数面向对象编程范式(下一节课我们会给你介绍 JavaScript 中两种面向对象编程的范式:基于类和基于原型),所以它也是正统的面向对象语言。
JavaScript 语言标准也已经明确说明,JavaScript 是一门面向对象的语言,我想标准中能这样说,正是因为 JavaScript 的高度动态性的对象系统。
面向对象真的需要模拟类吗? JavaScript 本身就是面向对象的,它并不需要模拟,只是它实现面向对象的方式和主流的流派不太一样,所以才让很多人产生了误会。
javaScript 创始人 Brendan Eich 在“原型运行时”的基础上引入了 new、this 等语言特性,使之“看起来语法更像 Java”
数据属性描述符
它比较接近于其它语言的属性概念。数据属性具有四个特征。使用getOwnPropertyDescriptor
查看数据属性。
- value:就是属性的值。
- writable:决定属性能否被赋值。
- enumerable:决定 for in 能否枚举该属性。
configurable:决定该属性能否被删除或者改变特征值。
// 1.对象字面量默认定义
let obj = {a:'111'} // 默认定义了四个数据描述符:value writable enumerable configurable
// 2.自定义数据描述符
Object.defineProperty(
{},'a', {value:'222',writable:false,enumerable:false,configurable:false})
console.log(obj1.a)//111
obj2.a = '333';
console.log(obj2.a)//222
访问器属性描述符
在大多数情况下,我们只关心数据属性的值即可。第二类属性是访问器(getter/setter)属性,它也有四个特征。使用
Object.defineProperty
定义访问器属性或者改变属性的特征。getter:函数或 undefined,在取属性值时被调用。
setter:函数或 undefined,在设置属性值时被调用。
// 1.在对象中直接定义
let obj3 = {
b: 'bbb', get b() {
console.log('get') //get
return obj2
}, set b(val) {
console.log('set', val) //set,ccc
}
}
console.log(obj3.b) //{a:'222'}
console.log(obj3.b = 'ccc') //ccc
// 2.defineProperty定义
let obj4 = { r: '111' }
Object.defineProperty(obj4, 'r', {
get() {
console.log('get')//get
return 'eee'
},
set (val) {
console.log('set',val); //set vvv
}
})
console.log(obj4.r) //eee
console.log(obj4.r = 'vvv') //vvv
注意:不能同时指定访问器和值或可写属性。也就是说只有
value writable enumerable configurable
或者getter setter enumerable configurable
可放在一起同时使用。运行时
运行时分为两个阶段:编译阶段、执行阶段
编译阶段:js通过编译生成执行上下文和可执行代码两部分。
执行阶段:执行可执行代码,输出结果。执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。
- 可执行代码就是指js引擎可以执行的代码,这些代码可以是【字节码、机器码】。我们写的js代码它看不懂,也不会执行,它要先把js编译成字节码、机器码才行。
js到底是什么语言?
- 新生成了一个对象
- 链接到原型
- 绑定 this
- 返回新对象
手写一个new函数
/*
* @param 构造函数
* @params 实例对象参数
*/
function _new() {
// 创建一个空的对象
let obj = new Object()
// 获得构造函数
let Con = [].shift.call(arguments)
// 链接到原型
obj.__proto__ = Con.prototype
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments)
// 确保 new 出来的是个对象
return typeof result === 'object' ? result : obj
}
// 测试
function People(name) {
this.name = name
}
let student1 = new People('zs')
console.log(student1)//People{name:'zs'}
let student2 = _new(People, 'ls')
console.log(student2)//People{name:'ls'}
/*
函数内部的机制:
1. 每个函数都有一个动态参数 this
2. 函数有实参和形参。形参在声明函数时声明,实参在调用函数时传入。
3. 函数有函数体
4. 每个函数都有返回值,没有用retrun关键字指定返回值,默认在函数体的底部返回undefined
*/
function fn(//1.动态参数this //2.形参列表arguments
) {
// 3.函数体
// 4.返回值 默认是 return undefined
}
知识补充:
关于语法糖的好坏:
1. 好处:只管用,不用知道内部原理。对新手特别友好;语法糖会提升编程效率。
2. 坏处:深入内部原理及其困难。有时候会干扰对代码执行的分析;提高了语法学习的门槛,让学习曲线由易到难。
什么是语法糖:
1. 封装复杂的内部逻辑,变为简单易用的代码。
2. 写的爽,用的爽,就像吃了糖一样。
有哪些语法糖:
1. js中语法糖数不胜数。es6疯狂的添加。常见的就是new关键字 class关键字 function关键字 对象字面量{},instanceof。
2. 学习js对新手的要求就是掌握语法糖的熟练应用;对中级工程师要求深入理解常见语法糖的原理。
this
- 宿主对象(host Objects):由 JavaScript 宿主环境提供的对象,它们的行为完全由宿主环境决定。
- 内置对象(Built-in Objects):由 JavaScript 语言提供的对象。
- 固有对象(Intrinsic Objects ):由标准规定,随着 JavaScript 运行时创建而自动创建的对象实例。
- 原生对象(Native Objects):可以由用户通过 Array、RegExp 等内置构造器或者特殊语法创建的对象。
- 普通对象(Ordinary Objects):由{}语法、Object 构造器或者 class 关键字定义类创建的对象,它能够被原型继承。
情况一:this指向调用者
// 宿主对象 === 全局对象
function foo() { //var foo = fn() {}
console.log(this) //window对象
console.log(this.a) //1
}
var a = 1;
foo();
/*
this第一条规则总结:
1. 声明的全局变量都是宿主对象(window)的属性;
2. this指向在函数定义阶段,无法确定;
3. this指向在执行阶段(运行时)才能确定。因为this相当于一个动态参数。
*/
// 宿主对象 === 全局对象
function foo() { //var foo = fn() {}
console.log(this) // obj
console.log(this.a) //2
}
var obj = {
a: 2,
foo: foo // obj.foo !== window.foo
}
obj.foo()
/*
this解惑
1. this特点:this只存在函数中,作为动态参数
2. 函数永远作为某个对象的属性存在,this恰恰指的是函数所在的那个对象。
3. this表达的意思是,我这个函数是哪个对象的行为。
*/
情况二:new和call、bind、apply改变this指向
new会将this指向新生成的对象。
方法 | 剩余入参(第一个参数传入的都是this要指向的对象) | 是否函数调用 | 应用场景 |
---|---|---|---|
call | 参数序列 | 是 | 实现继承 |
apply | 第二个参数是数组 | 是 | 经常跟数组有关系比如借助于数学对象实现数组最大值最小值 |
bind | 参数序列 | 否 | 常用于回调函数中。比如改变定时器内部的this指向或者传递多个不同的this |
call实现继承
script>
function Class1(arg1, arg2) {
// new Class2生成新的实例对象的过程中经过call改变this指向,指定到了Class2上
this.name = arg1;
this.pass = arg2;
this.showSub = function () {
return this.name - this.pass; //5 - 6
}
}
function Class2(arg1, arg2, arg3) {
// this -> Class2
Class1.call(this, arg1, arg2);
this.get3Arg = function () {
return arg3;
}
}
// this -> Class1
var class1 = new Class1(5, 6);//new Class1()与new Class1是一样的作用
console.log(class1.showSub()); //5-6=-1
var class2 = new Class2(10, 1, 12);
console.log(class2.showSub()); //10-1=9
console.log(class2.get3Arg());//第三个参数12
</script>
模拟实现call函数
function foo() {
console.log(this)
}
foo()//window
var arr1 = []
foo.call(arr1))//[]
// call原理
Function.prototype.myCall = function (target) { //形参就相当于 var target = undefined
/*
实现原理:把目标函数foo绑定在目标对象target上(本来函数的this指向window,但是想要让this指向我们指定的对象,所以有下面这些操作)
1. 获取目标对象
2. 获取目标函数 foo,并把目标函数foo绑定到目标对象上
3. 执行目标函数并返回
*/
//如果传参则使用Object()包裹一下,有可能不是一个对象,若没有传参默认为window
target = target ? Object(target) : window
//将targetFunction绑定到target上,targetFunction是定义的临时属性
target.targetFunction = this
//获取传入的除了target以外剩余的参数
let args = [...arguments].slice(1)
let result = target.targetFunction(...args)
//删除临时属性
delete target.targetFunction
return result
}
var arr1 = []
foo.myCall(arr1))//arr1 []
注意:target.targetFunction = this这行代码的意思就是把foo.call(arr1) 中的foo函数挂载到arr1上。当执行foo.myCall() 时, myCall里的 this 指向调用者foo,所以这里的this就是foo函数。
apply求最大最小值
<script>
const l = console.log
var arr = [13, 4, 53, 43, 43, 23]
const min = Math.min.apply(null, arr)
const max = Math.max.apply(null, arr)
const max = Math.max.call(null, ...arr)
l(min) //4
l(max) //53
</script>
apply实现原理
<script>
Function.prototype.myApply = function (context) {
var context = context || window
context.fn = this
var result
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
let arr = [1, 34, 66, 4, 0, 4];
let min = Math.min.myApply(window, arr)
let max = Math.max.myApply(window, arr)
console.log(min) //0
console.log(max) //66
</script>
bind在回调函数中使用和实现函数科里化
bind使用场景举例:
function fn() {
console.log(this)
}
var arr = [1, 3]
setTimeout(fn, 100)//window
setTimeout(fn.bind(arr), 100)//arr
// 异步调用this都是指向window
bind实现原理
Function.prototype.bind2 = function (context) {
var self = this; // 这句实际上是把调用bind2的函数赋给self,console.log(self)得到的是一个函数
var args = Array.prototype.slice.call(arguments, 1); // 获取传入的参数,将其变为数组存入args中
return function () {
var funArgs = Array.prototype.slice.call(arguments); // 这里的arguments是这个return函数中传入的参数
return self.apply(context, args.concat(funArgs)) // 将this指向context,将self的参数和return的function的参数拼接起来
}
}
new改变this指向
function foo() { //var foo = fn() {}
console.log(this) // c对象
console.log(this.a) //undefined
}
var c = new foo()
c.a = 3
console.log(c.a) //3
情况三:箭头函数与回调函数
- 回调函数指向window。
- 箭头函数不改变this指向。 ```javascript function fn1() { // var self = this const fn2 = () => { // console.log(‘this’, this) } fn2() }
fn1()//window setTimeout(fn1, 100)//window setTimeout(fn1.bind([]), 100)//[] fn1.call(Boolean)//Boolean
```javascript
function fn1() {
//let that = this;//es6之前没有箭头函数,一般用这种方式存this
const fn2 = () => {
//console.log('this', this)
function fn3() {
console.log('this', this)
}
fn3()
}
fn2()
}
fn1()//window
setTimeout(fn1, 100)//window
setTimeout(fn1.bind([]), 100)//window
fn1.call(Boolean)//window
// 1. 变量和函数的作用域在声明阶段就已经确定了。
// 2. this(每个函数都有的属于自己的动态参数)只能在函数调用时确定。
面向对象编程
面向对象有三个特性:封装、继承、多态
多态
多态的实际定义:同一个操作作用于不同的对象上,可以产生不同的解释和不同的执行结果
也就是说,对于JavaScript一切皆对象(null, undefined除外)的语言来说,将相同的行为作用在不同的对象上时会产生不同的行为结果
比如说,同样是叫声这个行为,猫和狗(两个对象)会产生不同的叫声,实际上这符合JavaScript多态的特征
多态背后的思想就是想“做什么”和 “谁去做”分离开来,也就是将“不变的事物”和“可变的事物”分离开来
以上的例子中可以说明,动物都会叫,这是不变的。而不同的动物怎么叫是可变的
把不变的隔离开来,把可变的封装起来,这给予了我们扩展代码的能力,不管有多少只动物,都可以通过增加代码的方式来实现,而无需改变产生行为的代码
封装
1 原始模式
把两个属性封装在一个对象里面就是最简单的封装了,但是这种方式有两个很明显的缺点:
- 如果多生成几个实例,写起来就非常麻烦(有多少写多少);
- 实例与原型之间没有任何办法可以看出有什么联系。
1.1 原始模式的改进var cat1 = {}; // 创建一个空对象
cat1.name = "大毛"; // 按照原型对象的属性赋值
cat1.color = "黄色";
var cat2 = {};
cat2.name = "二毛";
cat2.color = "黑色";
写一个函数解决重复代码的问题
然后生成实例对象,就相当于调用函数function Cat(name,color) {
return {
name:name,
color:color
}
}
这种方法的问题依然是,cat1和cat2之间没有内在的联系,不能反映出它们是同一个原型对象的实例。var cat1 = Cat("大毛","黄色");
var cat2 = Cat("二毛","黑色");
2 构造函数模式
为了解决从原型对象生成实例的问题,Javascript提供了一个构造函数(Constructor)模式
所谓”构造函数”,其实就是一个普通函数,但是内部使用了this变量。对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上。
比如,猫的原型对象现在可以这样写
生成实例对象function Cat(name,color){
this.name=name;
this.color=color;
}
这时cat1和cat2会自动含有一个constructor属性,指向它们的构造函数。var cat1 = new Cat("大毛","黄色");
var cat2 = new Cat("二毛","黑色");
alert(cat1.name); // 大毛
alert(cat1.color); // 黄色
Javascript还提供了一个instanceof运算符,验证原型对象与实例对象之间的关系。alert(cat1.constructor == Cat); //true
alert(cat2.constructor == Cat); //true
怎么判断实例和实例、实例和构造函数之间的关系?alert(cat1 instanceof Cat); //true
alert(cat2 instanceof Cat); //true
1. constructor 明确了实例和构造函数之间的关系。
2. instanceof运算符 明确了实例和构造函数之间的关系。
3. 间接判断实例和实例之前的关系。
2.1 存在的不足
构造函数方法很好用,但是存在一个浪费内存的问题。
请看,我们现在为Cat对象添加一个不变的属性type(种类),再添加一个方法eat(吃)。那么,原型对象Cat就变成了下面这样:
生成实例function Cat(name,color){
this.name = name;
this.color = color;
this.type = "猫科动物";
this.eat = function(){alert("吃老鼠");};
}
表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。那就是对于每一个实例对象,type属性和eat()方法都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存。这样既不环保,也缺乏效率。var cat1 = new Cat("大毛","黄色");
var cat2 = new Cat ("二毛","黑色");
alert(cat1.type); // 猫科动物
cat1.eat(); // 吃老鼠
能不能让type属性和eat()方法在内存中只生成一次,然后所有实例都指向那个内存地址呢?回答是可以的。alert(cat1.eat == cat2.eat); //false
3 Prototype模式
Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象(例如,Cat.prototype指向堆内存存储的Cat的原型对象)。这个对象的所有属性和方法,都会被构造函数的实例继承。
这意味着,我们可以把那些不变的属性和方法,直接定义在prototype对象上
生成实例function Cat(name,color){
this.name = name;
this.color = color;
}
Cat.prototype.type = "猫科动物";
Cat.prototype.eat = function(){alert("吃老鼠")};
这时所有实例的type属性和eat()方法,其实都是同一个内存地址,指向prototype对象,因此就提高了运行效率。var cat1 = new Cat("大毛","黄色");
var cat2 = new Cat("二毛","黑色");
alert(cat1.type); // 猫科动物
cat1.eat(); // 吃老鼠
3.1 验证方法alert(cat1.eat == cat2.eat); //true
isPrototypeOf()
这个方法用来判断,某个proptotype对象和某个实例之间的关系。
alert(Cat.prototype.isPrototypeOf(cat1)); //true
alert(Cat.prototype.isPrototypeOf(cat2)); //true
hasOwnProperty()
每个实例对象都有一个hasOwnProperty()方法,用来判断某一个属性到底是本地属性,还是继承自prototype对象的属性。
alert(cat1.hasOwnProperty("name")); // true
alert(cat1.hasOwnProperty("type")); // false
in运算符
in运算符可以用来判断,某个实例是否含有某个属性,不管是不是本地属性。
alert("name" in cat1); // true
alert("type" in cat1); // true
in运算符还可以用来遍历某个对象的所有属性。
for(var prop in cat1) { alert("cat1["+prop+"]="+cat1[prop]); }
继承
构造函数的继承
对象之间的”继承”的五种方法
比如,现在有一个”动物”对象的构造函数。
function Animal(){
this.species = "动物";
}
还有一个”猫”对象的构造函数
function Cat(name,color){
this.name = name;
this.color = color;
}
怎样才能使”猫”继承”动物”呢?
一、 构造函数绑定
第一种方法也是最简单的方法,使用call或apply方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:
function Cat(name,color){
Animal.apply(this, arguments);
this.name = name;
this.color = color;
}
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物
二、 prototype模式
第二种方法更常见,使用prototype属性。
如果”猫”的prototype对象,指向一个Animal的实例,那么所有”猫”的实例,就能继承Animal了。
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物
Cat.prototype = new Animal();
将Cat的prototype对象指向一个Animal的实例。它相当于完全删除了prototype 对象原先的值,然后赋予一个新值。
Cat.prototype.constructor = Cat;
任何一个prototype对象都有一个constructor属性,指向它的构造函数。如果没有”Cat.prototype = new Animal();”这一行,Cat.prototype.constructor是指向Cat的;加了这一行以后,Cat.prototype.constructor指向Animal。alert(Cat.prototype.constructor == Animal); //true
每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性。 alert(cat1.constructor == Cat.prototype.constructor); // true
因此,在运行Cat.prototype = new Animal();
这一行之后,cat1.constructor也指向Animal。alert(cat1.constructor == Animal); // true
这显然会导致继承链的紊乱(cat1明明是用构造函数Cat生成的),因此我们必须手动纠正,将Cat.prototype对象的constructor值改为Cat。这就是第二行的意思。
这是很重要的一点,编程时务必要遵守。下文都遵循这一点,即如果替换了prototype对象,o.prototype = {};
那么,下一步必然是为新的prototype对象加上constructor属性,并将这个属性指回原来的构造函数。o.prototype.constructor = o;
三、 直接继承prototype
第三种方法是对第二种方法的改进。由于Animal对象中,不变的属性都可以直接写入Animal.prototype。所以,我们也可以让Cat()跳过 Animal(),直接继承Animal.prototype。
我们先将Animal对象改写:
function Animal(){ }
Animal.prototype.species = "动物";
然后,将Cat的prototype对象,然后指向Animal的prototype对象,这样就完成了继承。
Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物
与前一种方法相比,这样做的优点是效率比较高(不用执行和建立Animal的实例了),比较省内存。缺点是 Cat.prototype和Animal.prototype现在指向了同一个对象,那么任何对Cat.prototype的修改,都会反映到Animal.prototype。Cat.prototype.constructor = Cat;
这行代码其实是有问题的,这一句实际上把Animal.prototype对象的constructor属性也改掉了。alert(Animal.prototype.constructor); // Cat
四、 利用空对象作为中介
由于”直接继承prototype”存在上述的缺点,所以就有第四种方法,利用一个空对象作为中介。
var F = function(){};
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;
F是空对象,所以几乎不占内存。这时,修改Cat的prototype对象,就不会影响到Animal的prototype对象。alert(Animal.prototype.constructor); // Animal
将上面的方法进行封装便于使用
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
//使用
extend(Cat,Animal);
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物
这个extend函数,就是YUI库如何实现继承的方法。
另外,说明一点,函数体最后一行,Child.uber = Parent.prototype;
意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。(uber是一个德语词,意思是”向上”、”上一层”。)这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。
五、 拷贝继承
上面是采用prototype对象,实现继承。我们也可以换一种思路,纯粹采用”拷贝”方法实现继承。简单说,如果把父对象的所有属性和方法,拷贝进子对象,不也能够实现继承吗?这样我们就有了第五种方法。
首先,还是把Animal的所有不变属性,都放到它的prototype对象上。
function Animal(){}
Animal.prototype.species = "动物";
然后,再写一个函数,实现属性拷贝的目的。
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}
这个函数的作用,就是将父对象的prototype对象中的属性,一一拷贝给Child对象的prototype对象。
使用的时候,这样写:
extend2(Cat, Animal);
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物
普通函数的继承(非构造函数继承)
什么是非构造函数继承?
例如:
//有一个对象叫做“中国人”
var Chinese = {
nation: "中国"
};
//还有另一个对象叫做“医生”
var Doctor = {
career: "医生"
};
//请问怎么样才能让“医生”去继承“中国人”,也就是说怎样才能生成一个“中国医生”这个对象?
//请注意,这两个对象都是普通对象,不是构造函数,无法使用构造函数方法实现“继承”。
一、object()方法
object()函数可以解决上述问题
function object(o) {
function F() {}
F.prototype = o;
return new F ();
}
//这个object()函数,其实只做一件事,就是把子对象的prototype属性,指向父对象,从而使得子对象与父对象连在一起。
使用:
//1、先在父对象的基础上,生成子对象
var Doctor = object(Chinese);
//2、再加上子对象本身的属性
Doctor.career = '医生';
//3、这时,子对象已经继承了父对象的属性了。
console.log(Doctor.nation); //中国
二、浅拷贝
//除了使用"prototype链"以外,还有另一种思路:把父对象的属性,全部拷贝给子对象,也能实现继承。
function extendCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
使用:
var Doctor = extendCopy(Chinese);
Doctor.career = '医生';
console.log(Doctor.nation); // 中国
这样做存在哪些问题?
//如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。
//现在给Chinese添加一个"出生地"属性,它的值是一个数组。
Chinese.birthPlaces = ['北京','上海','香港'];
通过extendCopy()函数,Doctor继承了Chinese。
var Doctor = extendCopy(Chinese);
//然后,我们为Doctor的"出生地"添加一个城市:
Doctor.birthPlaces.push('厦门');
console.log(Doctor.birthPlaces); //北京, 上海, 香港, 厦门
console.log(Chinese.birthPlaces); //北京, 上海, 香港, 厦门
//所以,extendCopy()只是拷贝基本类型的数据,我们把这种拷贝叫做"浅拷贝"。这是早期jQuery实现继承的方式。
三、深拷贝
所谓”深拷贝”,就是能够实现真正意义上的数组和对象的拷贝。它的实现并不难,只要递归调用”浅拷贝”就行了
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
if (typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {};
deepCopy(p[i], c[i]);
} else {
c[i] = p[i];
}
}
return c;
}
使用:
var Doctor = deepCopy(Chinese);
//现在,给父对象加一个属性,值为数组。然后,在子对象上修改这个属性
Chinese.birthPlaces = ['北京','上海','香港'];
Doctor.birthPlaces.push('厦门');
//这时,父对象就不会受到影响了。
console.log(Doctor.birthPlaces); //北京, 上海, 香港, 厦门
console.log(Chinese.birthPlaces); //北京, 上海, 香港
//目前,jQuery库使用的就是这种继承方法。
执行上下文(Execution context 作用域)
function (context) {}
执行上下文三个重要属性:
- VO(variate object) 只能在全局作用域访问,局部作用域通过AO(active variable object)访问。
- 作用域链
- this
js中什么是变量?大概三种。
- var/const/let
- function fn () {}
- function fn (a,b) {} 函数形参
深度理解变量声明;
- 在生成执行上下文时,会有两个阶段。
- 第一个阶段是创建的阶段(具体步骤是创建 VO),JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined,
- 所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。
函数优先于变量提升
let会形成临时死区: