目录
- vue2 的双向绑定的原理
- vue3 的双向绑定原理
- vue 的生命周期
- vue 子组件为何不能修改父组件的值
- js delete 删除数组的某一个值会怎么样
- vue 和 react 的 diff 算法
- 什么是闭包
- 原型链
- this指向
vue2 的双向绑定的原理
以下是 Vue 2 双向绑定的原理:
1. 核心概念
Vue 2 的双向绑定是通过数据劫持和发布订阅模式实现的。它允许数据和视图之间的双向通信,即数据的改变可以更新视图,视图的操作也可以更新数据。
2. 实现步骤
数据劫持:
Vue 2 使用 Object.defineProperty
对数据对象的属性进行劫持,在属性被访问或修改时添加自定义的行为。
javascript
function defineReactive(obj, key, value) {
let dep = new Dep(); // 创建一个空的订阅者列表
Object.defineProperty(obj, key, {
enumerable: true, // 允许属性被枚举
configurable: true, // 允许属性被修改
get: function() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set: function(newVal) {
if (value!== newVal) {
value = newVal;
dep.notify();
}
}
});
}
在这个函数中:
defineReactive
函数使用Object.defineProperty
对obj
的key
属性进行定义。get
方法:当该属性被访问时,如果Dep.target
存在(在 Vue 中,Dep.target
通常是当前正在编译的Watcher
),将其添加到dep
的订阅者列表中。set
方法:当该属性被修改时,如果新值与旧值不同,更新属性值并通知dep
的订阅者列表中的所有订阅者。
依赖收集:
每个组件实例都有一个 Watcher
对象,它是一个订阅者,当组件的模板中使用到某个数据时,会触发该数据的 get
方法,将该 Watcher
添加到该数据的订阅者列表中。
javascript
class Watcher {
constructor(vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get();
}
get() {
Dep.target = this;
let value = this.vm[this.exp];
Dep.target = null;
return value;
}
update() {
let newValue = this.vm[this.exp];
let oldValue = this.value;
if (newValue!== oldValue) {
this.value = newValue;
this.cb.call(this.vm, newValue, oldValue);
}
}
}
在这个类中:
Watcher
的get
方法会将自己设置为Dep.target
,并访问数据,触发数据的get
方法,从而将自己添加到该数据的订阅者列表中。update
方法会在数据更新时被调用,调用回调函数cb
更新视图。
发布订阅模式:
Dep
类是一个简单的发布者,它维护一个订阅者列表,并在数据变化时通知订阅者。
javascript
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
removeSub(sub) {
this.subs = this.subs.filter(s => s!== sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
在这个类中:
constructor
方法创建一个空的订阅者列表。addSub(sub)
方法添加一个订阅者。removeSub(sub)
方法移除一个订阅者。notify()
方法通知所有订阅者更新。
3. 整体流程
- 当 Vue 实例化时,会对
data
属性中的数据进行遍历,使用defineReactive
进行数据劫持。 - 当编译模板时,会创建
Watcher
对象,在使用数据时触发数据的get
方法,将Watcher
添加到该数据的订阅者列表中。 - 当数据发生变化时,触发数据的
set
方法,通知Watcher
进行更新,Watcher
调用update
方法更新视图。
4. 示例代码
html
<div id="app">
<input v-model="message">
{{ message }}
</div>
javascript
function Vue(options) {
this.data = options.data;
observe(this.data);
new Compile('#app', this);
}
function observe(data) {
if (!data || typeof data!== 'object') return;
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key]);
});
}
function Compile(el, vm) {
this.vm = vm;
this.el = document.querySelector(el);
this.compileElement(this.el);
}
Compile.prototype.compileElement = function (el) {
let childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
if (node.nodeType === 1) {
// 元素节点
this.compileElement(node);
} else if (node.nodeType === 3) {
// 文本节点
this.compileText(node);
}
});
};
Compile.prototype.compileText = function (node) {
let reg = /\{\{(.*?)\}\}/g;
let value = node.textContent;
if (reg.test(value)) {
let exp = RegExp.$1.trim();
node.textContent = value.replace(reg, this.vm.data[exp]);
new Watcher(this.vm, exp, function (newVal) {
node.textContent = value.replace(reg, newVal);
});
}
};
let app = new Vue({
data: {
message: 'Hello, Vue!'
}
});
代码解释
Vue
函数:
- 接收
options
,初始化data
属性,并调用observe
对数据进行观察。 - 调用
Compile
进行模板编译。
observe
函数:
- 遍历
data
对象,对每个属性使用defineReactive
进行数据劫持。
Compile
类:
- 编译模板元素,对于文本节点,如果存在
{``{...}}
插值表达式,使用Watcher
进行数据监听和更新。
Watcher
类:
- 在实例化时,会将自己添加到数据的订阅者列表中,并在更新时更新视图。
Dep
类:
- 作为发布者,管理订阅者列表,在数据更新时通知订阅者。
面试回答示例
"Vue 2 的双向绑定是通过数据劫持和发布订阅模式实现的。首先,使用 Object.defineProperty
对数据对象的属性进行劫持,在属性的 get
方法中进行依赖收集,将使用该数据的 Watcher
订阅者添加到该数据的订阅者列表中,在 set
方法中,当数据发生变化时通知订阅者列表中的 Watcher
。Watcher
是一个订阅者,负责更新视图,它会在实例化时将自己添加到数据的订阅者列表中,并在更新时调用回调函数更新视图。Dep
类是一个发布者,负责维护订阅者列表并通知订阅者更新。当 Vue 实例化时,会对 data
中的数据进行劫持,在模板编译时,会创建 Watcher
进行依赖收集,从而实现数据和视图的双向绑定。这样,当数据变化时,视图会更新;当用户操作视图(如通过 v-model
)时,会触发数据的更新,形成双向绑定的效果。"
通过这样的解释,可以向面试官展示你对 Vue 2 双向绑定原理的深入理解,包括数据劫持、依赖收集、发布订阅模式的使用以及整体的工作流程。
2. vue3 的双向绑定原理
Vue 3 的双向绑定机制相比 Vue 2 有了显著改进,主要通过 组合式 API 和 Proxy 实现。以下是完整解析:
一、核心机制演进
特性 | Vue 2 | Vue 3 |
---|---|---|
响应式基础 | Object.defineProperty | Proxy |
检测范围 | 对象属性 | 完整对象 |
数组检测 | 需特殊方法 | 原生支持 |
性能 | 递归转换属性 | 惰性代理 |
二、响应式系统核心实现
1. reactive() - 对象响应化
javascript
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key) // 依赖收集
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
trigger(target, key) // 触发更新
return true
}
})
}
2. ref() - 原始值响应化
javascript
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newVal) {
value = newVal
trigger(refObject, 'value')
}
}
return refObject
}
三、依赖收集与触发流程
-
依赖收集阶段:
组件渲染 读取响应式数据 触发getter 将当前effect存入dep
-
触发更新阶段:
数据变更 触发setter 从dep取出effect 执行effect重新渲染
四、v-model 双向绑定实现
组件示例:
html
<CustomInput v-model="searchText" />
<!-- 等价于 -->
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
组件实现:
javascript
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
const emitUpdate = (e) => {
emit('update:modelValue', e.target.value)
}
五、性能优化策略
-
编译时优化:
- 静态节点提升(Hoist Static)
- 补丁标志(Patch Flags)
- 树结构拍平(Tree Flattening)
-
响应式优化:
- 依赖关系缓存(effect缓存)
- 批量异步更新(nextTick合并)
-
源码结构优化:
javascript// 惰性代理示例 function shallowReactive(obj) { const proxy = new Proxy(obj, handlers) // 不立即递归代理嵌套对象 return proxy }
六、与 Vue 2 的对比升级
-
数组处理改进:
javascript// Vue 2 需要特殊处理 this.$set(this.items, index, newValue) // Vue 3 直接操作 state.items[index] = newValue // 自动触发更新
-
动态属性检测:
javascript// Vue 2 无法检测新增属性 this.$set(this.obj, 'newProp', value) // Vue 3 自动检测 state.newProp = value // 自动响应
七、开发注意事项
-
响应式丢失场景:
javascript// 解构会导致响应式丢失 const { x, y } = reactive({ x: 1, y: 2 }) // 正确做法 const pos = reactive({ x: 1, y: 2 }) const { x, y } = toRefs(pos)
-
性能敏感操作:
javascript// 大数据量使用shallowRef/shallowReactive const bigList = shallowRef([]) // 非响应式数据使用markRaw const foo = markRaw({ complex: object })
Vue 3 的双向绑定通过 Proxy 实现了更精细的依赖跟踪,配合编译时优化,在保证开发体验的同时提供了更好的运行时性能。理解其原理有助于编写更高效的 Vue 代码。
3. vue 的生命周期
一、Vue 2 和 Vue 3 生命周期对比
生命周期钩子对照表
Vue 2 选项式API | Vue 3 组合式API | 触发时机描述 |
---|---|---|
beforeCreate |
无直接对应 | 实例初始化前,data/methods未初始化 |
created |
setup() |
实例创建完成,data/methods可用 |
beforeMount |
onBeforeMount |
挂载开始前,DOM尚未生成 |
mounted |
onMounted |
挂载完成,DOM已生成 |
beforeUpdate |
onBeforeUpdate |
数据变化导致DOM更新前 |
updated |
onUpdated |
数据变化导致DOM更新后 |
beforeDestroy |
onBeforeUnmount |
实例销毁前(vue3改名更准确) |
destroyed |
onUnmounted |
实例销毁后(vue3改名更准确) |
activated |
onActivated |
keep-alive组件激活时 |
deactivated |
onDeactivated |
keep-alive组件停用时 |
errorCaptured |
onErrorCaptured |
捕获子孙组件错误时 |
二、生命周期完整流程图示
是 否 是 初始化事件和生命周期 beforeCreate 初始化注入和响应式 created 编译模板/生成render函数 beforeMount 创建VDOM并渲染 mounted 数据变化? beforeUpdate 重新渲染VDOM和DOM updated 是否调用销毁方法? beforeUnmount 移除DOM和事件监听 unmounted
三、组合式API中的使用示例
javascript
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
export default {
setup() {
onBeforeMount(() => {
console.log('挂载前')
})
onMounted(() => {
console.log('挂载完成')
})
onBeforeUpdate(() => {
console.log('更新前')
})
onUpdated(() => {
console.log('更新完成')
})
onBeforeUnmount(() => {
console.log('销毁前')
})
onUnmounted(() => {
console.log('销毁完成')
})
}
}
四、关键生命周期详解
1. created / setup
- 数据访问:可访问响应式data、computed等
- 异步请求:适合在此发起初始数据请求
- 注意:此时DOM未生成,不可操作DOM
2. mounted
- DOM操作:可安全操作DOM元素
- 子组件:保证所有子组件也已挂载
- 典型用途:初始化第三方库(如图表库)
3. updated
- DOM状态:可获取更新后的DOM状态
- 避免操作:不要在此修改状态,可能导致无限循环
- 性能优化:复杂操作建议使用watch替代
4. unmounted
- 清理工作:移除事件监听、定时器、取消网络请求等
- 第三方库:销毁插件实例(如销毁ECharts实例)
五、特殊场景生命周期
Keep-alive组件
javascript
onActivated(() => {
console.log('组件被激活')
})
onDeactivated(() => {
console.log('组件被缓存')
})
错误捕获
javascript
onErrorCaptured((err, instance, info) => {
console.error('捕获到错误:', err)
return false // 阻止错误继续向上传播
})
六、最佳实践建议
-
异步请求:
- 初始化数据:created/setup + async/await
- 避免在mounted中请求,会延迟首次渲染
-
DOM操作:
- 必须在mounted及之后生命周期进行
- 使用nextTick确保DOM更新完成
-
内存管理:
javascriptonUnmounted(() => { clearInterval(timer) eventBus.off('eventName', handler) chartInstance.dispose() })
-
性能优化:
- 避免在updated中进行复杂计算
- 大量数据更新考虑使用虚拟滚动
理解Vue生命周期有助于在正确时机执行代码,避免常见错误,并优化应用性能。Vue 3的组合式API提供了更灵活的生命周期管理方式。
4. vue 子组件为何不能修改父组件的值
一、核心原因:单向数据流原则
Vue 强制实施单向数据流(One-Way Data Flow)设计模式,这是现代前端框架的通用规范:
- 数据所有权明确:父组件拥有数据,子组件只有使用权
- 可预测性:数据变更源头唯一,便于追踪状态变化
- 维护性:避免多组件同时修改导致的混乱
二、直接修改的危害
如果允许子组件直接修改父组件值:
问题类型 | 具体表现 |
---|---|
状态混乱 | 多个子组件同时修改同一状态,难以确定最终值 |
调试困难 | 数据变更来源不明确,错误难以追踪 |
组件耦合 | 子组件必须了解父组件内部实现,破坏组件独立性 |
性能优化障碍 | Vue的响应式系统难以优化变更检测 |
三、Vue 的解决方案
1. Props + Events 标准模式
vue
<!-- 父组件 -->
<template>
<Child :value="parentValue" @update="handleUpdate" />
</template>
<script>
export default {
data() {
return { parentValue: 1 }
},
methods: {
handleUpdate(newVal) {
this.parentValue = newVal
}
}
}
</script>
<!-- 子组件 -->
<template>
<button @click="$emit('update', value + 1)">+1</button>
</template>
<script>
export default {
props: ['value']
}
</script>
2. v-model 语法糖(Vue 2)
vue
<!-- 父组件 -->
<Child v-model="parentValue" />
<!-- 等价于 -->
<Child :value="parentValue" @input="parentValue = $event" />
3. v-model 参数(Vue 3)
vue
<!-- 父组件 -->
<Child v-model:title="pageTitle" />
<!-- 子组件 -->
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>
4. .sync 修饰符(Vue 2)
vue
<!-- 父组件 -->
<Child :value.sync="parentValue" />
<!-- 子组件 -->
this.$emit('update:value', newValue)
四、特殊情况的处理方案
1. 需要直接修改的情况
javascript
// 子组件
props: {
value: {
type: Object,
default: () => ({})
}
},
methods: {
modifyParent() {
const newObj = JSON.parse(JSON.stringify(this.value))
newObj.property = 'new value'
this.$emit('update', newObj)
}
}
2. 使用 Vuex/Pinia 状态管理
javascript
// store
export const useStore = defineStore('main', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++
}
}
})
// 任何组件
import { useStore } from './store'
const store = useStore()
store.increment() // 通过集中式管理修改状态
五、底层原理分析
Vue 通过以下机制阻止直接修改:
-
Prop 代理 :Vue 在子组件实例上创建的 props 是只读代理
javascript// Vue 内部实现简化 const childProps = {} Object.defineProperty(childProps, 'value', { get() { return parentValue }, set() { if (process.env.NODE_ENV !== 'production') { warn(`Avoid mutating prop directly`) } } })
-
开发环境警告:在非生产环境下,Vue 会检测并警告 props 的直接修改
六、最佳实践建议
-
严格遵循单向数据流
- 父级通过 props 向下传递数据
- 子级通过事件向上通知变更意图
-
复杂场景处理方案:
场景 解决方案 需要修改父级对象属性 触发事件让父级自己修改 多层级组件通信 使用provide/inject 全局状态 使用Vuex/Pinia 临时本地修改 使用computed或ref拷贝prop值 -
代码规范检查:
javascript// ESLint规则推荐 "vue/no-mutating-props": "error"
理解并遵守这一设计原则,可以构建出更健壮、可维护的Vue应用架构。
5. js delete 删除数组的某一个值会怎么样
一、基本行为表现
当使用 delete
操作符删除数组元素时:
javascript
const arr = ['a', 'b', 'c', 'd'];
delete arr[1]; // 删除索引1的元素'b'
console.log(arr); // ['a', empty, 'c', 'd']
console.log(arr.length); // 4
console.log(arr[1]); // undefined
二、关键特性解析
1. 不会改变数组长度
delete
只会将指定位置的元素变为 empty 空位(稀疏数组)- 数组的
length
属性保持不变
2. 元素访问结果
- 被删除的位置会返回
undefined
- 但该位置仍在数组中(表现为 empty 而非 undefined)
javascript
console.log(1 in arr); // false (表示索引1不存在)
console.log(arr.hasOwnProperty(1)); // false
3. 遍历行为差异
不同遍历方法对空位的处理:
方法 | 处理方式 | 示例结果 |
---|---|---|
for循环 | 会处理空位(值为undefined) | 'a', undefined, 'c', 'd' |
forEach | 跳过空位 | 'a', 'c', 'd' |
map | 跳过空位 | ['a', empty, 'c', 'd'] |
filter | 移除空位 | ['a', 'c', 'd'] |
三、与 splice 方法对比
操作 | delete arr[i] | arr.splice(i, 1) |
---|---|---|
数组长度 | 不变 | 减少 |
空位产生 | 会 | 不会 |
索引重排 | 不重排 | 后续元素前移 |
适用场景 | 需要保留位置/长度时 | 需要真正移除元素时 |
四、实际应用建议
1. 应该使用 delete 的场景
- 需要保持数组长度不变(如游戏地图格子)
- 需要保留元素位置信息(如时间序列数据)
2. 不应该使用 delete 的场景
- 需要真正移除元素时(应改用
splice
) - 需要保证数组连续性的操作前(如
JSON.stringify
会忽略空位)
3. 正确的元素删除方法
javascript
// 方法1:splice (修改原数组)
arr.splice(index, 1);
// 方法2:filter (创建新数组)
const newArr = arr.filter((_, i) => i !== index);
// 方法3:设置length (截断数组)
arr.length = newLength;
五、特殊注意事项
-
类型化数组:
javascriptconst typedArray = new Uint8Array([1, 2, 3]); delete typedArray[1]; // 会将索引1位置设为0
-
性能考虑:
delete
操作比splice
快(不涉及元素移动)- 但后续操作空位数组可能更慢
-
Vue/React 响应式:
- 在响应式框架中,
delete
可能不会触发视图更新 - 应使用框架提供的删除方法(如 Vue 的
$delete
)
- 在响应式框架中,
六、底层原理
delete
操作符实际执行的是:
- 将指定属性的
[[Configurable]]
特性设为 true - 删除该属性
- 返回 true(即使属性不存在)
对于数组:
- 数组是特殊对象,索引是属性名
delete arr[i]
等同于删除对象的属性
理解 delete
对数组的这种特殊行为,有助于避免在需要真正移除元素时错误使用它。大多数情况下,splice
或 filter
才是更合适的选择。
6. vue 和 react 的 diff 算法
Vue和React作为流行的前端框架,都使用虚拟DOM(Virtual DOM)来提升渲染性能,而Diff算法是虚拟DOM的核心,它能找出新旧虚拟DOM之间的差异,从而只更新需要更新的真实DOM部分。下面为你分别介绍它们的Diff算法。
Vue的Diff算法
Vue的Diff算法采用了双指针和key的策略,通过比较新旧虚拟节点的差异,最小化DOM操作。具体步骤如下:
- 同级比较:只对同一层级的节点进行比较。
- 节点类型比较:若节点类型不同,直接替换。
- key比较:若有key,使用key进行更高效的比较和复用。
- 双指针遍历:使用首尾双指针遍历新旧节点列表。
React的Diff算法
React的Diff算法基于几个启发式策略,旨在减少比较次数,提高性能。具体步骤如下:
- 同级比较:和Vue一样,只比较同一层级的节点。
- 节点类型比较:节点类型不同时,直接替换。
- key比较:使用key来标识列表中的元素,便于复用和移动。
- 列表比较:采用双循环遍历新旧节点列表。
代码示例
以下是一个简单的Vue和React组件示例,来帮助你理解Diff算法的应用。
Vue示例
vue
<template>
<div>
<ul>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
<button @click="updateList">Update List</button>
</div>
</template>
<script>
export default {
data() {
return {
list: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
]
};
},
methods: {
updateList() {
this.list = [
{ id: 1, name: 'Updated Item 1' },
{ id: 2, name: 'Updated Item 2' },
{ id: 4, name: 'New Item 4' }
];
}
}
};
</script>
React示例
jsx
import React, { useState } from 'react';
const App = () => {
const [list, setList] = useState([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
]);
const updateList = () => {
setList([
{ id: 1, name: 'Updated Item 1' },
{ id: 2, name: 'Updated Item 2' },
{ id: 4, name: 'New Item 4' }
]);
};
return (
<div>
<ul>
{list.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<button onClick={updateList}>Update List</button>
</div>
);
};
export default App;
总结
- Vue:采用双指针和key策略,能更精准地找出差异,更新DOM。
- React:基于启发式策略,通过双循环遍历列表,性能也较为出色。
两者都运用了虚拟DOM和Diff算法,减少了不必要的DOM操作,提高了渲染性能。在实际开发中,合理使用key能进一步优化Diff算法的性能。
7. 什么是闭包
在 JavaScript 里,闭包是一个强大且重要的概念。下面为你详细解释 JavaScript 中的闭包。
定义
闭包是指有权访问另一个函数作用域中变量的函数。简单来说,即使外部函数执行完毕,其作用域内的变量也不会被销毁,而是会被闭包"捕获"并保留,使得这些变量能在外部函数之外被访问和修改。
形成条件
闭包的形成需要满足以下两个关键条件:
- 函数嵌套:必须存在一个外部函数和至少一个内部函数。
- 内部函数引用外部函数的变量:内部函数使用了外部函数作用域内的变量。
作用
闭包在 JavaScript 中有多种重要作用:
- 读取函数内部的变量:外部函数执行结束后,其内部变量会被闭包保存,可通过闭包在外部访问这些变量。
- 让这些变量的值始终保持在内存中:变量不会因外部函数执行完毕而被销毁,而是持续存在于内存里,方便后续使用。
- 封装私有变量和方法:可以使用闭包来创建私有变量和方法,避免全局作用域的污染。
示例
javascript
function outerFunction() {
// 外部函数的变量
let counter = 0;
// 内部函数,形成闭包
function innerFunction() {
counter++;
return counter;
}
return innerFunction;
}
// 创建闭包实例
const closure = outerFunction();
// 调用闭包
console.log(closure()); // 输出: 1
console.log(closure()); // 输出: 2
console.log(closure()); // 输出: 3
在这个示例中,outerFunction
是外部函数,innerFunction
是内部函数。innerFunction
引用了 outerFunction
作用域内的 counter
变量,从而形成了闭包。当 outerFunction
执行完毕后,counter
变量不会被销毁,而是被 innerFunction
捕获并保留。每次调用 closure
函数时,counter
变量的值都会增加。
闭包的潜在问题
虽然闭包功能强大,但也可能带来一些问题,比如内存泄漏。由于闭包会让变量一直存在于内存中,如果闭包使用不当,可能会导致内存占用过高。因此,在使用闭包时,需要注意内存的使用情况,避免不必要的内存消耗。
8. 原型链
原型链是JavaScript中实现继承和对象属性查找的一种机制。以下是关于原型链的详细介绍:
原型的概念
在JavaScript中,每个对象都有一个原型(prototype
)。原型也是一个对象,它可以包含一些属性和方法。当访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript引擎就会去它的原型对象中查找。
原型链的形成
- 所有的对象都默认从
Object.prototype
继承属性和方法。例如,toString()
、valueOf()
等方法就是从Object.prototype
继承来的。 - 当创建一个函数时,JavaScript会自动为这个函数添加一个
prototype
属性,这个属性指向一个对象,称为该函数的原型对象。当使用构造函数创建一个新对象时,新对象的__proto__
属性(也称为原型链指针)会指向构造函数的原型对象。这样就形成了一条链,从新对象开始,通过__proto__
不断指向它的原型对象,直到Object.prototype
,这条链就是原型链。
原型链的作用
- 实现继承 :通过原型链,一个对象可以继承另一个对象的属性和方法。例如,定义一个
Animal
构造函数,再定义一个Dog
构造函数,让Dog
的原型指向Animal
的实例,这样Dog
的实例就可以继承Animal
的属性和方法。 - 属性和方法的共享 :多个对象可以共享原型对象上的属性和方法,节省内存空间。比如,所有数组对象都共享
Array.prototype
上的push()
、pop()
等方法。
示例代码
javascript
// 定义一个构造函数
function Person(name) {
this.name = name;
}
// 在构造函数的原型上添加方法
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
// 创建一个Person的实例
const person1 = new Person('John');
// 访问实例的属性和方法,先在实例本身查找,找不到就去原型上查找
person1.sayHello(); // 输出 "Hello, my name is John"
console.log(person1.__proto__ === Person.prototype); // 输出 true
在这个例子中,person1
是 Person
构造函数的实例,它的 __proto__
属性指向 Person.prototype
。当调用 person1.sayHello()
时,由于 person1
本身没有 sayHello
方法,JavaScript会沿着原型链在 Person.prototype
上找到该方法并执行。
9. this指向
在 JavaScript 里,this
是一个特殊的关键字,它的指向取决于函数的调用方式。下面将为你详细介绍 this
在不同情况下的指向。
全局作用域中 this
的指向
在全局作用域里,this
指向全局对象。在浏览器环境中,全局对象是 window
;在 Node.js 环境里,全局对象是 global
。
javascript
console.log(this === window); // 在浏览器环境中输出 true
this.globalVariable = 'I am a global variable';
console.log(window.globalVariable); // 输出: I am a global variable
函数作为普通函数调用时 this
的指向
当函数作为普通函数调用时,this
指向全局对象(在严格模式下,this
是 undefined
)。
javascript
function normalFunction() {
console.log(this);
}
normalFunction(); // 在非严格模式下输出 window,在严格模式下输出 undefined
函数作为对象方法调用时 this
的指向
当函数作为对象的方法调用时,this
指向调用该方法的对象。
javascript
const person = {
name: 'John',
sayHello: function() {
console.log(`Hello, my name is ${this.name}`);
}
};
person.sayHello(); // 输出: Hello, my name is John
构造函数中 this
的指向
当使用 new
关键字调用函数时,该函数就成为了构造函数,此时 this
指向新创建的对象。
javascript
function Person(name) {
this.name = name;
this.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
}
const john = new Person('John');
john.sayHello(); // 输出: Hello, my name is John
call
、apply
和 bind
方法对 this
指向的影响
call
方法 :call
方法可以调用一个函数,并且可以指定该函数内部this
的指向。
javascript
function greet(message) {
console.log(`${message}, my name is ${this.name}`);
}
const person1 = { name: 'Alice' };
greet.call(person1, 'Hi'); // 输出: Hi, my name is Alice
apply
方法 :apply
方法和call
方法类似,不同之处在于apply
方法接受一个数组作为参数。
javascript
function greet(message) {
console.log(`${message}, my name is ${this.name}`);
}
const person2 = { name: 'Bob' };
greet.apply(person2, ['Hello']); // 输出: Hello, my name is Bob
bind
方法 :bind
方法会创建一个新的函数,在调用时会将this
绑定到指定的对象上。
javascript
function greet(message) {
console.log(`${message}, my name is ${this.name}`);
}
const person3 = { name: 'Charlie' };
const boundGreet = greet.bind(person3);
boundGreet('Hey'); // 输出: Hey, my name is Charlie
箭头函数中 this
的指向
箭头函数没有自己的 this
,它的 this
继承自外层函数。
javascript
const obj = {
name: 'David',
sayHello: function() {
const arrowFunction = () => {
console.log(`Hello, my name is ${this.name}`);
};
arrowFunction();
}
};
obj.sayHello(); // 输出: Hello, my name is David
理解 this
关键字的指向是 JavaScript 中的一个重要部分,不同的调用方式会导致 this
指向不同的对象。在实际开发中,要根据具体情况来确定 this
的指向。